神经网络变得简单(第 95 部分):降低变换器模型中的内存消耗·进阶篇
🧠

神经网络变得简单(第 95 部分):降低变换器模型中的内存消耗·进阶篇

(2/3)· 当 GQA 的 1/头 省内存上限仍不够用时,跨层 KV 共享把占用压到 2/层 附近

案例拆解新手友好 第 2/3 篇
不少人在跑长上下文 LLM 推理时只盯着多头减头数,以为 GQA 的 1/头 省缓存就是终点。其实在批量与层数同时放大后,KV 缓存仍会吃掉远超权重的显存,模型直接 OOM 或被迫砍上下文。把共享范围只锁在一层内,是这类崩溃最常见的认知漏点。

「注意力层张量的循环初始化」

在 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。

MQL5 / C++
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 跑一遍确认显存申请耗时。

MQL5 / C++
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 加速模型仅作信号辅助,实盘须严控仓位。

MQL5 / C++
   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 策略测试器用历史数据验证显存占用与初始化耗时。

MQL5 / C++
      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 日志验证每一步返回。

MQL5 / C++
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。外汇与贵金属行情下用这类结构做信号,回测过拟合概率偏高,实盘前务必做样本外验证。

MQL5 / C++
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
把跨层共享的账交给小布盯盘算
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到不同 KV 配置下的显存与延迟权衡,你专注策略而非底层张量排布。

常见问题

MQA 在一层内所有关注头共用单一 KV 投影,缓存降为 1/头;GQA 把头分若干组每组共享 KV,介于多头与 MQA 之间,合理分组可接近原模型品质。
当模型层数深、批量大、上下文长时,KV 缓存随层数与序列长线性增长,仅层内减头无法突破每头下限,极端部署仍可能超出显存带宽。
可以,小布盯盘的品种页内置了不同注意力配置的显存与延迟诊断,省去你手算跨层共享比例的重复劳动。
内存可压到近似 2/层 甚至更低,论文实验显示品质下滑不显著,但稳定性与任务类型强相关,实盘需按数据分布验证。
价差与跳空使序列非平稳,压缩后表征偏差可能放大,任何省内存配置都只是概率上可行,须以历史回测与样本外检验为准。