OpenCL:并行世界的桥梁·进阶篇

OpenCL:并行世界的桥梁·进阶篇

(2/3)· 从 75 倍提速的离谱数据到 API 与向量类型,进阶篇只讲能落地的并行写法

含代码示例实战向 第 2/3 篇

不少交易者以为 MQL5 的多线程 EA 就等于用满了多核,结果一个算 pi 的朴素循环在单机单核上慢到怀疑人生。等他们听说 OpenCL 能快几十倍,又卡在 SDK 安装和核函数写法上迟迟不敢动手。本篇接着基础概念往下,直接拆开终端里能跑的并行代码结构。

◍ 在 MT5 里用 OpenCL 跑蒙特卡洛求 π 的实测账

把 GPU 计算接进 MQL5 并不玄学,核心是先建上下文、编译内核、开缓冲区,再把任务丢给显卡。下面这段是 float 精度下的执行骨架,照抄到 MT5 脚本里就能跑。 int clPrg = CLProgramCreate( clCtx, clSrc ); int clKrn = CLKernelCreate( clPrg, "pi" ); uint st=GetTickCount(); int clMem=CLBufferCreate(clCtx,_divisor*sizeof(float),CL_MEM_READ_WRITE); // type float CLSetKernelArgMem(clKrn,0,clMem); const uint offs[ 1 ] = { 0 }; const uint works[ 1 ] = { _divisor }; bool ex=CLExecute(clKrn,1,offs,works); float buf[]; ArrayResize(buf,_divisor); uint read=CLBufferRead(clMem,buf); Print("read = "+i2s(read)+" elements"); float sum=0.0; for(int cnt=0; cnt<_divisor; cnt++) sum+=buf[cnt]; float pi=float(sum*_step); Print("pi = "+d2s(pi,12)); CLBufferFree(clMem); CLKernelFree(clKrn); CLProgramFree(clPrg); CLContextFree(clCtx); double gone=(GetTickCount()-st)/1000.; Print("OpenCl: gone = "+d2s(gone,3)+" sec."); 逐行拆一下:CLProgramCreate 用源码 clSrc 编译出程序对象;CLKernelCreate 取出名为 pi 的内核函数。CLBufferCreate 按 _divisor 个 float 申请显存,CLSetKernelArgMem 把这块显存绑给内核第 0 号参数。offs 是偏移、works 是总工作量,CLExecute 一发,GPU 就开始并行算。读回用 CLBufferRead 填进 buf 数组,循环累加后乘 _step 得到 π 近似值。 实盘日志不会骗人:2012.05.03 在 EURUSD H1 上,float 版 _step=1e-9、_intrnCnt=25000,读回 40000 个元素,算出 pi=3.141622066498,耗时 5.538 秒。同环境 double 版精度到 3.141592653590,但耗时翻倍到 12.480 秒。 外汇与贵金属交易本就高杠杆高风险,这类 OpenCL 基准只验证计算管线本身;真要搬进策略,显存带宽和内核编译开销可能吃掉短线信号的时效,建议先在离线脚本里复刻这组数字再谈集成。

MQL5 / C++
class="type">int clPrg = CLProgramCreate( clCtx, clSrc );
class="type">int clKrn = CLKernelCreate( clPrg, "pi" );
class="type">uint st=GetTickCount();
class="type">int clMem=CLBufferCreate(clCtx,_divisor*class="kw">sizeof(class="type">float),CL_MEM_READ_WRITE); class=class="str">"cmt">// type class="type">float
CLSetKernelArgMem(clKrn,class="num">0,clMem);
class="kw">const class="type">uint offs[ class="num">1 ]  = { class="num">0 };
class="kw">const class="type">uint works[ class="num">1 ] = { _divisor };
class="type">bool ex=CLExecute(clKrn,class="num">1,offs,works);
class="type">float buf[];
ArrayResize(buf,_divisor);
class="type">uint read=CLBufferRead(clMem,buf);
Print("read = "+i2s(read)+" elements");
class="type">float sum=class="num">0.0;
for(class="type">int cnt=class="num">0; cnt<_divisor; cnt++) sum+=buf[cnt];
class="type">float pi=class="type">float(sum*_step);
Print("pi = "+d2s(pi,class="num">12));
CLBufferFree(clMem);
CLKernelFree(clKrn);
CLProgramFree(clPrg);
CLContextFree(clCtx);
class="type">class="kw">double gone=(GetTickCount()-st)/class="num">1000.;
Print("OpenCl: gone = "+d2s(gone,class="num">3)+" sec.");

「双精度下 OpenCL 算 pi 的真实耗时」

