神经网络变得简单(第 87 部分):时间序列补片化·进阶篇
📘

神经网络变得简单(第 87 部分):时间序列补片化·进阶篇

第 2/3 篇

◍ GPU 上的 Adam 权重更新内核

这段 OpenCL 内核跑在显卡上,用三维全局 ID 把输入窗口、输出窗口和变量维度摊平并行。c、r、v 分别取 get_global_id(0/1/2),对应输入位置、输出位置和变量索引,避免 CPU 端逐权重循环。 梯度累加循环里,当 c == window_in 时乘 1,否则乘对应 inputs[i],再乘 outputs_gr[0];total 由 inputs_total/(variables*step) 算出,决定回看多少样本块。梯度若算出 NaN 直接归零,防止显存污染扩散。 权重更新套了 Adam:mt 是一阶矩(b1 默认 0.9 倾向),vt 是二阶矩(b2 默认 0.999),delta 用学习率 l 乘 mt/(sqrt(vt)+1e-37) 再减 L1/L2 正则项。fabs(delta)>0 才写回 clamp 到 ±MAX_WEIGHT,否则跳过省一次写操作。 在 MT5 里把 b1、b2 和 l 接进神经网络面板,若显存报错多半是 window_in*variables 超了显卡 max work-group size,先调小 iStep 验证。

MQL5 / C++
 __global class="type">float *weights_m,
 __global class="type">float *weights_v,
 const class="type">int inputs_total,
 const class="type">float l,
 const class="type">float b1,
 const class="type">float b2,
 class="type">int step
 )
{
 const class="type">int c = get_global_id(class="num">0);
 const class="type">int r = get_global_id(class="num">1);
 const class="type">int v = get_global_id(class="num">2);
 const class="type">int window_in = get_global_size(class="num">0) - class="num">1;
 const class="type">int window_out = get_global_size(class="num">1);
 const class="type">int variables = get_global_size(class="num">2);
 const class="type">int start_input = c * variables + v;
 const class="type">int step_input = step * variables;
 const class="type">int start_out = v * window_out + r;
 const class="type">int step_out = variables * window_out;
 const class="type">int total = inputs_total / (variables * step);
 class="type">float grad = class="num">0;
 for(class="type">int p = class="num">0; p < total; p++)
   {
     class="type">int i = start_input + i * step_input;
     class="type">int o = start_out + i * step_out;
     grad += (c == window_in ? class="num">1 : inputs[i]) * outputs_gr[class="num">0];
   }
 if(isnan(grad))
     grad = class="num">0;
 const class="type">int shift_weights = (window_in + class="num">1) * (window_out * v + r) + c;
class=class="str">"cmt">//---
 class="type">float weight = weights[shift_weights];
 class="type">float mt = b1 * weights_m[shift_weights] + (class="num">1 - b1) * grad;
 class="type">float vt = b2 * weights_v[shift_weights] + (class="num">1 - b2) * pow(grad, class="num">2);
 class="type">float delta = l * (mt / (sqrt(vt) + class="num">1.0e-37f) - (l1 * sign(weight) + l2 * weight));
 if(fabs(delta) > class="num">0)
     weights[shift_weights] = clamp(weight + delta, -MAX_WEIGHT, MAX_WEIGHT);
 weights_m[shift_weights] = mt;
 weights_v[shift_weights] = vt;
}
class CNeuronPatching   :  class="kw">public CNeuronBaseOCL
  {
class="kw">protected:
   class="type">uint         iWindowIn;
   class="type">uint         iStep;

Patch 神经元的 OpenCL 初始化与内存预留

在 MT5 的 OpenCL 神经网络框架里,CNeuronPatching 这个类专门做局部窗口补丁(patching)映射。它的成员变量先声明了窗口输出长度 iWindowOut、变量数 iVariables 和补丁数 iCount,三个 CBufferFloat 分别承载权重、一阶动量和二阶动量,供 GPU 端批量更新用。 Init 函数是落地关键。它先调用基类 CNeuronBaseOCL::Init,传入的神经元输入维度被强行拉成 window_out * count * variables;随后用 MathMax 把 window_in、window_out、step、variables、count 全部兜底到至少 1,避免零宽窗口在显存上炸核。 内存预留看这句:total = (window_in + 1) * window_out * variables,再 Reserve 到 cPatchWeights。假设 window_in=30、window_out=10、variables=4,total 就是 31*10*4=1240 个 float,约 4.96 KB 显存;k = 1/sqrt(total) ≈ 0.0284,用作权重初始化的缩放因子,偏向 Xavier 风格的稳定收敛。 让小布替你跑这套 把上面 Init 的参数在 MT5 策略测试器里改成 EURUSD 的 H1 序列(window_in=48、variables=5),观察 Reserve 后的显存占用是否随 total 线性增长;外汇与贵金属杠杆交易高风险,参数验证仅限技术层面,不涉及任何收益暗示。

MQL5 / C++
class="type">uint iWindowOut;
class="type">uint iVariables;
class="type">uint iCount;
class=class="str">"cmt">//---
CBufferFloat cPatchWeights;
CBufferFloat cPatchFirstMomentum;
CBufferFloat cPatchSecondMomentum;
class=class="str">"cmt">//---
class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL);
class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL);
class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *NeuronOCL);
class="kw">public:
 CNeuronPatching(class="type">void){};
 ~CNeuronPatching(class="type">void){};
 class=class="str">"cmt">//---
 class="kw">virtual class="type">bool Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window_in,
 class="type">uint step, class="type">uint window_out, class="type">uint count, class="type">uint variables,
 ENUM_OPTIMIZATION optimization_type, class="type">uint batch);
 class=class="str">"cmt">//---
 class="kw">virtual class="type">bool Save(class="type">int const file_handle);
 class="kw">virtual class="type">bool Load(class="type">int const file_handle);
 class=class="str">"cmt">//---
 class="kw">virtual class="type">int Type(class="type">void) const { class="kw">return defNeuronPatchingOCL; }
 class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj);
 class=class="str">"cmt">//---
 class="kw">virtual class="type">bool WeightsUpdate(CNeuronBaseOCL *source, class="type">float tau);
};
class="type">bool CNeuronPatching::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
 class="type">uint window_in, class="type">uint step, class="type">uint window_out,
 class="type">uint count, class="type">uint variables,
 ENUM_OPTIMIZATION optimization_type, class="type">uint batch
 )
 {
 if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window_out * count * variables,
 optimization_type, batch))
 class="kw">return false;
 iWindowIn = MathMax(window_in, class="num">1);
 iWindowOut = MathMax(window_out, class="num">1);
 iStep = MathMax(step, class="num">1);
 iVariables = MathMax(variables, class="num">1);
 iCount = MathMax(count, class="num">1);
 class="type">int total = class="type">int((window_in + class="num">1) * window_out * variables);
 if(!cPatchWeights.Reserve(total))
 class="kw">return false;
 class="type">float k = class="type">float(class="num">1 / sqrt(total));

