内置对并行计算的支持:OpenCL

OpenCL 是一种开放并行编程标准,它允许你创建在现代处理器的多个核心上同时执行的应用程序,这些处理器具有不同的架构,特别是图形 (GPU) 或中央 (CPU) 处理器。

换句话说,OpenCL 允许你使用中央处理器的所有核心或显卡的所有计算能力来计算一个任务,最终减少了程序的执行时间。因此,OpenCL 的使用对于计算密集型任务非常有用,但需要注意的是,解决这些任务的算法必须可分为并行线程。这些包括,例如训练神经网络、傅立叶变换或求解大维数的方程组。

例如,关于交易细节,可以使用脚本、指标或 EA 交易来提高性能,该脚本、指标或 EA 交易对多个符号和时间范围的历史数据进行复杂而冗长的分析,并且每个符号和时间范围的计算都不依赖于其他符号和时间范围。

与此同时,初学者经常会有这样一个问题,即是否可以使用 OpenCL 来加速 EA 交易的测试和优化。答案是否定的。测试重现了连续交易的真实过程,因此每下一根柱线或逐笔交易明细都取决于前一根柱线或逐笔交易明细的结果,这使得一次计算无法并行化。至于优化,测试程序的代理只支持 CPU 核心。这是由于报价或逐笔交易明细的全面分析、跟踪头寸和计算余额及权益的复杂性。而如果不考虑复杂性,则可以将所有模拟交易环境的计算和所需的可靠性转移到 OpenCL,从而在显卡核心上实现自己的优化引擎。

OpenCL 的意思是开放计算语言。它类似于 C 和 C++ 语言,因此也类似于 MQL5。然而,为了编写(即“编译”)一个 OpenCL 程序,将输入数据传递给这个程序,并在多个核心上并行运行程序,获取计算结果,则需要使用一个特殊的编程接口(一组函数)。这个 OpenCL API 也可用于希望实现并行执行的MQL程序。

要使用 OpenCL,没有必要在电脑上安装显卡,因为有一个中央处理器就足够了。在任何情况下,都需要制造商提供的特殊驱动程序(要求 OpenCL 1.1 版及更高版本)。如果你的电脑装有使用了显卡的游戏或其他软件(例如,科技、视频编辑器等),那么必要的软件层很可能已经可用。这可以通过使用 OpenCL 调用在终端中运行 MQL 程序来检查(至少是终端交付的一个简单例子,参见下文)。

如果没有 OpenCL 支持,你将在日志中看到一个错误。

OpenCL OpenCL not found, please install OpenCL drivers

如果你的电脑上装有合适的设备,并且已启用 OpenCL 支持,则终端将显示一条消息,显示该设备(也可能有多个设备)的名称和类型。例如:

OpenCL Device #0: CPU GenuineIntel Intel(R) Core(TM) i7-2700K CPU @ 3.50GHz with OpenCL 1.1 (8 units, 3510 MHz, 16301 Mb, version 2.0, rating 25)
OpenCL Device #1: GPU Intel(R) Corporation Intel(R) UHD Graphics 630 with OpenCL 2.1 (24 units, 1200 MHz, 13014 Mb, version 26.20.100.7985, rating 73)

为各种设备安装驱动程序的过程见 mql5.com 文章中的说明。。可支持来自 Intel、AMD、ATI 和 Nvidia 的当下最流行的设备。

在核心数量和分布式计算速度方面,中央处理器的性能明显不如显卡,但一个好的多核中央处理器就足以显著提高性能。

重要事项:如果你的计算机装有支持 OpenCL 的显卡,那么就不需要在 CPU 上安装 OpenCL 软件模拟!

OpenCL 设备驱动程序自动化核心之间的计算分布。例如,如果你需要对不同向量执行一百万次相同类型的计算,而只有一千个核心可用,那么当前一个任务准备就绪并且核心被释放时,驱动程序将自动启动下一个任务。

