Appearance
内置的并行计算支持:OpenCL
OpenCL是一种开放的并行编程标准,它允许创建能够在现代处理器(尤其是图形处理器(GPU)或中央处理器(CPU))的多个核心上同时执行的应用程序,这些处理器的架构各不相同。
换句话说,OpenCL允许你利用中央处理器的所有核心或显卡的所有计算能力来计算同一个任务,最终减少程序的执行时间。因此,OpenCL对于计算密集型任务非常有用,但需要注意的是,解决这些任务的算法必须能够被分割成并行线程。这些任务包括例如神经网络训练、傅里叶变换或求解大维度的方程组等。
例如,就交易的具体情况而言,对于那些对多个交易品种和时间框架的历史数据进行复杂且耗时分析的脚本、指标或智能交易系统(EA),由于对每个品种和时间框架的计算相互独立,使用OpenCL可以提高其性能。
同时,初学者常常会有这样的疑问:是否可以使用OpenCL来加速智能交易系统(EA)的测试和优化?对于这两个问题的答案都是否定的。测试再现的是顺序交易的真实过程,因此每一个后续的K线柱或行情数据点都依赖于前一个的结果,这使得无法对单次运行的计算进行并行化处理。至于优化,测试器的代理仅支持CPU核心。这是因为全面分析报价或行情数据、跟踪持仓以及计算余额和净值的复杂性所致。然而,如果你不畏惧这种复杂性,你可以通过将所有能够以所需的可靠性模拟交易环境的计算转移到OpenCL上,在显卡核心上实现自己的优化引擎。
OpenCL即开放计算语言。它与C和C++语言类似,因此也与MQL5类似。然而,为了准备(“编译”)一个OpenCL程序,向其传递输入数据,在多个核心上并行运行它并获取计算结果,需要使用一个特殊的编程接口(一组函数)。对于希望实现并行执行的MQL程序,这个OpenCL API也是可用的。
使用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设备驱动程序会自动在各个核心之间分配计算任务。例如,如果你需要对不同的向量进行一百万次相同类型的计算,而你只有一千个核心可用,那么驱动程序会在前一个任务完成且核心空闲时自动启动下一个任务。
在MQL程序中设置OpenCL运行时环境的准备操作仅需使用上述OpenCL API的函数执行一次。
- 为OpenCL程序创建上下文(选择一个设备,如显卡、CPU或任何可用设备):
CLContextCreate(CL_USE_ANY)
。该函数将返回一个上下文描述符(一个整数,我们暂且将其表示为ContextHandle
)。 - 在接收到的上下文中创建一个OpenCL程序:使用
CLProgramCreate
函数调用,根据OpenCL语言的源代码对其进行编译,代码文本通过参数Source
传递:CLProgramCreate(ContextHandle, Source, BuildLog)
。该函数将返回程序句柄(整数ProgramHandle
)。这里需要注意的是,在这个程序的源代码内部,必须有至少一个用特殊关键字__kernel
(或简单地写为kernel
)标记的函数:它们包含了要并行化的算法部分(见下面的示例)。当然,为了简化(分解源代码),程序员可以将内核函数的逻辑子任务划分到其他辅助函数中,并从内核中调用它们:同时,不需要用kernel
这个词来标记辅助函数。 - 根据在OpenCL程序代码中标记为内核形成函数之一的名称注册一个内核以执行:
CLKernelCreate(ProgramHandle, KernelName)
。调用这个函数将返回内核的句柄(一个整数,比如说KernelHandle
)。你可以在OpenCL代码中准备许多不同的函数,并将它们注册为不同的内核。 - 如有必要,为通过引用传递给内核的数据数组以及返回的值/数组创建缓冲区:
CLBufferCreate(ContextHandle, Size * sizeof(double), CL_MEM_READ_WRITE)
等等。缓冲区也通过描述符进行标识和管理。
接下来,如有必要,可以一次或多次(例如,在指标或智能交易系统的事件处理程序中)按照以下方案直接执行计算:
- 传递输入数据和/或使用
CLSetKernelArg(KernelHandle,...)
和/或CLSetKernelArgMem(KernelHandle,..., BufferHandle)
绑定输入/输出缓冲区。第一个函数用于设置标量值,第二个函数相当于通过引用传递或接收一个值(或一个值数组)。在这个阶段,数据从MQL5移动到OpenCL执行核心。CLBufferWrite(BufferHandle,...)
将数据写入缓冲区。在执行内核时,参数和缓冲区将对OpenCL程序可用。 - 通过调用特定的内核
CLExecute(KernelHandle,...)
执行并行计算。内核函数将能够将其工作结果写入输出缓冲区。 - 使用
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
脚本,它对时间序列进行小波变换(你可以采用根据随机魏尔斯特拉斯模型生成的人工曲线,或者当前金融工具的价格增量)。无论如何,该算法的初始数据是一个数组,它是一个序列的二维图像。
运行这个脚本时,与运行任何其他包含OpenCL代码的MQL程序一样,终端将选择最快的设备(如果有多个设备,并且在程序本身中没有选择特定设备,或者之前没有定义过)。关于这一点的信息显示在“日志”选项卡中(终端日志,而不是智能交易系统日志)。
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核心上计算)显示记录的计算速度测量值。
OpenCL: GPU device 'Intel(R) UHD Graphics 630' selected
time CPU=5235 ms, time GPU=125 ms, CPU/GPU ratio: 41.880000
速度比根据任务的具体情况可能会达到几十倍。
该脚本在图表上显示原始图像、以增量形式表示的导数以及小波变换的结果。
原始模拟序列、其增量和小波变换
原始模拟序列、其增量和小波变换
请注意,脚本运行完后,图形对象会保留在图表上。需要手动将它们删除。
以下是在单独的文件MQL5/Scripts/Examples/OpenCL/Double/Kernels/wavelet.cl
中实现的小波变换的OpenCL源代码的样子:
c
// 需要提高计算精度为双精度
// (默认情况下,没有这个指令我们得到的是单精度浮点数)
#pragma OPENCL EXTENSION cl_khr_fp64 : enable
// 辅助函数Morlet
double Morlet(const double t)
{
return exp(-t * t * 0.5) * cos(M_2_PI * t);
}
// OpenCL内核函数
__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语法、内置函数和操作原理的完整信息,可以在Khronos Group的官方网站上找到。
特别值得注意的是,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源代码:
c
#resource "Kernels/wavelet.cl" as string cl_program
图像大小由宏定义设置:
c
#define SIZE_X 600
#define SIZE_Y 200
为了管理OpenCL,使用了包含COpenCL
类的标准库。它的方法具有类似的名称,并且在内部使用了MQL5 API中相应的内置OpenCL函数。建议你熟悉一下这个库。
c
#include <OpenCL/OpenCL.mqh>
下面以简化形式(没有错误检查和可视化)展示了启动变换的MQL代码。与小波变换相关的操作总结在CWavelet
类中。
c
class CWavelet
{
protected:
...
int m_xsize; // 图像沿轴的维度
int m_ysize;
double m_wavelet_data_GPU[]; // 结果存储在这里
COpenCL m_OpenCL; // 包装对象
...
};
主要的并行计算由其方法CalculateWavelet_GPU
组织。
c
bool CWavelet::CalculateWavelet_GPU(double &data[], uint &time)
{
int datacount = ArraySize(data); // 图像大小(点数)
// 根据其源代码编译cl程序
m_OpenCL.Initialize(cl_program, true);
// 从cl文件中注册单个内核函数
m_OpenCL.SetKernelsCount(1);
m_OpenCL.KernelCreate(0, "Wavelet_GPU");
// 注册2个用于输入和输出数据的缓冲区,写入输入数组
m_OpenCL.SetBuffersCount(2);
m_OpenCL.BufferFromArray(0, data, 0, datacount, CL_MEM_READ_ONLY);
m_OpenCL.BufferCreate(1, m_xsize * m_ysize * sizeof(double), CL_MEM_READ_WRITE);
m_OpenCL.SetArgumentBuffer(0, 0, 0);
m_OpenCL.SetArgumentBuffer(0, 4, 1);
ArrayResize(m_wavelet_data_GPU, m_xsize * m_ysize);
uint work[2]; // 分析二维图像的任务 - 因此维度为2
uint offset[2] = {0, 0}; // 从最开始开始(或者你可以跳过一些)
work[0] = m_xsize;
work[1] = m_ysize;
// 设置输入数据
m_OpenCL.SetArgument(0, 1, datacount);
m_OpenCL.SetArgument(0, 2, m_xsize);
m_OpenCL.SetArgument(0, 3, m_ysize);
time = GetTickCount(); // 用于速度测量的截止时间
// 在GPU上开始计算,二维任务
m_OpenCL.Execute(0, 2, offset, work);
// 将结果获取到输出缓冲区
m_OpenCL.BufferRead(1, m_wavelet_data_GPU, 0, 0, m_xsize * m_ysize);
time = GetTickCount() - time;
m_OpenCL.Shutdown(); // 释放所有资源 - 调用所有必要的函数CL***Free
return true;
}
在示例的源代码中,有一行调用PreparePriceData
的代码被注释掉了,用于根据真实价格准备输入数组:你可以激活它,以替代之前调用PrepareModelData
的那一行(PrepareModelData
生成一个人工数值)。
c
void OnStart()
{
int momentum_period = 8;
double price_data[];
double momentum_data[];
PrepareModelData(price_data, SIZE_X + momentum_period);
// PreparePriceData("EURUSD", PERIOD_M1, price_data, SIZE_X + momentum_period);
PrepareMomentumData(price_data, momentum_data,
... // 可视化序列和增量
CWavelet wavelet;
uint time_gpu = 0;
wavelet.CalculateWavelet_GPU(momentum_data, time_gpu);
... // 可视化小波变换的结果
}
为OpenCL操作分配了一组特殊的错误代码(以 ERR_OPENCL_
为前缀,从代码 5100,即 ERR_OPENCL_NOT_SUPPORTED
开始)。这些代码在帮助文档中有详细描述。如果OpenCL程序执行出现问题,终端会在日志中输出详细的诊断信息,并指明错误代码。