「补丁层权重初始化与核函数参数绑定」

在 CNeuronPatching 的初始化段,先用 for 循环把 total 个权重塞进 cPatchWeights,每个权重由 GenerateWeight() 经 (2*x*k - k)*WeightsMultiplier 映射,任一 Add 失败立即返回 false,说明权重缓冲必须全量写入才能继续。 随后 cPatchWeights.BufferCreate(OpenCL) 把权重推到显存,一阶、二阶动量缓冲则用 BufferInit(total,0) 清零后再 BufferCreate,三个 Create 任一失败同样中断——这套顺序在 MT5 的 OpenCL 环境里若显存不足会直接暴露在第 2~3 个 Create 的 false 上。 feedForward 里先判空 NeuronOCL 和 OpenCL,再设 global_work_size 为 {iCount, iWindowOut, iVariables} 三维,对应补丁数量、输出窗口与变量数;任何 SetArgumentBuffer / SetArgument 失败都用 printf 打 __FUNCTION__、GetLastError()、__LINE__,方便在策略测试器日志里定位是哪一根参数没绑上。 把这段代码原样丢进你的 EA 神经元类,跑一次 EURUSD 的 H1 回测,若日志出现 'Error of set parameter kernel' 且行号落在 SetArgument(def_k_PatchCreate, def_k_ptc_step,...) 附近,大概率是你 iStep 与上游 NeuronOCL.Neurons() 的整除关系没对齐,外汇与贵金属品种杠杆高,这类底层绑定错误会放大实盘滑点风险。

