在 ONNX 模型中使用 float16 和 float8 格式·进阶篇
(2/3)·半精度与 8 位浮点正挤进 MT5 推理链路,精度换算错了模型就静默崩
「用 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 打印值,比看任何说明都实在。
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 终端把这段打印出来,和你自己写的转换函数逐位比对,能直接确认半精度编码有没有偏。 外汇与贵金属杠杆高,这类推理验证只在策略研发环境做,别挂实盘前不核精度——半精度截断误差可能在极端波动时被放大。
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,半精度截断误差可能倾向被实时报价噪声淹没,但回测里它足够稳。
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 直接存盘省内存。外汇与贵金属杠杆高、滑点突变频繁,拿这类底层转换做信号前处理时仍要留误差监控,避免极端行情下精度塌陷。