【腾讯犀牛鸟2026】Youtu-VL 模型的 ncnn 移植与跨平台部署 #6853
Edwardssss
started this conversation in
Show and tell
Replies: 0 comments
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.
【腾讯犀牛鸟2026】Youtu-VL 模型的 ncnn 移植与跨平台部署
任务目标
issue #6789
将腾讯 tencent/Youtu-VL-4B-Instruct 从 PyTorch 推理链路完整移植到 ncnn 推理框架,实现一个纯 C++、运行时仅依赖 ncnn 与标准库的多模态推理程序。
程序读取一张图片和任意文本 prompt,在纯 C++ 内完成全部工作并输出文本:
完成情况
结果展示
纯文本解码
纯文本推理与 Pytorch 参考完全对齐:
"Hello"Hello! How can I help you today?"What is the capital of France?"The capital of France is Paris.云端图文测试
在阿里云 ECS(8 vCPU / 61 GB RAM)上串行运行 8 项视觉任务,纯 CPU 推理,全部通过:
"There is **1 real dog** in the image..."<box><x_173><y_105><x_227><y_199></box><ref>car</ref><box>...(256 tokens)<ref>floor</ref><ref>bed</ref>...<ins><poly>...<ref>dog</ref><ref>cake</ref>...<depth>...<person><box>...逐层精度对齐数据
在 fp32 下测得(与 PyTorch 参考逐模块比对):
模型导出策略
模块拆解
Youtu-VL 拆为 5 个模块,各自采用最优导出方式:
关键导出实践
pnnx 无法稳定转换完整多模态模型: 原始模型包含 Hugging Face remote code、动态 shape、FlashAttention、窗口注意力等复杂结构,直接整体转换会生成 pnnx 无法稳定解析或 ncnn 无法正确执行的图。解决办法是按计算边界拆分子图,动态部分全部使用 C++ runtime。
**不应直接 trace HF 层:**其内部的 dtype 分支与控制流会使 pnnx 解析失败。所以改用纯
nn.Linear与 RMSNorm 进行重建,从 HF 逐名拷贝权重,并以随机输入自检后放行(视觉层相对误差 2.8e-07,MLA 层 6.8e-08)。ncnn .bin 导出: 每组权重前必须写入
\x00\x00\x00\x00flag_struct 标记。MLA 自定义层实现
架构设计
MLA 继承
ncnn::Layer实现自定义层。每层持有 12 组权重,实现 decoder layer 的 8 步前向计算:KV Cache 设计
MLA 使用压缩 KV cache:只缓存
kv_latent_out(形状[total_kv_len, 512]),而非展开后的完整 K/V。每轮推理时从压缩 latent 实时展开出 k_nope、k_rot、v。相比标准 MHA 大幅减少 KV cache 显存占用。KV Cache 的拼接必须为 chronological(旧→新)。
大权重加载
40 层 decoder 共 16.7GB 权重。采用逐层
fread:每层先读 4 字节长度,再读该长度的 float 数据,每层约 0.42GB,分 40 次加载,避免一次性 mmap 16GB 导致内存不足。实现难点
1. Interleave RoPE 的双重融合风险
YoutuLLM 的
rope_interleave=True,其 RoPE 在标准 rotate-half 之前多一步反交错重排:两个环节都可能被 pnnx 融合成 ncnn 的
RotaryEmbed,而该算子的轴语义与此处的交错布局并不匹配。解决方案:将反交错与 rotate-half 合并为两个常量矩阵
P与PR = P @ R,改写为:2. ncnn 在小尺寸张量上越界
ncnn 的 RMSNorm 与 UnaryOp 在
h小于 SIMD 打包宽度时越界崩溃,而 decode 的查询长度天然为 1。解决方案:将 decode 图按查询长度 8 而非 1 导出:runtime 输入 8 份复制的当前 token embedding,仅取输出第 0 行。
3. lm_head Gemm 全零问题
ncnn 的
Gemm_x86::create_pipeline在处理[M=1, N=283386, K=2560]的极端瘦矩阵时,tile packing 产生全零输出。解决方案:权重以
[vocab, hidden]非转置方式存储。Embedding 与 lm_head 均不进图,而是以原始二进制存储,runtime 直接查表(embedding)、以 final_norm 后逐行点积取 argmax(lm_head)。跨平台 BLAS 条件编译
调试方法
移植前先运行原始模型,获取 Pytorch 输出参考:输入 token ids、pixel_values、vision 输出、merger 输出、逐层 hidden、prefill logits、greedy token。此后每完成一个模块,都与和参考输出进行逐级比对。
Tokenizer 与图片预处理这类需要逐位复刻的模块,先在 Python 中写出原型并对参考验证通过,再逐行移植。
构建与运行
依赖
libblas-dev/ macOS: Accelerate.framework / Windows: vcpkg openblas)快速开始
Windows 注意事项
GetCommandLineW取 UTF-16 转 UTF-8SetConsoleOutputCP(CP_UTF8),否则输出乱码~/)而非 NTFS,否则 CMakeconfigure_file报权限错误已知限制
YoutuDensePrediction后处理,本移植未覆盖All reactions