MQL5 / C++
  for(class="type">int i = class="num">0; i < total; i++)
    {
      if(!cPatchWeights.Add((class="num">2 * GenerateWeight()*k - k)*WeightsMultiplier))
        class="kw">return false;
    }
  if(!cPatchWeights.BufferCreate(OpenCL))
    class="kw">return false;
  if(!cPatchFirstMomentum.BufferInit(total, class="num">0) ||
     !cPatchFirstMomentum.BufferCreate(OpenCL))
    class="kw">return false;
  if(!cPatchSecondMomentum.BufferInit(total, class="num">0) ||
     !cPatchSecondMomentum.BufferCreate(OpenCL))
    class="kw">return false;
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CNeuronPatching::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
  if(!NeuronOCL || !OpenCL)
    class="kw">return false;
class=class="str">"cmt">//---
  class="type">uint global_work_offset[class="num">3] = {class="num">0, class="num">0, class="num">0};
  class="type">uint global_work_size[class="num">3] = {iCount, iWindowOut, iVariables};
  ResetLastError();
  if(!OpenCL.SetArgumentBuffer(def_k_PatchCreate, def_k_ptc_inputs, NeuronOCL.getOutputIndex()))
    {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      class="kw">return false;
    }
  if(!OpenCL.SetArgumentBuffer(def_k_PatchCreate, def_k_ptc_weights, cPatchWeights.GetIndex()))
    {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      class="kw">return false;
    }
  if(!OpenCL.SetArgumentBuffer(def_k_PatchCreate, def_k_ptc_outputs, Output.GetIndex()))
    {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      class="kw">return false;
    }
  if(!OpenCL.SetArgument(def_k_PatchCreate, def_k_ptc_activation, (class="type">int)activation))
    {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      class="kw">return false;
    }
  if(!OpenCL.SetArgument(def_k_PatchCreate, def_k_ptc_inputs_total, (class="type">int)NeuronOCL.Neurons()))
    {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      class="kw">return false;
    }
  if(!OpenCL.SetArgument(def_k_PatchCreate, def_k_ptc_window_in, (class="type">int)iWindowIn))
    {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      class="kw">return false;
    }
  if(!OpenCL.SetArgument(def_k_PatchCreate, def_k_ptc_step, (class="type">int)iStep))
    {

◍ 反向传播里补丁梯度的参数绑定

在 CNeuronPatching::calcInputGradients 里,OpenCL 核函数 def_k_PatchHiddenGradient 需要把前向层的输出、梯度以及补丁权重全部绑定到显存缓冲,否则 GPU 端算不出隐藏层梯度。 代码先校验 NeuronOCL 与 OpenCL 句柄非空,随后定义二维工作组:global_work_offset 固定为 {0,0},global_work_size 取 NeuronOCL.Neurons()/iVariables 与 iVariables,这决定了多少线程并行处理输入块。 每一条 SetArgumentBuffer 失败都会用 printf 打出 __FUNCTION__、GetLastError() 和 __LINE__,直接 return false 中断。这种写法让你在 MT5 Experts 日志里能精确到哪一行绑定崩了。 激活函数类型、输出总数 iCount、输入窗口 iWindowIn 都以 int 强转后通过 SetArgument 传入;其中 iWindowIn 控制补丁在时序上的回溯宽度,调它等于改特征抽取的感受野。 外汇与贵金属行情跳变频繁,这类 GPU 神经层若参数越界,EA 可能在实盘高波动时静默失效,务必在策略测试器用历史 tick 跑一遍核调用。

MQL5 / C++
  if(!OpenCL.Execute(def_k_PatchCreate, class="num">3, global_work_offset, global_work_size))
  {
    printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError());
    class="kw">return false;
  }
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CNeuronPatching::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
  if(!NeuronOCL || !OpenCL)
    class="kw">return false;
