交易中的神经网络:层次化向量变换器(HiVT)·进阶篇
◍ Transformer 层里的张量尺寸怎么算
在 MT5 用 OpenCL 跑神经网络时,多头自注意力层的初始化要先算清各张量尺寸,否则显存分配会直接失败。下面这段 Init 逻辑把 window、heads、units 等参数用 fmax 兜底到至少 1,再推导出 Q、KV、Score 等缓冲大小。 以默认参数 window=1、window_key=1、heads=1、units=1、variables=1 为例:num_q = 1*1*1*1 = 1,num_kv 同样为 1,scores = 1*1*1*1 = 1,out = 1*1*1 = 1。这些数很小,但 layer 数一上去,w0 的尺寸 (window_key*heads+1)*window 会随层数线性膨胀。 循环里按 iLayers 重复建 Q/KV 缓冲,每两层 d<2 各建一组;只有当 i % iLayersToOneKV == 0 时才单独初始化 KV 张量,这是压缩 KV 缓存的关键开关。调参时若发现显存爆了,先查 iLayersToOneKV 是不是设太小导致 KV 冗余。 外汇与贵金属行情高波动,这类 GPU 推理模块只用于辅助信号生成,实盘前务必在策略测试器里跑通初始化。
class="type">uint window, class="type">uint window_key, class="type">uint heads, class="type">uint heads_kv, class="type">uint units_count, class="type">uint layers, class="type">uint layers_to_one_kv, class="type">uint variables, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count * variables, optimization_type, batch)) class="kw">return false; iWindow = fmax(window, class="num">1); iWindowKey = fmax(window_key, class="num">1); iUnits = fmax(units_count, class="num">1); iHeads = fmax(heads, class="num">1); iLayers = fmax(layers, class="num">1); iHeadsKV = fmax(heads_kv, class="num">1); iLayersToOneKV = fmax(layers_to_one_kv, class="num">1); iVariables = variables; class="type">uint num_q = iWindowKey * iHeads * iUnits * iVariables; class=class="str">"cmt">//Size of Q tensor class="type">uint num_kv = iWindowKey * iHeadsKV * iUnits * iVariables; class=class="str">"cmt">//Size of KV tensor class="type">uint q_weights = (iWindow * iHeads + class="num">1) * iWindowKey; class=class="str">"cmt">//Size of weights&class="macro">#x27; matrix of Q tenzor class="type">uint kv_weights = (iWindow * iHeadsKV + class="num">1) * iWindowKey; class=class="str">"cmt">//Size of weights&class="macro">#x27; matrix of KV tenzor class="type">uint scores = iUnits * iUnits * iHeads * iVariables; class=class="str">"cmt">//Size of Score tensor class="type">uint mh_out = iWindowKey * iHeads * iUnits * iVariables; class=class="str">"cmt">//Size of multi-heads self-attention class="type">uint out = iWindow * iUnits * iVariables; class=class="str">"cmt">//Size of attention out tensore class="type">uint w0 = (iWindowKey * iHeads + class="num">1) * iWindow; class=class="str">"cmt">//Size W0 weights matrix class="type">uint gate = (class="num">2 * iWindow + class="num">1) * iWindow; class=class="str">"cmt">//Size of weights&class="macro">#x27; matrix gate layer class="type">uint self = (iWindow + class="num">1) * iWindow; class=class="str">"cmt">//Size of weights&class="macro">#x27; matrix self layer for(class="type">uint i = class="num">0; i < iLayers; i++) { CBufferFloat *temp = NULL; for(class="type">int d = class="num">0; d < class="num">2; d++) { class=class="str">"cmt">//--- Initilize Q tensor temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.BufferInit(num_q, class="num">0)) class="kw">return false; if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!QKV_Tensors.Add(temp)) class="kw">return false; class=class="str">"cmt">//--- Initilize KV tensor if(i % iLayersToOneKV == class="num">0) {
「在 OpenCL 上铺 KV 与分数缓存」
把注意力机制搬进 MT5 的 GPU 计算管线时,先得把 K、V、拼接 KV 以及 score 这几类浮点缓冲在显存侧建好。下面这段初始化逻辑会重复构造 CBufferFloat 并逐个挂到对应的张量列表里,任何一步失败就直接返回 false,保证后续前向计算不会拿到空指针。 注意第三个缓冲的大小是 2 * num_kv 而不是 num_kv,因为 KV_Tensors 存的是 K 与 V 在最后一维拼接后的结果,通道数翻倍。若 num_kv 取 64,则该缓冲长度应为 128,这个细节写错会让内核读取越界。 分数缓冲用 scores 变量定长,与多头数量和解码步数有关;代码里统一用 BufferInit(..., 0) 把初始值清零,再 BufferCreate(OpenCL) 推到设备上下文。外汇与贵金属行情的高波动可能让注意力权重在极端跳空时剧烈变化,用 GPU 批处理虽快,但参数设错会引发静默 NaN,复盘前务必在策略测试器单步跟一遍。 [CODE] 逐行拆解 temp = new CBufferFloat(); // 在堆上新建一个浮点缓冲对象,用于承载一类张量数据 if(CheckPointer(temp) == POINTER_INVALID) // 检查指针有效性,分配失败则进入下一行返回 return false; if(!temp.BufferInit(num_kv, 0)) // 按 num_kv 长度把缓冲清零初始化,失败则退出 return false; if(!temp.BufferCreate(OpenCL)) // 在 OpenCL 设备(显卡)上创建实际存储,失败退出 return false; if(!K_Tensors.Add(temp)) // 挂到 K 张量数组,后续多头注意力的 K 走这里 return false; // 下面 V_Tensors 一段完全同构,只是最后 Add 进 V 列表 temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit(num_kv, 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!V_Tensors.Add(temp)) return false; // KV 拼接缓冲:长度是 2*num_kv temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit(2 * num_kv, 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!KV_Tensors.Add(temp)) return false; // 分数缓冲:用 scores 变量定长 temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit(scores, 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!S_Tensors.Add(temp)) return false; // 多头注意力输出缓冲(片段末尾,未完) temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID)
temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.BufferInit(num_kv, class="num">0)) class="kw">return false; if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!K_Tensors.Add(temp)) class="kw">return false; temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.BufferInit(num_kv, class="num">0)) class="kw">return false; if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!V_Tensors.Add(temp)) class="kw">return false; temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.BufferInit(class="num">2 * num_kv, class="num">0)) class="kw">return false; if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!KV_Tensors.Add(temp)) class="kw">return false; class=class="str">"cmt">//--- Initialize scores temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.BufferInit(scores, class="num">0)) class="kw">return false; if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!S_Tensors.Add(temp)) class="kw">return false; class=class="str">"cmt">//--- Initialize multi-heads attention out temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID)
注意力与前馈张量的显存预分配
在 MT5 里跑 Transformer 类模型,最容易被忽略的是 OpenCL 缓冲区的初始化顺序。上面这段逻辑把注意力输出、前馈拼接、门控与自映射四类张量逐一 new 出来,任何一步失败直接 return false,避免半初始化状态污染后续计算图。 注意力输出用 BufferInit(mh_out, 0) 占位,前馈主输出用 BufferInit(out, 0),而拼接层直接申请 2 * out 长度——这说明特征维度在 concat 后翻倍,写 EA 时若手改 out 参数,拼接缓冲必须同步乘 2,否则越界报错。 所有 temp 对象都走 BufferCreate(OpenCL) 推到显存,再塞进 AO_Tensors 或 FF_Tensors 容器管理。外汇与贵金属杠杆高、波动突发行情多,这类 GPU 张量若初始化遗漏,实盘可能在滑点放大时直接崩算,建议在策略加载期就跑一次完整初始化校验。
class="kw">return false; if(!temp.BufferInit(mh_out, class="num">0)) class="kw">return false; if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!AO_Tensors.Add(temp)) class="kw">return false; class=class="str">"cmt">//--- Initialize attention out temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.BufferInit(out, class="num">0)) class="kw">return false; if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!FF_Tensors.Add(temp)) class="kw">return false; class=class="str">"cmt">//--- Initialize Concatenate temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.BufferInit(class="num">2 * out, class="num">0)) class="kw">return false; if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!FF_Tensors.Add(temp)) class="kw">return false; class=class="str">"cmt">//--- Initialize Gate temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.BufferInit(out, class="num">0)) class="kw">return false; if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!FF_Tensors.Add(temp)) class="kw">return false; class=class="str">"cmt">//--- Initialize Self temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.BufferInit(out, class="num">0)) class="kw">return false; if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!FF_Tensors.Add(temp))
◍ Transformer 层内张量与 QKV 权重的初始化细节
这段代码落在神经网络前向层构建的收尾阶段,负责把每一层的输出/梯度张量挂进 FF_Tensors,并为注意力机制准备 Q、K 权重缓冲。最后一层(i == iLayers - 1)不走常规 temp 分支,而是直接把 Output 或 Gradient 塞进容器后 continue,避免多余显存申请。 常规层会 new 一个 CBufferFloat,先过 CheckPointer 判空,再 BufferInit(out, 0) 预清、BufferCreate(OpenCL) 推到显存,任何一步失败立即 return false。这种逐层短路返回的风格,在 MT5 跑大规模层数时能把初始化失败定位精确到具体层号。 Q 权重初始化用了一个关键缩放因子:k = 1 / sqrt(iWindow + 1),循环 q_weights 次,每次写入 GenerateWeight() * 2 * k - k。也就是说权重被约束在 [-k, k] 区间,iWindow 越大单权绝对值越小,倾向缓解宽窗口下的梯度爆炸。 K 权重不是每层都建,仅当 i % iLayersToOneKV == 0 才分配 kv_weights 长度并走同一套 [-k, k] 缩放。共享 K 的逻辑能省下可观显存,但 iLayersToOneKV 设错会让后续注意力计算维度对不上,开 MT5 调试时建议先打印 QKV_Weights 和 K_Weights 的 Size 核对。
class="kw">return false; class=class="str">"cmt">//--- Initialize Out if(i == iLayers - class="num">1) { if(!FF_Tensors.Add(d == class="num">0 ? Output : Gradient)) class="kw">return false; class="kw">continue; } temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.BufferInit(out, class="num">0)) class="kw">return false; if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!FF_Tensors.Add(temp)) class="kw">return false; } class=class="str">"cmt">//--- Initilize Q weights temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.Reserve(q_weights)) class="kw">return false; class="type">float k = (class="type">float)(class="num">1 / sqrt(iWindow + class="num">1)); for(class="type">uint w = class="num">0; w < q_weights; w++) { if(!temp.Add(GenerateWeight() * class="num">2 * k - k)) class="kw">return false; } if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!QKV_Weights.Add(temp)) class="kw">return false; class=class="str">"cmt">//--- Initialize K weights if(i % iLayersToOneKV == class="num">0) { temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.Reserve(kv_weights)) class="kw">return false; for(class="type">uint w = class="num">0; w < kv_weights; w++) { if(!temp.Add(GenerateWeight() * class="num">2 * k - k)) class="kw">return false; } if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!K_Weights.Add(temp))
「权重张量的 OpenCL 初始化收口」
这段逻辑处在网络参数初始化的末尾,依次把键值权重、偏置权重与门控权重推入对应的 GPU 缓冲容器。每一类权重都先以 CBufferFloat 动态申请,若指针校验返回 POINTER_INVALID 就直接 return false,避免在 MT5 终端里出现野指针导致的 EA 崩溃。 键值权重循环调用 GenerateWeight()*2*k-k 做区间映射,k 在此前已定义为缩放系数;循环长度为 kv_weights,若任意一次 Add 失败同样回退。随后 Reserve(kv_weights) 预留显存,BufferCreate(OpenCL) 才真正把数据搬上计算设备。 门控部分把 k 重算为 1/sqrt(2*iWindow+1),当 iWindow=30 时 k≈0.126,权重散布范围被压缩得更窄,倾向让门控信号更平稳。最后 Self 分支刚 new 出 temp 并过指针检查,片段在此截断,说明同类容器还可能继续挂接。 在 MT5 里跑这套时,建议把 iWindow 改成你实际的特征窗口,重新编译后看 Experts 日志里 BufferCreate 的返回,若频繁 false 多半是显存预留不足。外汇与贵金属杠杆交易高风险,参数验证请先在策略测试器用历史数据跑通。
class="kw">return false; class=class="str">"cmt">//--- temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.Reserve(kv_weights)) class="kw">return false; for(class="type">uint w = class="num">0; w < kv_weights; w++) { if(!temp.Add(GenerateWeight() * class="num">2 * k - k)) class="kw">return false; } if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!V_Weights.Add(temp)) class="kw">return false; } class=class="str">"cmt">//--- Initialize Weights0 temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.Reserve(w0)) class="kw">return false; for(class="type">uint w = class="num">0; w < w0; w++) { if(!temp.Add(GenerateWeight() * class="num">2 * k - k)) class="kw">return false; } if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!FF_Weights.Add(temp)) class="kw">return false; class=class="str">"cmt">//--- Initialize Gate Weights temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.Reserve(gate)) class="kw">return false; k = (class="type">float)(class="num">1 / sqrt(class="num">2 * iWindow + class="num">1)); for(class="type">uint w = class="num">0; w < gate; w++) { if(!temp.Add(GenerateWeight() * class="num">2 * k - k)) class="kw">return false; } if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!FF_Weights.Add(temp)) class="kw">return false; class=class="str">"cmt">//--- Self temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false;