在 EURUSD 的 H1 图表上跑同一段 OpenCL 双精度核,两次日志给出的结果很直白:pi 都收敛到 3.141592653590,但采样规模差了 1000 倍。

  • 05.03 那次读了 40000000 个元素、内部计数 _intrnCnt=25000,OpenCl 耗时 5.070 秒;2012.05.16 那次只读了 40000 个元素、_intrnCnt 同样设为 25000 时耗时 2.262 秒,而另一组 _intrnCnt=25 的轻量调用更快。

_step 恒为 0.000000001000,说明迭代步长在双精度下锁死,精度不随读取量变化。外汇与贵金属杠杆高、滑点随机,这类 GPU 基准只反映终端算力,不直接预示任何行情方向。 想复现,就把下面这段日志对应的 EA 挂到 MT5 的 EURUSD H1,对比自己机器的 gone 秒数——显卡驱动和 OpenCL 运行时版本会显著挪动这个数字。

MQL5 / C++
class="num">2012.05.class="num">03 class="num">03:class="num">25:class="num">23    OCL_pi_double(EURUSD,H1)      DOUBLE: _step = class="num">0.000000001000; _intrnCnt = class="num">25000
class="num">2012.05.class="num">03 class="num">03:class="num">26:class="num">55    OCL_pi_double(EURUSD,H1)      ________________________
class="num">2012.05.class="num">03 class="num">03:class="num">26:class="num">55    OCL_pi_double(EURUSD,H1)      OpenCl: gone = class="num">5.070 sec.
class="num">2012.05.class="num">03 class="num">03:class="num">26:class="num">55    OCL_pi_double(EURUSD,H1)      pi = class="num">3.141592653590
class="num">2012.05.class="num">03 class="num">03:class="num">26:class="num">55    OCL_pi_double(EURUSD,H1)      read = class="num">40000000 elements
class="num">2012.05.class="num">03 class="num">03:class="num">26:class="num">50    OCL_pi_double(EURUSD,H1)      DOUBLE: _step = class="num">0.000000001000; _intrnCnt = class="num">25
class="num">2012.05.class="num">16 class="num">00:class="num">22:class="num">46    OCL_pi_double(EURUSD,H1)       ________________________
class="num">2012.05.class="num">16 class="num">00:class="num">22:class="num">46    OCL_pi_double(EURUSD,H1)       OpenCl: gone = class="num">2.262 sec.
class="num">2012.05.class="num">16 class="num">00:class="num">22:class="num">46    OCL_pi_double(EURUSD,H1)       pi = class="num">3.141592653590
class="num">2012.05.class="num">16 class="num">00:class="num">22:class="num">46    OCL_pi_double(EURUSD,H1)       read = class="num">40000 elements
class="num">2012.05.class="num">16 class="num">00:class="num">22:class="num">44    OCL_pi_double(EURUSD,H1)       DOUBLE: _step = class="num">0.000000001000; _intrnCnt = class="num">25000

在 MT5 里跑通 OpenCL 计算管线

MT5 的 OpenCL 支持让 EA 能把并行计算丢给显卡或 CPU 核心。上下文用 CLContextCreate() 建立,它只绑定一个设备——双芯显卡会被识别成两个独立设备,CPU 本身也始终算一个设备。Radeon HD 5870 这类老卡就有约 20 个 SIMD 核心、每核心 16 个流处理器、估算线程数高达 5000,足够说明并行规模。 程序、内核、内存对象都得挂到同一个上下文下面。CLProgramCreate() 读入源码字符串生成 Program,但 MetaQuotes 实现里一个 Program 只能有一个内核;CLKernelCreate() 按函数名抓取内核,例如名为 "pi" 的函数。缓冲区必须用 CLBufferCreate() 在 GPU 侧开好,或用 CLBufferWrite() 从 RAM 搬过去,CLBufferRead() 再搬回来——设备内存有效性取决于硬件厂商,开内存对象要留神别爆显存。 参数通过 CLSetKernelArgMem() 绑缓冲区、CLSetKernelArg() 绑普通值(如 double)。执行时 CLEnqueueNDRangeKernel 类调用按 works 数量孵化出等量内核实例,走 SPMD 模式铺到不同流核心同时算。作者曾在无独显的 CPU 上直接跑通,证明代码不挑硬件,但想提速仍得懂底层规格。 内核代码就是个长字符串,头部 __kernel void 固定无返回值,数组按引用加 __global,简单类型按值传。下标 i 标识当前计算单元算哪一格,结果写进 out[] 再读回主程序。对象销毁要严格逆创建顺序。下面这段最小管线可直接贴进 MT5 看效果:

MQL5 / C++
class="type">int clCtx = CLContextCreate( _device );
class="type">int clPrg = CLProgramCreate( clCtx, clSrc );
class="type">int clKrn = CLKernelCreate( clPrg, "pi" );
class="type">int clMem = CLBufferCreate( clCtx,  _divisor * class="kw">sizeof( class="type">class="kw">double ), CL_MEM_READ_WRITE );
CLSetKernelArgMem( clKrn, class="num">0, clMem );
__kernel class="type">void pi( __global class="type">float *out )
class="type">bool  CLSetKernelArg( class="type">int   kernel,          class=class="str">"cmt">// handle to the kernel of the OpenCL program
                      class="type">uint  arg_index,       class=class="str">"cmt">// OpenCL function argument number
                      class="type">void  arg_value );     class=class="str">"cmt">// function argument value
class="type">class="kw">double x0 = -class="num">2;
CLSetKernelArg( cl_krn, class="num">1, x0 );

◍ 把 OpenCL 核跑完再回收资源

在 MT5 里调用 OpenCL 做并行计算,真正吃算力的那一行是 CLExecute:把编译好的内核丢给设备,指定工作维度、偏移量和总工作量。下面这段代码就是一次最小闭环——启动、取数、释放。

MQL5 / C++
class="type">bool ex = CLExecute( clKrn, class="num">1, offs, works );
class="type">float buf[ ]; 
ArrayResize( buf, _divisor );
class="type">uint read = CLBufferRead( clMem, buf );
CLBufferFree( clMem );
CLKernelFree( clKrn );
CLProgramFree( clPrg );
CLContextFree( clCtx );
__kernel class="type">void pi( __global class="type">float *out )
class="type">int i = get_global_id( class="num">0 );
逐行看:CLExecute 返回 bool,非 true 就说明设备没跑起来,常见于 _divisor 与 works 不匹配。buf 先用空数组声明,再用 ArrayResize 按 _divisor 扩到实际长度,这一步漏了会读到垃圾值。CLBufferRead 把显存里的计算结果搬回 buf,返回值是实际读到的元素数,应当等于 _divisor。 后面四个 Free 必须成对出现:buffer、kernel、program、context 不释放,MT5 终端的 OpenCL 句柄会泄漏,多跑几轮策略测试就可能报「context limit exceeded」。最后那段 __kernel void pi 是设备端内核骨架,get_global_id(0) 拿到的是当前线程在全局的一维索引,用来给 out 数组写各自负责的格子。 外汇与贵金属品种用这类 GPU 加速回测时波动跳空频繁,句柄异常会让结果失真,务必在实盘前用历史数据跑通整段释放逻辑。

MQL5 / C++
class="type">bool ex = CLExecute( clKrn, class="num">1, offs, works );
class="type">float buf[ ]; 
ArrayResize( buf, _divisor );
class="type">uint read = CLBufferRead( clMem, buf );
CLBufferFree( clMem );
CLKernelFree( clKrn );
CLProgramFree( clPrg );
CLContextFree( clCtx );
__kernel class="type">void pi( __global class="type">float *out )
class="type">int i = get_global_id( class="num">0 );

「OpenCL 向量类型怎么声明和取分量」

在 MT5 里写 OpenCL 内核,会碰到一类 C99 没有的类型:向量数据类型。它们写成 (u)charN、(u)shortN、(u)intN、(u)longN、floatN,N 只能是 2、3、4、8、16。这类类型专为并行计算准备,但别以为只要换 N 值编译器就真能并行——经验上并不总是如此,内核逻辑一致仅 N 不同,实际加速效果得自己在显卡上测。 声明很直白:右侧常量宽度要和左侧变量匹配。比如 uint4 u = (uint4)(1); 会把标量 1 扩成 (1,1,1,1);但 float4 f = (float4)(1.0f, 2.0f); 直接报错,因为只给了两个分量。向量既是数组也是结构,float4 用 .x/.y/.z/.w 取分量,long3 只有 .x/.y/.z,float2 根本没有 .z。 取分量还能打包重排:.xyzw 可写成 pos.wzyx 实现反转,pos.xxyy 能复制分量。赋值时左值也认组标记,如 pos.xw = (float2)(5.0f,6.0f) 只改第1、4分量;但 pos.xx = ... 因 x 重复而非法,pos.xy = (float4)... 因维度不匹配也非法。 除了字母标记,还有数值索引 .s0~.sf:float4 的 f.s0 是第1分量,16维向量的 x.sa 是第11分量(a 对应10,从0计)。注意 .s 数字和 .xyzw 字母不能混用,像 f.x12w 就无效。 向量还支持 .lo.hi.even.odd 后缀,分别取下半、上半、偶序、奇序分量,可嵌套到拆出标量为止。3分量类型本质是按4分量排的,第4分量值未定义,写内核时容易踩坑。 算术和关系运算按分量逐做;标量和向量运算时,标量先被扩成同维向量再算。要做 GPU 精确计算,得用 doubleN,内核开头加 #pragma OPENCL EXTENSION cl_khr_fp64 : enable 开启双精度——外汇高频回测跑浮点累计误差可能偏大,双精度更稳但慢,自己权衡。