class=class="str">"cmt">//---
  class="type">uint global_work_offset[class="num">2] = {class="num">0, class="num">0};
  class="type">uint global_work_size[class="num">2] = {NeuronOCL.Neurons() / iVariables, iVariables};
  ResetLastError();
  if(!OpenCL.SetArgumentBuffer(def_k_PatchHiddenGradient, def_k_pthg_inputs, NeuronOCL.getOutputIndex()))
  {
    printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
    class="kw">return false;
  }
  if(!OpenCL.SetArgumentBuffer(def_k_PatchHiddenGradient, def_k_pthg_inputs_gr, NeuronOCL.getGradientIndex()))
  {
    printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
    class="kw">return false;
  }
  if(!OpenCL.SetArgumentBuffer(def_k_PatchHiddenGradient, def_k_pthg_weights, cPatchWeights.GetIndex()))
  {
    printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
    class="kw">return false;
  }
  if(!OpenCL.SetArgumentBuffer(def_k_PatchHiddenGradient, def_k_pthg_outputs_gr, Gradient.GetIndex()))
  {
    printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
    class="kw">return false;
  }
  if(!OpenCL.SetArgument(def_k_PatchHiddenGradient, def_k_pthg_activation, (class="type">int)NeuronOCL.Activation()))
  {
    printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
    class="kw">return false;
  }
  if(!OpenCL.SetArgument(def_k_PatchHiddenGradient, def_k_pthg_outputs_total, (class="type">int)iCount))
  {
    printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
    class="kw">return false;
  }
  if(!OpenCL.SetArgument(def_k_PatchHiddenGradient, def_k_pthg_window_in, (class="type">int)iWindowIn))
  {
    printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
    class="kw">return false;
  }

补丁层在 GPU 上的梯度与权重落地

在 MT5 的 OpenCL 封装里,CNeuronPatching 把隐藏层梯度计算拆成两次内核调用:先给 def_k_PatchHiddenGradient 内核塞入步长 iStep 与输出窗口 iWindowOut,再执行 2 维全局工组尺寸。任何一次 SetArgument 返回 false,都会用 printf 打出 __FUNCTION__ 与 GetLastError(),直接 return false,这意味着 GPU 参数绑定失败会立即中断训练前向。 权重更新走的是 Adam 变体内核 def_k_PatchUpdateWeightsAdam。global_work_size 被设为 {iWindowIn+1, iWindowOut, iVariables},也就是输入窗口加偏置项、乘输出窗口、乘变量数三维并发。五个缓冲区(输入激活、输出梯度、权重、一阶矩、二阶矩)必须依次用 SetArgumentBuffer 绑定到对应索引,漏绑一个就回 false。 实际调参时,iWindowIn 与 iWindowOut 直接决定显存占用和内核发射规模。若回测中 EURUSD 的 H1 样本在 iVariables=10、iWindowOut=64 时内核超时,可优先砍 iWindowIn 观察 Execute 是否稳定返回 true。外汇与贵金属杠杆品种下跑这套 OCL 训练,显存崩坏可能导致 MT5 终端卡死,属于高风险操作。

MQL5 / C++
  if(!OpenCL.SetArgument(def_k_PatchHiddenGradient, def_k_pthg_step, (class="type">int)iStep))
    {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      class="kw">return false;
    }
  if(!OpenCL.SetArgument(def_k_PatchHiddenGradient, def_k_pthg_window_out, (class="type">int)iWindowOut))
    {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      class="kw">return false;
    }
  if(!OpenCL.Execute(def_k_PatchHiddenGradient, class="num">2, global_work_offset, global_work_size))
    {
      printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError());
      class="kw">return false;
    }
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CNeuronPatching::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
  if(!NeuronOCL || !OpenCL)
      class="kw">return false;