使用上述 OpenCL API 的函数,在 MQL 程序中设置 OpenCL 运行时环境的准备操作仅执行一次。

  1. 为 OpenCL 程序创建上下文(选择设备,如显卡、CPU 或任何可用设备):CLContextCreate(CL_USE_ANY)。该函数将返回一个上下文描述符(一个整数,我们将其表示为有条件的 ContextHandle)。
  2. 在接收到的上下文中创建 OpenCL 程序:使用 CLProgramCreate 函数调用,基于 OpenCL 语言的源代码进行编译,代码的文本通过参数 Source :CLProgramCreate(ContextHandle, Source, BuildLog) 传递给该函数。该函数将返回程序句柄(整数 ProgramHandle)。这里需要注意的是,在该程序的源代码内部,必须包含标有特殊关键字 __kernel(或 kernel)的函数(至少一个):它们包含要并行化的算法部分(参见下面的例子)。当然,为了简化(分解源代码),程序员可以将内核函数的逻辑子任务划分到其他辅助函数中,并从内核中调用:同时,不需要用 kernel 这个词来标记辅助函数。
  3. 按 OpenCL 程序代码中标记为内核形成的函数的名称注册要执行的内核:CLKernelCreate(ProgramHandle, KernelName)。调用该函数将返回一个内核句柄(一个整数,比如 KernelHandle)。你可以在 OpenCL 代码中创建多个不同的函数,并将它们注册为不同的内核。
  4. 如有必要,为按引用传递给内核的数据数组,以及为返回值/数组创建缓冲区:CLBufferCreate(ContextHandle, Size * sizeof(double), CL_MEM_READ_WRITE),等。缓冲区也通过描述符进行标识和管理。

然后,如有必要(例如,在指标或 EA 交易事件处理程序中),将根据以下方案直接执行一次或多次计算:

  1. 使用 CLSetKernelArg(KernelHandle,...) 和/或 CLSetKernelArgMem(KernelHandle,..., BufferHandle) 传递输入数据和/或绑定输入/输出缓冲区。第一个函数提供标量值的设置,第二个函数相当于按引用传递或接收值(或值的数组)。在此阶段,数据从 MQL5 迁移到 OpenCL 执行核心。CLBufferWrite(BufferHandle,...) 将数据写入缓冲区。内核执行期间,OpenCL 程序将可以使用参数和缓冲区。
  2. 通过调用特定内核 CLExecute(KernelHandle,...) 执行并行计算。内核函数能够将其工作结果写入输出缓冲区。
  3. 使用 CLBufferRead(BufferHandle) 获取结果。在此阶段,数据从 OpenCL 移回到 MQL5。

计算完成后,应释放所有描述符:CLBufferFree(BufferHandle)CLKernelFree(KernelHandle)CLProgramFree(ProgramHandle)CLContextFree(ContextHandle)

该顺序通常如下图所示。

MQL 程序和 OpenCL 附件之间的交互方案

MQL 程序和 OpenCL 附件之间的交互方案

建议在单独的文本文件中编写 OpenCL 源代码,然后使用 资源变量将文件连接到 MQL5 程序

终端提供的标准头文件库包含一个用于 OpenCL 的包装类:MQL5/Include/OpenCL/OpenCL.mqh

OpenCL 的使用示例可以在 MQL5/Scripts/Examples/OpenCL/ 文件夹中找到。特别是 MQL5/Scripts/Examples/OpenCL/Double/Wavelet.mq5 脚本,它产生时间序列的小波变换(你可以根据随机 Weierstrass 模型或当前金融工具的价格增量取一条人工曲线)。在任何情况下,算法的初始数据都是一个数组,它是一组二维图像。

运行这个脚本时,与运行任何其他包含 OpenCL 代码的 MQL 程序一样,终端将选择最快的设备(如果有多个设备,并且程序没有选定特定设备,或者之前没有定义特定设备的话)。有关这方面的信息显示在 Journal 选项卡(终端日志,而不是 EA 交易)中。

