在 ONNX 模型中使用 float16 和 float8 格式·进阶篇
🧠

在 ONNX 模型中使用 float16 和 float8 格式·进阶篇

(2/3)·半精度与 8 位浮点正挤进 MT5 推理链路,精度换算错了模型就静默崩

进阶 第 2/3 篇
不少人在 MT5 里跑 ONNX 模型,看到 FLOAT16 枚举就直接 Cast,却没注意 BFLOAT16 和标准 FP16 的尾数分布不同。格式参数填错,推理结果不会报错,只会悄悄偏移。外汇与贵金属模型对数值敏感,这类隐性误差在高杠杆下放大很快。

「用 FP16 张量喂 ONNX 模型并回算误差」

在 MT5 里跑 ONNX 推理,如果模型输入是 float16 精度,得先把普通 double 数组压成 16 位无符号短整型。下面这段逻辑先把 12 个元素的 test_data 通过 ArrayToFP16 转成 ushort 数组 data_uint16,转换失败就打印错误码并退出,成功才继续。 转换后代码用 U<ushort> 容器包住 3*4 共 12 个 FP16 值作为模型输入,同时用 ArrayFromFP16 还原出 test_data_float 做对照。循环里把每个 ushort 原值、还原浮点、十六进制都打出来,方便在终端肉眼核对位宽有没有被截歪。 真正推理走 OnnxRun(model_handle, ONNX_NO_CONVERSION, input_float16_values, output_float_values),注意第二个参数禁止框架自作主张做精度转换,否则 FP16 语义就废了。跑完用 output_float_values 和原始 test_data 逐位求差,sum_error 累加绝对值,这是验证模型没偷偷做类型cast的最直接数值证据。 外汇与贵金属行情下接这类模型推理属于高风险操作,回测误差小不代表实盘信号可靠,开 MT5 把这段贴进脚本、用你自己的 res_model 跑一遍 sum_error 打印值,比看任何说明都实在。

MQL5 / C++
  class="type">class="kw">ushort data_uint16[class="num">12];
  if(!ArrayToFP16(data_uint16,test_data,FLOAT_FP16))
    {
      Print("error in ArrayToFP16. error code=",GetLastError());
      class="kw">return(class="kw">false);
    }
  Print("test array:");
  ArrayPrint(test_data);
  Print("ArrayToFP16:");
  ArrayPrint(data_uint16);
  U<class="type">class="kw">ushort> input_float16_values[class="num">3*class="num">4];
  U<class="type">class="kw">float>  output_float_values[class="num">3*class="num">4];
  class="type">class="kw">float test_data_float[];
  if(!ArrayFromFP16(test_data_float,data_uint16,FLOAT_FP16))
    {
      Print("error in ArrayFromFP16. error code=",GetLastError());
      class="kw">return(class="kw">false);
    }
  for(class="type">int i=class="num">0; i<class="num">12; i++)
    {
      input_float16_values[i].value=data_uint16[i];
      PrintFormat("%d class="kw">input value =%f  Hex float16 = %s  class="type">class="kw">ushort value=%d",i,test_data_float[i],ArrayToString(input_float16_values[i].uc),input_float16_values[i].value);
    }
  Print("ONNX class="kw">input array:");
  ArrayPrint(input_float16_values);
  class="type">bool res=OnnxRun(model_handle,ONNX_NO_CONVERSION,input_float16_values,output_float_values);
  if(!res)
    {
      PrintFormat("error in OnnxRun. error code=%d",GetLastError());
      class="kw">return(class="kw">false);
    }
  Print("ONNX output array:");
  ArrayPrint(output_float_values);
class=class="str">"cmt">//---
  class="type">class="kw">double sum_error=class="num">0.0;
  for(class="type">int i=class="num">0; i<class="num">12; i++)
    {
      class="type">class="kw">double delta=test_data[i]-(class="type">class="kw">double)output_float_values[i].value;
      sum_error+=MathAbs(delta);
      PrintFormat("%d output class="type">class="kw">float %f = %s difference=%f",i,output_float_values[i].value,ArrayToString(output_float_values[i].uc),delta);
    }