class=class="str">"cmt">//---
  class="type">uint global_work_offset[class="num">3] = {class="num">0, class="num">0, class="num">0};
  class="type">uint global_work_size[class="num">3] = {iWindowIn + class="num">1, iWindowOut, iVariables};
  ResetLastError();
  if(!OpenCL.SetArgumentBuffer(def_k_PatchUpdateWeightsAdam, def_k_ptuwa_inputs, NeuronOCL.getOutputIndex()))
    {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      class="kw">return false;
    }
  if(!OpenCL.SetArgumentBuffer(def_k_PatchUpdateWeightsAdam, def_k_ptuwa_outputs_gr, getGradientIndex()))
    {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      class="kw">return false;
    }
  if(!OpenCL.SetArgumentBuffer(def_k_PatchUpdateWeightsAdam, def_k_ptuwa_weights, cPatchWeights.GetIndex()))
    {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      class="kw">return false;
    }
  if(!OpenCL.SetArgumentBuffer(def_k_PatchUpdateWeightsAdam, def_k_ptuwa_weights_m,
cPatchFirstMomentum.GetIndex()))
    {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      class="kw">return false;
    }
  if(!OpenCL.SetArgumentBuffer(def_k_PatchUpdateWeightsAdam, def_k_ptuwa_weights_v,
cPatchSecondMomentum.GetIndex()))
    {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      class="kw">return false;
    }
  if(!OpenCL.SetArgument(def_k_PatchUpdateWeightsAdam, def_k_ptuwa_inputs_total, (class="type">int)NeuronOCL.Neurons()))
    {

「把 Adam 参数塞进 OpenCL 内核再搭编码器」

在 GPU 上跑神经网络时,把学习率、一阶矩衰减 b1、二阶矩衰减 b2 和当前步数 iStep 逐个通过 SetArgument 喂给 PatchUpdateWeightsAdam 内核,任何一步失败就打印函数名、错误码和行号并退出,避免静默崩在显存里。 内核参数绑定完后用 Execute 拉起计算,work_dim 设为 3,配合 global_work_offset 与 global_work_size 控制并行粒度;若返回 false 同样要捕获 GetLastError 方便在 MT5 Experts 日志里定位。 CreateEncoderDescriptions 负责把编码器的层结构建起来:先清数组,再压入输入层,神经元类型 defNeuronBaseOCL,节点数 = HistoryBars * BarDescr,优化器直接挂 ADAM。 第二层用 defNeuronBatchNormOCL 做批归一化,batch 写死 1000,第三层接 defNeuronLearnabledPE 可学习位置编码,节点数沿用前层 prev_count;外汇与贵金属行情高频跳变,这类结构过拟合风险偏高,参数请在历史样本上先小步验证。

MQL5 / C++
    if(!OpenCL.SetArgument(def_k_PatchUpdateWeightsAdam, def_k_ptuwa_l, lr))
    {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      class="kw">return false;
    }
    if(!OpenCL.SetArgument(def_k_PatchUpdateWeightsAdam, def_k_ptuwa_b1, b1))
    {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      class="kw">return false;
    }
    if(!OpenCL.SetArgument(def_k_PatchUpdateWeightsAdam, def_k_ptuwa_step, (class="type">int)iStep))
    {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      class="kw">return false;
    }
    if(!OpenCL.SetArgument(def_k_PatchUpdateWeightsAdam, def_k_ptuwa_b2, b2))
    {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      class="kw">return false;
    }
    if(!OpenCL.Execute(def_k_PatchUpdateWeightsAdam, class="num">3, global_work_offset, global_work_size))
    {
      printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError());
      class="kw">return false;
    }
class=class="str">"cmt">//---
    class="kw">return true;
  }
class="type">bool CreateEncoderDescriptions(CArrayObj *encoder)
  {
class=class="str">"cmt">//---
    CLayerDescription *descr;
class=class="str">"cmt">//---
    if(!encoder)
    {
      encoder = new CArrayObj();
      if(!encoder)
        class="kw">return false;
    }
class=class="str">"cmt">//--- Encoder
    encoder.Clear();
class=class="str">"cmt">//--- Input layer
    if(!(descr = new CLayerDescription()))
      class="kw">return false;
    descr.type = defNeuronBaseOCL;
    class="type">int prev_count = descr.count = (HistoryBars * BarDescr);
    descr.activation = None;
    descr.optimization = ADAM;
    if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">1
    if(!(descr = new CLayerDescription()))
      class="kw">return false;
    descr.type = defNeuronBatchNormOCL;
    descr.count = prev_count;
    descr.batch = class="num">1000;
    descr.activation = None;
    descr.optimization = ADAM;
    if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">2
    if(!(descr = new CLayerDescription()))
      class="kw">return false;
    descr.type = defNeuronLearnabledPE;
    descr.count = prev_count;
    if(!encoder.Add(descr))
    {

常见问题

把learning rate、beta1、beta2、epsilon以及时间步计数器打包成常量缓冲区传入内核,在内核里按标准Adam公式逐元素更新权重与动量。
需为输入补丁、权重矩阵、梯度矩阵各预留一份显存,并按最大batch的补丁数预分配,避免运行时重复申请导致延迟。
可以。小布能按你给的神经元结构自动生成OpenCL参数绑定清单,并提示每层权重与梯度显存预留是否匹配。
必须将补丁索引、对应权重指针、输入补丁缓存三者绑定一致,否则梯度回传会错位导致训练不收敛。
确保每次迭代后把GPU端更新好的权重拷回主机端对应层,并在编码器序列化时按层名对齐保存。