Scripts script Wavelet (EURUSD,H1) loaded successfully
OpenCL  device #0: GPU NVIDIA Corporation NVIDIA GeForce GTX 1650 with OpenCL 3.0 (16 units, 1560 MHz, 4095 Mb, version 512.72)
OpenCL  device #1: GPU Intel(R) Corporation Intel(R) UHD Graphics 630 with OpenCL 3.0 (24 units, 1150 MHz, 6491 Mb, version 27.20.100.8935)
OpenCL  device performance test started
OpenCL  device performance test successfully finished
OpenCL  device #0: GPU NVIDIA Corporation NVIDIA GeForce GTX 1650 with OpenCL 3.0 (16 units, 1560 MHz, 4095 Mb, version 512.72, rating 129)
OpenCL  device #1: GPU Intel(R) Corporation Intel(R) UHD Graphics 630 with OpenCL 3.0 (24 units, 1150 MHz, 6491 Mb, version 27.20.100.8935, rating 136)
Scripts script Wavelet (EURUSD,H1) removed

作为执行的结果,脚本以通常的方式(在 CPU 上以串行方式)和并行方式(在 OpenCL 内核上)在 Experts 选项卡中显示计算速度测量记录。

OpenCL: GPU device 'Intel(R) UHD Graphics 630' selected
time CPU=5235 ms, time GPU=125 ms, CPU/GPU ratio: 41.880000

速度比,根据任务的具体情况,可以达到几十。

该脚本在图表上显示原始图像、其增量形式的导数以及小波变换的结果。

原始模拟序列、其增量以及小波变换

原始模拟序列、其增量以及小波变换

请注意,在脚本完成工作后,图形对象仍保留在图表上。需要手动删除它们。

以下是小波变换的 OpenCL 源代码示例,在一个单独的文件 MQL5/Scripts/Examples/OpenCL/Double/Kernels/wavelet.cl 中实现。

// 需要增加两倍的计算精度
//(默认情况下,如果没有这个指令,我们会得到浮点数)
#pragma OPENCL EXTENSION cl_khr_fp64 : enable
   
//辅助功能小程序
double Morlet(const double t)
{
   return exp(-t * t * 0.5) * cos(M_2_PI * t);
}
   
// OpenCL kernel函数
__kernel void Wavelet_GPU(__global double *data, int datacount,
   int x_size, int y_size, __global double *result)
{
   size_t i = get_global_id(0);
   size_t j = get_global_id(1);
   double a1 = (double)10e-10;
   double a2 = (double)15.0;
   double da = (a2 - a1) / (double)y_size;
   double db = ((double)datacount - (double)0.0) / x_size;
   double a = a1 + j * da;
   double b = 0 + i * db;
   double B = (double)1.0;
   double B_inv = (double)1.0 / B;
   double a_inv = (double)1.0 / a;
   double dt = (double)1.0;
   double coef = (double)0.0;
   
   for(int k = 0; k < datacount; k++)
   {
      double arg = (dt * k - b) * a_inv;
      arg = -B_inv * arg * arg;
      coef = coef + exp(arg);
   }
   
   double sum = (float)0.0;
   for(int k = 0; k < datacount; k++)
   {
      double arg = (dt * k - b) * a_inv;
      sum += data[k] * Morlet(arg);
   }
   sum = sum / coef;
   uint pos = (int)(j * x_size + i);
   result[pos] = sum;
}

有关 OpenCL 语法、内置函数和操作原理的完整信息,请访问 科纳斯组织的官方网站。

特别有趣的是,OpenCL 不仅支持常见的标量数字数据类型(从 char 开始,以 double 结束),还支持 (u)charN, (u)shortN, (u)intN, (u)longN, floatN, doubleN 向量,其中 N = {2|3|4|8|16},表示向量的长度。在本例中,没有使用这种方法。

除了上面提到的 kernel 关键字之外,get_global_id 函数在并行计算的组织中起到一个重要的作用:它允许你在代码中找到当前正运行的计算子任务的编号。很明显,不同子任务中的计算应该是不同的(否则使用多个核心就没有意义了)。在本例中,由于任务涉及二维图像的分析,使用两个正交坐标来识别其片段会更加方便。在上面的代码中,我们使用两个调用 get_global_id(0)get_global_id(1) 来获取它们。

实际上,我们在调用 MQL5 函数 CLExecute 时为任务设置了数据维度(参见下文)。