class=class="str">"cmt">//---
  PrintFormat("test=%s  sum_error=%f",__FUNCTION__,sum_error);
class=class="str">"cmt">//---
  class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| TestCastFloat16ToFloat                                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool TestCastFloat16ToFloat(class="kw">const class="type">uchar &res_model[])
  {
  class="type">uchar model[];
  PatchONNXModel(res_model,model);
class=class="str">"cmt">//--- get model handle
  class="type">long model_handle=INVALID_HANDLE;
class=class="str">"cmt">//--- get model handle
  if(!CreateModel(model_handle,model))
    class="kw">return(class="kw">false);
class=class="str">"cmt">//--- prepare class="kw">input and output shapes
  if(!PrepareShapes(model_handle))
    class="kw">return(class="kw">false);
class=class="str">"cmt">//--- run ONNX model
  if(!RunCastFloat16ToFloat(model_handle))
    class="kw">return(class="kw">false);
class=class="str">"cmt">//--- release model handle

◍ Float16 转 Double 的 ONNX 推理闭环

在 MT5 脚本里跑 ONNX 模型,核心不是算得多花哨,而是把句柄生命周期管明白。TestCastFloat16ToDouble 这个函数先 Patch 模型字节、拿 handle、建输入输出 shape、跑图,最后必须 OnnxRelease 释放,否则 EA 反复调用会漏内存。 看 OnStart 的调度:先测 ExtModel1(Float16→Double),再测 ExtModel2(Float16→Float),任一返回 false 就直接 return 1 退出。这说明模型加载或推理失败是硬错误,不适合静默吞掉。 回测日志给了一组可核对的数字。EURUSD H1 下输入 1~12 的连续 double,经 ArrayToFP16 压成 ushort:1.0 对应 15360(十六进制 [00,3c]),2.0 对应 16384([00,40]),3.0 对应 16896([00,42]),4.0 对应 17408([00,44])。打开 MT5 终端把这段打印出来,和你自己写的转换函数逐位比对,能直接确认半精度编码有没有偏。 外汇与贵金属杠杆高,这类推理验证只在策略研发环境做,别挂实盘前不核精度——半精度截断误差可能在极端波动时被放大。

MQL5 / C++
  OnnxRelease(model_handle);
class=class="str">"cmt">//---
  class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| TestCastFloat16ToDouble                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool TestCastFloat16ToDouble(class="kw">const class="type">uchar &res_model[])
  {
  class="type">uchar model[];
  PatchONNXModel(res_model,model);
class=class="str">"cmt">//---
  class="type">long model_handle=INVALID_HANDLE;
class=class="str">"cmt">//--- get model handle
  if(!CreateModel(model_handle,model))
     class="kw">return(class="kw">false);
class=class="str">"cmt">//--- prepare class="kw">input and output shapes
  if(!PrepareShapes(model_handle))
     class="kw">return(class="kw">false);
class=class="str">"cmt">//--- run ONNX model
  if(!RunCastFloat16ToDouble(model_handle))
     class="kw">return(class="kw">false);
class=class="str">"cmt">//--- release model handle
  OnnxRelease(model_handle);
class=class="str">"cmt">//---
  class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script program start function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnStart(class="type">void)
  {
  if(!TestCastFloat16ToDouble(ExtModel1))
     class="kw">return class="num">1;
  if(!TestCastFloat16ToFloat(ExtModel2))
     class="kw">return class="num">1;
class=class="str">"cmt">//---
  class="kw">return class="num">0;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

Float16 在 ONNX 输入输出里的实际映射

把 EURUSD H1 上的 1~12 连续浮点塞进 float16 容器,打印出的十六进制随整数步进规律变化:5.0 对应 [00,45]、ushort 17664,到 12.0 变成 [00,4a]、ushort 18944,每一步指数位 +1 时 ushort 净增 256。 注意第 8 项输入 9.0 的十六进制是 [80,48] 而非 [00,48],ushort 18560 比 8.0 的 18432 多 128——这是尾数最高位翻转,说明奇数整数在半精度里走了不同编码路径,但还原值仍准确。 喂给 ONNX 的 input array 直接收 ushort 形态:索引 0~11 依次是 15360、16384…18944,和前面逐条 cast 的结果完全对齐;模型回吐的 output array 则是标准 float——[0] 出 1.00000、[5] 出 6.00000,证明半精度入、单精度出这条链路在 MT5 端跑通了。 开 MT5 把这段日志复现一遍,重点看 [80,48] 那行:若你的 broker 点差在 EURUSD 上常态 >1.2 pip,半精度截断误差可能倾向被实时报价噪声淹没,但回测里它足够稳。

MQL5 / C++
TestCastFloat16(EURUSD,H1)      class="num">4 class="kw">input value =class="num">5.000000   Hex float16 = [class="num">00,class="num">45]   class="type">class="kw">ushort value=class="num">17664
TestCastFloat16(EURUSD,H1)      class="num">5 class="kw">input value =class="num">6.000000   Hex float16 = [class="num">00,class="num">46]   class="type">class="kw">ushort value=class="num">17920
TestCastFloat16(EURUSD,H1)      class="num">6 class="kw">input value =class="num">7.000000   Hex float16 = [class="num">00,class="num">47]   class="type">class="kw">ushort value=class="num">18176
TestCastFloat16(EURUSD,H1)      class="num">7 class="kw">input value =class="num">8.000000   Hex float16 = [class="num">00,class="num">48]   class="type">class="kw">ushort value=class="num">18432
TestCastFloat16(EURUSD,H1)      class="num">8 class="kw">input value =class="num">9.000000   Hex float16 = [class="num">80,class="num">48]   class="type">class="kw">ushort value=class="num">18560
TestCastFloat16(EURUSD,H1)      class="num">9 class="kw">input value =class="num">10.000000  Hex float16 = [class="num">00,class="num">49]   class="type">class="kw">ushort value=class="num">18688
TestCastFloat16(EURUSD,H1)      class="num">10 class="kw">input value =class="num">11.000000  Hex float16 = [class="num">80,class="num">49]   class="type">class="kw">ushort value=class="num">18816
TestCastFloat16(EURUSD,H1)      class="num">11 class="kw">input value =class="num">12.000000  Hex float16 = [class="num">00,4a]   class="type">class="kw">ushort value=class="num">18944
TestCastFloat16(EURUSD,H1)      ONNX class="kw">input array:
TestCastFloat16(EURUSD,H1)          [uc] [value]
TestCastFloat16(EURUSD,H1)       [ class="num">0]   ...   class="num">15360
TestCastFloat16(EURUSD,H1)       [ class="num">1]   ...   class="num">16384
TestCastFloat16(EURUSD,H1)       [ class="num">2]   ...   class="num">16896
TestCastFloat16(EURUSD,H1)       [ class="num">3]   ...   class="num">17408
TestCastFloat16(EURUSD,H1)       [ class="num">4]   ...   class="num">17664
TestCastFloat16(EURUSD,H1)       [ class="num">5]   ...   class="num">17920
TestCastFloat16(EURUSD,H1)       [ class="num">6]   ...   class="num">18176
TestCastFloat16(EURUSD,H1)       [ class="num">7]   ...   class="num">18432
TestCastFloat16(EURUSD,H1)       [ class="num">8]   ...   class="num">18560
TestCastFloat16(EURUSD,H1)       [ class="num">9]   ...   class="num">18688
TestCastFloat16(EURUSD,H1)       [class="num">10]   ...   class="num">18816
TestCastFloat16(EURUSD,H1)       [class="num">11]   ...   class="num">18944
TestCastFloat16(EURUSD,H1)      ONNX output array:
TestCastFloat16(EURUSD,H1)          [uc]  [value]
TestCastFloat16(EURUSD,H1)       [ class="num">0]   ...   class="num">1.00000
TestCastFloat16(EURUSD,H1)       [ class="num">1]   ...   class="num">2.00000
TestCastFloat16(EURUSD,H1)       [ class="num">2]   ...   class="num">3.00000
TestCastFloat16(EURUSD,H1)       [ class="num">3]   ...   class="num">4.00000
TestCastFloat16(EURUSD,H1)       [ class="num">4]   ...   class="num">5.00000
TestCastFloat16(EURUSD,H1)       [ class="num">5]   ...   class="num">6.00000

「float16 转 double 的字节落地验证」

在 EURUSD 的 H1 周期下跑一组 float16 到 double 的强制转换测试,索引 0 到 9 依次喂入 1.0 到 10.0 的整数等价浮值,终端打印出的 double 输出与输入完全一致,difference 字段全程为 0.000000。 从内存字节看,1.0 落为 [00,00,00,00,00,00,f0,3f],2.0 则是 [00,00,00,00,00,00,00,40],尾数部分每加 1.0 就抬一个 0x08 步进(如 3.0 为 08,40、4.0 为 10,40),符合 IEEE754 双精度小端排列。 这组日志说明:在 MT5 的 float16 中间类型参与计算再扩回 double 时,小整数域没有精度折损;但外汇与贵金属杠杆高、点值微小,真要拿半精度做报价缓存仍须自行压边界用例,别默认全域零误差。

◍ Float16 与 double 互转的实测字节痕迹

在 EURUSD 的 H1 周期下跑了一轮 Float16 转 double 的校验,输入 11.0 与 12.0 时,输出 double 分别为 11.000000 与 12.000000,对应 8 字节十六进制为 [00,00,00,00,00,00,26,40] 与 [00,00,00,00,00,00,28,40],difference 均为 0.000000,说明该路径在整数段没有精度折损。 接着对 1.0 到 12.0 的连续数组做 ArrayToFP16,得到的 ushort 序列是 15360、16384、16896、17408、17664、17920、18176、18432、18560、18688、18816、18944。拆开看,1.0 的 float16 十六进制是 [00,3c]、ushort 值 15360;9.0 出现了 [80,48]、ushort 18560,高位字节带 0x80 是因为尾数字节跨了边界,这和 IEEE 754 半精度的位布局能对上。 想确认自己环境是否一致,直接在 MT5 里把这段日志逻辑抄出来跑一遍即可。外汇与贵金属行情波动剧烈,这类底层数值实验只解决精度可信度,不预示任何报价方向。

float16 与 ONNX 张量的往返校验

在 EURUSD H1 上跑 TestCastFloat16,输入 11.0 被压成 float16 后十六进制为 [80,49],对应 ushort 18816;输入 12.0 则得到 [00,4a] 与 ushort 18944。这说明半精度编码在 MT5 里就是两个字节的定点映射,直接看 ushort 就能反推原始量级。 把 0~12 的整数序列灌进 ONNX 输入数组,回显的 ushort 从 15360 阶梯式走到 18944,步长大致 1024;模型原样吐回 1.00000 到 12.00000 的 float,再转回 32 位浮点十六进制如 [00,00,80,3f],difference 全为 0.000000。 这种零误差往返验证了半精度张量在推理前后不会被量化噪声吃掉,做特征归一化时可以用 ushort 直接存盘省内存。外汇与贵金属杠杆高、滑点突变频繁,拿这类底层转换做信号前处理时仍要留误差监控,避免极端行情下精度塌陷。

让小布替你校验枚举映射
这些 FP16/FP8 与 float/double 的换算函数和小布盯盘的 AIGC 已内置,打开对应品种页即可看到模型数据类型的兼容提示,你只需专注策略逻辑。

常见问题

标准 FP16 尾数 10 位、指数 5 位;BFLOAT16 尾数 7 位、指数 8 位,动态范围更接近 float32。选错格式会导致数值截断模式不同,推理精度偏移。
e4m3 尾数更多,适合系数权重;e5m2 指数更长,适合梯度类数据。MT5 的 ENUM_FLOAT8_FORMAT 已区分,转换时 fmt 参数必须对应。
可以。小布在品种页的模型诊断模块会读取 ONNX 数据类型枚举并标注,省去你手写脚本打印 ENUM_ONNX_DATA_TYPE 的步骤。
在 MT5 环境下通常反而更快,因内存带宽减半;但边缘重建可能略软,需看你对画质阈值的容忍度。
大概率 fmt 没设对,或源模型用了 E4M3FNUZ 而不支持负零与 Inf,转换函数未匹配子格式就会静默归零。