MQL5 / C++
float4 f = ( float4 ) ( class="num">1.0f, class="num">2.0f, class="num">3.0f, class="num">4.0f);
uint4  u = ( uint4  ) ( class="num">1 );            class=class="str">"cmt">/// u is converted to a vector(class="num">1, class="num">1, class="num">1, class="num">1).
float4 f = ( float4 ) ( ( float2 )( class="num">1.0f, class="num">2.0f ), ( float2 )( class="num">3.0f, class="num">4.0f ) );
float4 f = ( float4 ) ( class="num">1.0f, ( float2 )( class="num">2.0f, class="num">3.0f ), class="num">4.0f );
float4 f = ( float4 ) ( class="num">1.0f, class="num">2.0f );   class=class="str">"cmt">/// error
float2 pos;
pos.x = class="num">1.0f; class=class="str">"cmt">// valid
pos.z = class="num">1.0f; class=class="str">"cmt">// invalid because pos.z does not exist
float3 pos;
pos.z = class="num">1.0f; class=class="str">"cmt">// valid
pos.w = class="num">1.0f; class=class="str">"cmt">// invalid because pos.w does not exist
float4 c;
c.xyzw = ( float4 ) ( class="num">1.0f, class="num">2.0f, class="num">3.0f, class="num">4.0f );
c.z = class="num">1.0f;
c.xy =  ( float2 ) ( class="num">3.0f, class="num">4.0f );
c.xyz = ( float3 ) ( class="num">3.0f, class="num">4.0f, class="num">5.0f );
float4 pos = ( float4 ) ( class="num">1.0f, class="num">2.0f, class="num">3.0f, class="num">4.0f );
float4 swiz= pos.wzyx;                                      class=class="str">"cmt">// swiz = ( class="num">4.0f, class="num">3.0f, class="num">2.0f, class="num">1.0f )
float4 dup = pos.xxyy;                                      class=class="str">"cmt">// dup = ( class="num">1.0f, class="num">1.0f, class="num">2.0f, class="num">2.0f )
float4 pos = ( float4 ) ( class="num">1.0f, class="num">2.0f, class="num">3.0f, class="num">4.0f );
pos.xw     = ( float2 ) ( class="num">5.0f, class="num">6.0f );                     class=class="str">"cmt">// pos = ( class="num">5.0f, class="num">2.0f, class="num">3.0f, class="num">6.0f )
pos.wx     = ( float2 ) ( class="num">7.0f, class="num">8.0f );                     class=class="str">"cmt">// pos = ( class="num">8.0f, class="num">2.0f, class="num">3.0f, class="num">7.0f )
pos.xyz   = ( float3 ) ( class="num">3.0f, class="num">5.0f, class="num">9.0f );                class=class="str">"cmt">// pos = ( class="num">3.0f, class="num">5.0f, class="num">9.0f, class="num">4.0f )
pos.xx     = ( float2 ) ( class="num">3.0f, class="num">4.0f );                     class=class="str">"cmt">// invalid as &class="macro">#x27;x&class="macro">#x27; is used twice
pos.xy     = ( float4 ) (class="num">1.0f, class="num">2.0f, class="num">3.0f, class="num">4.0f ); class=class="str">"cmt">// mismatch between float2 and float4
float4 a, b, c, d;
float16 x;
x = ( float16 ) ( a, b, c, d );
x = ( float16 ) ( a.xxxx, b.xyz, c.xyz, d.xyz, a.yzw );
x = ( float16 ) ( a.xxxxxxx, b.xyz, c.xyz, d.xyz );   class=class="str">"cmt">// invalid as the component a.xxxxxxx is not a valid vector type
float8 f;
float16 x;
float4 f, a;
a = f.x12w;                                                class=class="str">"cmt">// invalid as numeric indices are intermixed with the letter notations .xyzw
a.xyzw = f.s0123;                                          class=class="str">"cmt">// valid
float4 vf;

向量分量截取与运算的实测拆解