Wavelet.mq5 文件中,OpenCL 源代码包含在指令中:

#resource "Kernels/wavelet.cl" as string cl_program

图像大小通过宏设置:

#define SIZE_X 600
#define SIZE_Y 200

为了管理 OpenCL,使用了带有 COpenCL 类的标准库。它的方法具有相似的名称,并且在内部使用 MQL5 API 中对应的内置 OpenCL 函数。建议先熟悉一下这部分内容。

#include <OpenCL/OpenCL.mqh>

以一种简化的形式(没有错误检查和可视化),启动变换的 MQL 代码如下所示。CWavelet 类中总结了与小波变换相关的操作。

class CWavelet
{
protected:
   ...
   int        m_xsize;              //沿轴的图像大小
   int        m_ysize;
   double     m_wavelet_data_GPU[]; //结果在这里显示
   COpenCL    m_OpenCL;             //包装对象
   ...
};

通过 CalculateWavelet_GPU 方法组织主要的并行计算。

bool CWavelet::CalculateWavelet_GPU(double &data[], uint &time)
{
   int datacount = ArraySize(data); //图像大小(点数)
   
   //根据源代码编译cl程序
   m_OpenCL.Initialize(cl_programtrue);
   
   //从 cl 文件注册一个 kernel 函数
   m_OpenCL.SetKernelsCount(1);
   m_OpenCL.KernelCreate(0"Wavelet_GPU");
   
   //注册两个缓存器,进行输入和输出数据,写入输入数组 register 2 buffers for input and output data, write the input array
   m_OpenCL.SetBuffersCount(2);
   m_OpenCL.BufferFromArray(0data0datacountCL_MEM_READ_ONLY);
   m_OpenCL.BufferCreate(1m_xsize * m_ysize * sizeof(double), CL_MEM_READ_WRITE);
   m_OpenCL.SetArgumentBuffer(000);
   m_OpenCL.SetArgumentBuffer(041);
   
   ArrayResize(m_wavelet_data_GPUm_xsize * m_ysize);
   uint work[2];              //分析一个二维图形的任务 - 即维度 2
   uint offset[2] = {00};   //从最开头开始(或者你可以跳过一些内容)
   work[0] = m_xsize;
   work[1] = m_ysize;
   
   //设置输入数据 
   m_OpenCL.SetArgument(01datacount);
   m_OpenCL.SetArgument(02m_xsize);
   m_OpenCL.SetArgument(03m_ysize);
   
   time = GetTickCount();     /速度测量的切断时间
   //开始在 GPU 上进行计算,二维任务
   m_OpenCL.Execute(02offsetwork);
   
   //将结果存入输出缓存器
   m_OpenCL.BufferRead(1m_wavelet_data_GPU00m_xsize * m_ysize);
   
   time = GetTickCount() - time;
   
   m_OpenCL.Shutdown(); //释放所有资源 - 调用所有必要函数 CL***Free
   return true;
}

在示例的源代码中,有一个注释掉的行调用 PreparePriceData 用于创建一个基于实际价格的输入数组:你可以用 PrepareModelData 调用(该调用生成一个对数)激活它,而不是前面的行。

void OnStart()
{
   int momentum_period = 8;
   double price_data[];
   double momentum_data[];
   PrepareModelData(price_dataSIZE_X + momentum_period);
   
   // PreparePriceData("EURUSD", PERIOD_M1, price_data, SIZE_X + momentum_period);
   
   PrepareMomentumData(price_datamomentum_datamomentum_period);
   ... //系列和增量的可视化
   CWavelet wavelet;
   uint time_gpu = 0;
   wavelet.CalculateWavelet_GPU(momentum_datatime_gpu);
   ... //小波变换结果的可视化
}

一组特殊的错误代码(带有 ERR_OPENCL_ 前缀,从代码 5100 开始,ERR_OPENCL_NOT_SUPPORTED)已分配给 OPENCL 操作。这些代码在 帮助中说明。如果 OpenCL 程序的执行出现问题,则终端会将详细的诊断信息输出到日志,展示错误代码。