HunyuanOCR ncnn 移植 #6794
AiChiTuDouPian
started this conversation in
Show and tell
Replies: 1 comment 1 reply
-
|
@AiChiTuDouPian 你提供的 gitee 仓库无法访问,请检查,或在github上也放一个 |
Beta Was this translation helpful? Give feedback.
1 reply
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
HunyuanOCR-ncnn 技术报告
作者: AiChiTuDouPian
日期: 2026 年 7 月
版本: v0.1
摘要
本报告详细记录了将腾讯混元 HunyuanOCR(约 1B 参数的多模态 OCR 模型)从 PyTorch 完整移植到 ncnn 推理框架的技术细节。移植后的 C++17 运行时在 fp32 精度下与 PyTorch 原版数值完全对齐(误差
< 1e-5),greedy 解码的最终 OCR 文本一致。本报告重点讨论六个在移植过程中遇到的关键难点:(1) ncnn Gemm 层权重二进制布局的误读;(2) MultiHeadAttention 层
flag_struct的 432 字节级联偏移;(3) ViT 位置嵌入从 32×32 到 40×40 的双线性插值;(4) PatchMerger(Perceptron)绕开 ncnn 的 C++ 直读方案;(5) EOS 停止 token 与 chat 模板语义的不一致;(6) XD-RoPE 4 轴旋转 + YaRN 长上下文缩放。每个难点都给出了定位过程、根因分析和修复方案。1. 项目背景
1.1 模型架构
HunyuanOCR 是一个视觉-语言多模态模型,由 5 个主要组件构成:
1.2 关键超参数
1.3 设计目标
< 1e-5,最终文本完全一致。2. 整体架构
2.1 子模型拆分策略
为了适配 ncnn 的静态形状约束并降低单次 trace 的复杂度,我们把 PyTorch 模型拆分成 5 个独立的 ncnn 子模型,外加一个 C++ 直读的 Perceptron:
vision_encoder.ncnn.{param,bin}perceptron_weights.binprojector.ncnn.{param,bin}embed.ncnn.{param,bin}decoder.ncnn.{param,bin}lm_head.ncnn.{param,bin}2.2 为什么 Perceptron 不走 ncnn
Perceptron 是 HunYuanVisionTransformer 的
perceiver_resampler模块,包含 RMSNorm、stride-2 Conv2d、GELU、1×1 Conv2d、Linear。我们在尝试通过 pnnx 导出时遇到两个问题:(h, w, c)的解释与 PyTorch 的(B, C, H, W)不完全一致,stride=2 时边界处理有差异。param文件里 Conv2d 权重 offset 在 bin 里对不上 ncnnload_model的读取顺序。最终方案是绕开 ncnn,把 Perceptron 的所有权重(RMSNorm gamma、Conv2d weights/biases、Linear weight/bias)打包成一个 fp32 raw 文件
perceptron_weights.bin,在 C++ 里用fread直读并手工实现前向。这给了我们对数值的完全控制。2.3 运行时调用流
3. 关键难点与解决方案
3.1 难点 1:ncnn Gemm 权重二进制布局误读
现象
ViT 第一层 MLP 的输出与 PyTorch 完全不对齐(cosine similarity ≈ 0.1)。进一步观察发现所有 Gemm 层的结果都偏差巨大,但 bias 加上去后数值范围又接近预期,暗示 bias 是对的而 weight 错位了。
根因
ncnn 的 Gemm 层参数有:
当
constantA=0, constantB=1, constantC=1时,load_model()只读取B_data和C_data,不读A_data。B_data的形状是(N, K)row-major,恰好等于 PyTorchLinear.weight的形状(weight.shape = (out_features, in_features))。我们最初的导出脚本错误地假设
constantA=1并按(A_data, B_data, C_data)三元组写入 bin,导致每个 Gemm 层多写了一个(K, K)的 A 矩阵(单位阵),后续所有层的 offset 全部错位。修复
验证:每个 Gemm 层写入的字节数 =
N*K*4 + N*4,与 ncnnload_model读取量完全一致。3.2 难点 2:MHA
flag_struct引发的字节偏移现象
Gemm 修复后,ViT 的 MLP 路径正确,但 attention 路径(Q/K/V/O 四个 Linear)的输出仍然完全错误(cos ≈ -0.03)。单独读取 MHA 权重与 PyTorch 对比完全一致(max_diff=0.0)。
定位过程
multiheadattention.cpp的load_model加printf打印读到的权重前 4 个元素——没有输出。printf不会生效。必须重编 ncnn 并 install。load_model对type=0的mb.load(size, 0)会先读 4 字节flag_struct(标记 fp32/fp16/int8),再读权重数据;对type=1的mb.load(size, 1)(bias)不读 flag,直接读数据。根因
预编译的 ncnn(位于
C:\hunyuan\ncnn\build\install)是一个旧版本,其multiheadattention.cpp的load_model没有正确处理type=0的 flag_struct。这导致每个 MHA 权重被少读 4 字节 flag,整个 bin 文件的 offset 级联错位:修复
重编 ncnn 并重新 install:
然后重编 hunyuan-ocr-ncnn(因为 ncnn 是静态链接的)。修复后所有 27 层 transformer 的 attention 输出与 PyTorch cosine similarity = 1.0。
验证脚本
verify_mha_weights.py直接按 offset 计算 MHA 权重在 bin 文件中的位置,跳过 flag 读取,对比 PyTorch 权重,结果 max_diff = 0.0。这证实了权重本身没问题,是 ncnn 的 load_model 没正确读 flag。3.4 难点 4:Perceptron(PatchMerger)绕开 ncnn 的 C++ 直读
问题
Perceptron 的
Conv2d(stride=2)在 ncnn 里对 3D 输入的解释与 PyTorch 不一致:(B, C, H, W)经过 stride=2 Conv2d →(B, C', H/2, W/2)(h, w, c),stride=2 Conv2d 的边界处理略有差异实现方案
用 C++ 手写 Perceptron 的前向,绕开 ncnn 的 Conv2d:
所有权重(RMSNorm gamma、Conv2d kernel/bias、Linear weight/bias)在
perceptron_weights.bin里按顺序排列,C++ 用fread一次性读入并手工实现 conv2d + gemm + gelu + rmsnorm。优势
printf打印中间张量的 RMS、min/max。3.5 难点 5:EOS token 语义不一致
现象
OCR 文本正确生成,但模型不停止,继续输出额外的评论、翻译、解释。例如 En.png 的正确输出是
"Compressed and interpretable light-weight model ready for edge deployment",但 C++ 运行时在这之后继续生成了几十 token 的中文翻译。定位
对比 C++ 和 PyTorch 的生成序列:
generate()<|hy_Assistant|>)C++ 的
is_eos_token_id()只把 120020(kEos=kEod,即<|hy_place▁holder▁no▁2|>)当作 EOS。但 HunyuanOCR 的 greedy 解码实际上在 120007(<|hy_Assistant|>)处停止,这个 token 在 chat 格式里既是 turn-start 也是 turn-end。修复
同时更新
common.h里kAssistant的注释,明确其双重语义。修复后行为
En.png(889×69)图片中的文本内容是:Compressed and interpretable light-weight model ready for edge deploymentzimu.png(1441×803)对不起 我是诺曼·斯贝尔曼 Sorry, I'm Norm Spellman.3.6 难点 6:XD-RoPE 4 轴旋转 + YaRN 缩放
模型要求
HunyuanOCR 的 LLM 使用 XD-RoPE:前 64 维用 4 轴旋转(
xdrope_section=[16,16,16,16],分别对应t, x, y, z四个位置索引),后 64 维用标准 1D RoPE(仅t轴)。ViT 的 attention 不使用 RoPE(见 3.3 节)。实现
YaRN 长上下文缩放
配置里的
rope_scaling参数:{ "alpha": 1000.0, "beta_fast": 32.0, "beta_slow": 1.0, "factor": 1.0, "mscale": 1.0, "mscale_all_dim": 1.0, "type": "yarn" }这些参数影响 cos/sin 表的频率计算,但
factor=1.0意味着实际不做缩放(模型训练时未启用长上下文)。实现时保留 YaRN 公式以兼容未来扩展。验证
tests/test_xdrope.cpp和scripts/xdrope_reference.py提供 Python 参考实现,对比 C++ 和 Python 的 cos/sin 值,误差< 1e-6。4. 测试结果
4.1 数值对齐测试
使用
scripts/verify_pytorch_alignment.py进行 8 阶段对齐验证:< 1e-6< 1e-5< 1e-5< 1e-5< 1e-5< 1e-54.2 ViT 逐层对比
compare_block0_v2.py对比第一个 transformer block 的每个子步骤:27 层 transformer 全部对比,所有层 cosine = 1.0000。
4.3 MHA 权重验证
verify_mha_weights.py直接按 offset 读取vision_encoder.ncnn.bin中的 MHA 权重,与 PyTorch 对比:4.4 端到端 OCR 测试
En.png图片中的文本内容是:Compressed and interpretable light-weight model ready for edge deploymentzimu.png对不起 我是诺曼·斯贝尔曼 Sorry, I'm Norm Spellman.4.5 Token 级对齐
对
En.png的生成序列做 token-by-token 对比:23 个 token 完全一致,包括最后的 EOS token 120007。
4.6 单元测试
5. 性能分析
5.1 各阶段耗时(En.png, CPU 4 线程)
5.2 性能瓶颈
5.3 优化路线
6. 经验总结
6.1 ncnn 移植的常见陷阱
load_model读哪些 blob。写 bin 时必须与 param 里的标志严格对应。type=0的权重读 4 字节 flag,type=1的 bias 不读。如果 ncnn 版本过旧,可能没有这个处理。(h, w, c)与 PyTorch 的(B, C, H, W)在 stride>1 时边界处理有差异。Reshape层),长序列需要 padding。6.2 调试方法论
cmake --build . --target install再重编项目。6.3 模型特殊行为
HunYuanVisionAttention.eager_attention_forward的position_embeddings参数存在但未参与计算。<|hy_Assistant|>) 在 chat 格式里既是 turn-start 也是 turn-end,必须在停止条件里包含。7. 结论
本项目成功将 HunyuanOCR (1B) 从 PyTorch 完整移植到 ncnn,在 fp32 精度下实现了与原版完全对齐的数值结果(误差
< 1e-5,最终文本一致)。六个关键难点(Gemm 布局、MHA flag、位置嵌入插值、Perceptron 直读、EOS 语义、XD-RoPE)均有明确的根因分析和可复现的修复方案。当前版本已可用于生产环境的 CPU 推理,未来工作集中在 KV cache、Vulkan 量化和 aarch64 int8 优化上,以进一步提升性能并拓展部署场景。
参考文献
项目链接
https://gitee.com/zyxssss/hunyuan-ocr-ncnn
模型文件链接
https://huggingface.co/xxzigou/HunyuanOCR-ncnn
附录
测试及打印
使用混元OCR官网的测试图片,结果如下:
Beta Was this translation helpful? Give feedback.
All reactions