神经网络变得简单(第 95 部分):降低变换器模型中的内存消耗·进阶篇
(2/3)· 当 GQA 的 1/头 省内存上限仍不够用时,跨层 KV 共享把占用压到 2/层 附近
「注意力层张量的循环初始化」
在 MT5 用 OpenCL 跑 Transformer 类模型时,每一层网络都要预先在显存侧建好 Q、K/V、scores 和多头输出这几组浮点缓冲。下面这段循环用 d<2 控制双头结构,每层各 new 一个 CBufferFloat 并塞进对应容器,任一环节失败直接 return false 中断加载。 K/V 张量不是每层都建,代码里用 i % iLayersToOneKV == 0 做间隔判断——比如设成 2 就代表每两层共享一次 KV,能省掉近一半的显存申请。scores 和 mh_out 则每层必建,尺寸分别由 scores、mh_out 变量给定,初始化值统一填 0。 实盘跑前建议把 num_q、num_kv、scores、mh_out 打印出来核对:若 num_q=128、num_kv=64、scores=128、mh_out=128,双头下 Q_Tensors 总长约 256 浮点。外汇与贵金属杠杆高,这类 GPU 推理若缓冲尺寸算错,EA 加载失败不会报明确错误,只静默 return false。
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) { 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(!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) 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
前馈层与Q权重的显存张量落地
在 MT5 的 OpenCL 环境里搭神经网络,每一层前向张量都得先申请 CBufferFloat 再挂到 FF_Tensors 容器,任何一步失败直接 return false,避免野指针污染后续计算。 第一段初始化 Feed Forward 1 时,缓冲区按 4 * out 大小开(out 为上层输出维度),相比普通层多配 4 倍容量,大概率是为后续激活或梯度留冗余;最后一层则按 d==0 塞 Output 否则塞 Gradient,跳过常规 temp 分配。 Q 权重走另一条路:先 Reserve(q_weights) 预留空间,再用 k = 1/sqrt(iWindow+1) 做 Xavier 风格缩放,循环里每个权重 = GenerateWeight()*2k - k,把随机值压到 [-k, k] 区间。 外汇与贵金属行情噪声大,这类 GPU 张量若维度算错,EA 在实盘可能静默崩掉,建议开 MT5 用 __OPENCL_PROFILE 跑一遍确认显存申请耗时。
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 Feed Forward class="num">1 temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.BufferInit(class="num">4 * 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 Feed Forward class="num">2 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">//--- Initialize 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;
◍ Transformer权重的初始化与显存落盘
这段代码是 MT5 里把注意力机制权重塞进 OpenCL 缓冲区的核心片段。每一层循环先建 QKV 权重缓冲,若 BufferCreate 或数组追加失败直接返回 false,保证初始化过程不静默出错。 KV 权重并非每层都建:当层索引 i 能被 iLayersToOneKV 整除时才新建 CBufferFloat,用 1/sqrt(iWindow+1) 作为缩放系数 k,把随机权重映射到 [-k, k] 区间。假设窗口 iWindow=63,则 k≈0.125,权重绝对值被压在 0.125 以内,倾向于抑制深层梯度爆炸。 Weighs0 与 FF 权重在每层都重新 new 缓冲,走同样的 Reserve → Add(GenerateWeight()*2*k-k) → BufferCreate(OpenCL) 流程,最后挂到 FF_Weights 动态数组。开 MT5 把这段贴进 EA 初始化函数,改 iWindow 看 k 值变化,能直接验证显存占用随窗口变大而上升。外汇与贵金属杠杆交易高风险,此类 GPU 加速模型仅作信号辅助,实盘须严控仓位。
if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!QKV_Weights.Add(temp)) class="kw">return false; class=class="str">"cmt">//--- Initialize KV 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; class="type">float k = (class="type">float)(class="num">1 / sqrt(iWindow + class="num">1)); 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(!KV_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 FF Weights temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.Reserve(ff_1)) class="kw">return false; for(class="type">uint w = class="num">0; w < ff_1; 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))
「Feed-Forward 与 QKV 权重的显存初始化」
这段逻辑出现在 Transformer 类指标的内存分配阶段,负责给前馈网络和注意力机制的两组权重在 OpenCL 缓冲里占位。先看前馈部分:先 new 一个 CBufferFloat,若指针无效直接返回 false,随后 Reserve(ff_2) 预留前馈尺寸,失败同样退出。 权重缩放系数 k 由公式 1/sqrt(4*iWindow+1) 算出,循环 ff_2 次把 GenerateWeight()*2*k - k 写进 temp,相当于把随机权重映射到 [-k, k] 区间;这一步若任意一次 Add 失败就回滚。 随后 temp.BufferCreate(OpenCL) 把数据推到显存,并加入 FF_Weights 动态数组。注意 iWindow 若取 50,则 k≈0.0707,权重的初始振幅被压得很低,训练早期梯度可能偏平。 QKV 部分按优化器分支:SGD 只跑 1 轮 d 循环,其他优化器跑 2 轮。每轮 new 缓冲区、BufferInit(q_weights,0) 清零、BufferCreate 上显存、加入 QKV_Weights;当 i%iLayersToOneKV==0 时额外建 kv_weights 大小的 KV_Weights 缓冲。 最后那段 w0 初始化把 BufferInit(w0,0) 的零缓冲也推上 OpenCL,供后续偏置或输出层使用。外汇与贵金属市场波动剧烈、杠杆风险高,这类 GPU 指标仅作概率参考,实盘前务必在 MT5 策略测试器用历史数据验证显存占用与初始化耗时。
class="kw">return false; temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.Reserve(ff_2)) class="kw">return false; k = (class="type">float)(class="num">1 / sqrt(class="num">4 * iWindow + class="num">1)); for(class="type">uint w = class="num">0; w < ff_2; 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">//--- for(class="type">int d = class="num">0; d < (optimization == SGD ? class="num">1 : class="num">2); d++) { temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.BufferInit(q_weights, class="num">0)) class="kw">return false; if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!QKV_Weights.Add(temp)) class="kw">return false; if(i % iLayersToOneKV == class="num">0) { temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.BufferInit(kv_weights, class="num">0)) class="kw">return false; if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!KV_Weights.Add(temp)) class="kw">return false; } temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.BufferInit(w0, class="num">0)) class="kw">return false; if(!temp.BufferCreate(OpenCL)) class="kw">return false;
多头注意力里的权重缓冲与前向落地
这段逻辑在 MLMHAttention 类里负责把前馈层的权重缓冲挂到 OpenCL 显存上,任何一步 BufferCreate 失败就直接 return false,整个网络初始化随之中断。注意 ff_1 与 ff_2 是两个独立 CBufferFloat,分别用 BufferInit(ff_1,0) 和 BufferInit(ff_2,0) 开零填充缓冲,再走 BufferCreate(OpenCL) 推到设备端。 feedForward 里按 iLayers 循环,第 0 层输入取自 NeuronOCL.getOutput(),之后层取 FF_Tensors.At(6*i-4)。卷积前向调用 ConvolutionForward 时,权重下标随优化器切换:SGD 用系数 2,其他用 3,这是显存布局里容易漏看的一处分支。 当 i%iLayersToOneKV==0 才计算 KV,i_kv 决定 KV_Tensors 偏移,卷积输出维度是 2*iWindowKey*iHeadsKV。外汇与贵金属行情用这类结构做序列建模时,显存申请失败会静默拖垮回测,建议先在 MT5 策略测试器开 OpenCL 日志验证每一步返回。
if(!FF_Weights.Add(temp)) class="kw">return false; class=class="str">"cmt">//--- Initialize FF Weights temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.BufferInit(ff_1, class="num">0)) class="kw">return false; if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!FF_Weights.Add(temp)) class="kw">return false; temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.BufferInit(ff_2, class="num">0)) class="kw">return false; if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!FF_Weights.Add(temp)) class="kw">return false; } } if(!Temp.BufferInit(MathMax(num_kv, out), class="num">0)) class="kw">return false; if(!Temp.BufferCreate(OpenCL)) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronMLMHAttentionMLKV::feedForward(CNeuronBaseOCL *NeuronOCL) { if(CheckPointer(NeuronOCL) == POINTER_INVALID) class="kw">return false; CBufferFloat *kv = NULL; for(class="type">uint i = class="num">0; (i < iLayers && !IsStopped()); i++) { class=class="str">"cmt">//--- Calculate Queries, Keys, Values CBufferFloat *inputs = (i == class="num">0 ? NeuronOCL.getOutput() : FF_Tensors.At(class="num">6 * i - class="num">4)); CBufferFloat *q = QKV_Tensors.At(i * class="num">2); if(IsStopped() || !ConvolutionForward(QKV_Weights.At(i * (optimization == SGD ? class="num">2 : class="num">3)), inputs, q, iWindow, iWindowKey * iHeads, None)) class="kw">return false; if((i % iLayersToOneKV) == class="num">0) { class="type">uint i_kv = i / iLayersToOneKV; kv = KV_Tensors.At(i_kv * class="num">2); if(IsStopped() || !ConvolutionForward(KV_Weights.At(i_kv * (optimization == SGD ? class="num">2 : class="num">3)), inputs, kv, iWindow, class="num">2 * iWindowKey * iHeadsKV, None)) class="kw">return false; } class=class="str">"cmt">//--- Score calculation and Multi-heads attention calculation CBufferFloat *temp = S_Tensors.At(i * class="num">2);
◍ 反向传播里梯度怎么穿过多头注意力层
前面把前向的注意力与卷积前馈跑通了,模型能不能训起来,关键看 calcInputGradients 怎么把输出梯度往回送。这段代码是 CNeuronMLMHAttentionMLKV 的反向核心,逐层倒序处理,从 iLayers-1 一路减到 0。 循环里先判断当前层是否复用 KV:当 i 是最后一层,或 (i+1) 能被 iLayersToOneKV 整除时,kv_g 会重新指向 KV_Tensors 里对应槽位,索引算法是 (i / iLayersToOneKV) * 2 + 1,这保证了多头之间梯度不串层。 前馈部分的回传分两步卷积梯度。先对权重下标 i*(optimization==SGD?6:9)+2 调 ConvolutionInputGradients,把 out_grad 经 FF_Tensors 的 i*6+1 和 i*6+4 槽反向传到 4*iWindow→iWindow 维度,激活填 None;紧接着用 +1 权重把 i*6+4 的梯度经 i*6 槽和临时缓冲 temp(i*6+3)回传,维度反过来、激活用 LReLU。 SGD 与 Adam 类优化器在这里的权重步长差异是实打实的:前者每层占 6 个权重块,后者占 9 个,回传时所有 At() 下标都跟着 optimization 分支走,改优化器不用动循环结构,只变偏移量。 在 MT5 里把这段接上你自己的张量分配,跑一个 iWindow=64、iHeads=4 的小样本,观察 FF_Tensors 总块数是否等于 iLayers*6,不对就会在 IsStopped 前越界返回 false。外汇与贵金属行情下用这类结构做信号,回测过拟合概率偏高,实盘前务必做样本外验证。
class="type">bool CNeuronMLMHAttentionMLKV::calcInputGradients(CNeuronBaseOCL *prevLayer) { if(CheckPointer(prevLayer) == POINTER_INVALID) class="kw">return false; CBufferFloat *out_grad = Gradient; CBufferFloat *kv_g = KV_Tensors.At(KV_Tensors.Total() - class="num">1); for(class="type">int i = class="type">int(iLayers - class="num">1); (i >= class="num">0 && !IsStopped()); i--) { if(i == class="type">int(iLayers - class="num">1) || (i + class="num">1) % iLayersToOneKV == class="num">0) kv_g = KV_Tensors.At((i / iLayersToOneKV) * class="num">2 + class="num">1); class=class="str">"cmt">//--- Passing gradient through feed forward layers if(IsStopped() || !ConvolutionInputGradients(FF_Weights.At(i * (optimization == SGD ? class="num">6 : class="num">9) + class="num">2), out_grad, FF_Tensors.At(i * class="num">6 + class="num">1), FF_Tensors.At(i * class="num">6 + class="num">4), class="num">4 * iWindow, iWindow, None)) class="kw">return false; CBufferFloat *temp = FF_Tensors.At(i * class="num">6 + class="num">3); if(IsStopped() || !ConvolutionInputGradients(FF_Weights.At(i * (optimization == SGD ? class="num">6 : class="num">9) + class="num">1), FF_Tensors.At(i * class="num">6 + class="num">4), FF_Tensors.At(i * class="num">6), temp, iWindow, class="num">4 * iWindow, LReLU)) class="kw">return false; class=class="str">"cmt">//--- Sum and normilize gradients