在 MT5 的 OpenCL 内核里,floatN 类型的分量访问靠 .lo / .hi / .even / .odd 这类后缀完成,等价于把寄存器里的数据按奇偶或高低位重新分组。下面这段直接贴出可编译的片段,跑一遍就能看到每个截取结果。 float2 low = vf.lo; // vf.xy float2 high = vf.hi; // vf.zw float2 even = vf.even; // vf.xz float2 odd = vf.odd; // vf.yw float8 u = (float8)(1.0f,2.0f,3.0f,4.0f,5.0f,6.0f,7.0f,8.0f); float2 s = u.lo.lo; // (1.0f,2.0f) float2 t = u.hi.lo; // (5.0f,6.0f) float2 q = u.even.lo; // (1.0f,3.0f) float r = u.odd.lo.hi; // 4.0f 注意当源向量维度小于截取要求时,高位分量会是 undefined。比如 float3 vf=(1.0f,2.0f,3.0f) 时,vf.hi 得到 (3.0f, undefined),这种未定义行为在回测 GPU 指标时可能让结果漂移,必须手动补零。 基础算术对 float4 是直接逐元素并行:d=(1,2,3,4)、w=(5,8,10,-1) 相加得 (6,10,13,3),相乘得 (5,16,30,-4),相除得 (5,4,3.333333,-0.25)。外汇与贵金属行情用这类向量批处理算多周期波动率,能压低 CPU 占用,但杠杆品种波动剧烈,GPU 精度误差叠加风险偏高,上线前建议在 MT5 策略测试器用真实tick校验。

MQL5 / C++
float2 low  = vf.lo;           class=class="str">"cmt">// vf.xy
float2 high = vf.hi;           class=class="str">"cmt">// vf.zw
float2 even = vf.even;         class=class="str">"cmt">// vf.xz
float2 odd  = vf.odd;          class=class="str">"cmt">// vf.yw
float8 u = (float8) ( class="num">1.0f, class="num">2.0f, class="num">3.0f, class="num">4.0f, class="num">5.0f, class="num">6.0f, class="num">7.0f, class="num">8.0f );
float2 s = u.lo.lo;                                            class=class="str">"cmt">//  ( class="num">1.0f, class="num">2.0f )
float2 t = u.hi.lo;                                            class=class="str">"cmt">//  ( class="num">5.0f, class="num">6.0f )
float2 q = u.even.lo;                                          class=class="str">"cmt">//  ( class="num">1.0f, class="num">3.0f )
class="type">float  r = u.odd.lo.hi;                                        class=class="str">"cmt">//  class="num">4.0f
float3 vf   = (float3) (class="num">1.0f, class="num">2.0f, class="num">3.0f);
float2 low  = vf.lo;                                           class=class="str">"cmt">// ( class="num">1.0f, class="num">2.0f );
float2 high = vf.hi;                                           class=class="str">"cmt">// ( class="num">3.0f, undefined );
float4  d   = (float4) ( class="num">1.0f, class="num">2.0f, class="num">3.0f, class="num">4.0f );
float4  w   = (float4) ( class="num">5.0f, class="num">8.0f, class="num">10.0f, -class="num">1.0f );
float4  _sum = d + w;                                          class=class="str">"cmt">// ( class="num">6.0f, class="num">10.0f, class="num">13.0f, class="num">3.0f )
float4  _mul = d * w;                                          class=class="str">"cmt">// ( class="num">5.0f, class="num">16.0f, class="num">30.0f, -class="num">4.0f )
float4  _div = w / d;                                          class=class="str">"cmt">// ( class="num">5.0f,  class="num">4.0f, class="num">3.333333f, -class="num">0.25f )
class="macro">#pragma OPENCL EXTENSION cl_khr_fp64 : enable 
把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到 OpenCL 脚本的耗时对比与设备占用,你专注决策而非调环境。

常见问题

不一定。Intel 的 Ivy Bridge 及以上集成显卡或安装 AMD APP SDK / Intel OpenCL Runtime 后,CPU 的 x86 核心也能执行 OpenCL 代码,低端机也可能获得约 25~75 倍提速。
向量类型如 float4 可一次性打包四个浮点做并行运算,比逐元素数组循环更贴合 GPU 架构,但需注意内存对齐与编译器优化不足带来的隐性损耗。
可以,小布盯盘对应品种页内置了 AIGC 诊断,能展示脚本在 CPU/GPU 上的耗时与设备占用,省去你手动接 SDK 测基准的麻烦。
差异来自 SDK 版本、是否启用 IGP、源语言优化程度以及散热降频。论坛实测中源语言优化不足常是加速未达预期的主因,外汇贵金属交易用并行回测仍属高风险实验。