两天踩8个坑!MLC-LLM 交叉编译部署 Jetson 全记录,性能竟然没损失?

场景:Jetson Orin 8GB 部署 Qwen2.5-1.5B/3B 大模型

前言:为什么我要折腾交叉编译?

最近在搞边缘设备上的大模型部署,手头有一块 Jetson Orin 8GB 开发板。本来用 MLC-LLM 的 JIT(Just-In-Time)编译方案跑起来了,但每次启动都要在设备上编译 2 分钟,还得挂 swap 防 OOM,这谁受得了?

而且网络不稳定的时候,第一次启动直接卡死在下载 HuggingFace 权重上…

痛点有三

  1. ❌ 启动太慢(2 分钟编译)
  2. ❌ 容易 OOM(8GB 内存编译不够用)
  3. ❌ 离线不了(每次都要联网下权重)

于是我想:能不能在 PC 上把模型编译好,直接拷贝到 Jetson 上跑?

答案是:能! 但过程踩了 8 个坑,花了我两天时间…

先说结论(不看过程的可以直接跳最后)

✅ 性能对比(实测数据)

指标JIT 版交叉编译版评价
1.5B 吞吐60 tok/s59 tok/s持平(误差 1%)
3B 首次响应0.175s0.098s快 44%!
启动时间~2 分钟秒级起飞
内存占用编译时 8GB 爆满编译时 31GB 宽松安全
离线部署自由

核心发现:交叉编译不仅没损失性能,反而因为编译期参数优化,3B 模型的首次响应快了将近一半!


正文开始:踩坑全记录

坑 ①:import tvm 失败?缺个 pytest!

场景:镜像构建到最后验证层,执行 python -c "import tvm" 直接炸了:

ModuleNotFoundError: No module named 'pytest'

我一查导入链,发现是这样的:tvm → tvm.rpc → tvm.rpc.testing → tvm.testing → tvm_ffi.testing → pytest

原因:新一代 TVM 把 pytest 写进了顶层导入链,你的 venv 里没装 pytest,导入就失败。

解决:Dockerfile 加一行:

RUN uv pip install pytest

教训:上游依赖变化快,看博客抄的命令可能已经过时了,对着源码核对最靠谱。


坑 ②:tvm.support.libinfo() 消失了?

继续构建,看到博客上说用这个命令检查 LLVM 支持情况:

python -c "import tvm; print(tvm.support.libinfo().get('USE_LLVM'))"

结果报错:AttributeError: module 'tvm.support' has no attribute 'libinfo'

查源码:新版 TVM 已经把这个 API 删了!整个 support 模块只剩两个函数。

正确姿势:用功能探测,不读配置字符串:

python -c "import tvm; tvm.get_global_func('target.build.llvm'); print('OK')"

这个函数只在 LLVM 存在时才会注册,存在 = 真正编进去了,比读字符串靠谱。


坑 ③:构建时能用,运行时找不到库?

构建验证全部通过,兴冲冲 docker run 起容器跑 mlc_llm,结果:

OSError: libfpA_intB_gemm.so: cannot open shared object file

定位:构建时我用了 export LD_LIBRARY_PATH=$(find ...),但这个环境变量只在那个 RUN 层里有效!docker run 启动的容器根本继承不到。

解决:固化到 ld 缓存(生成镜像的一部分):

RUN find /opt/mlc-llm /opt/venv -name '*.so' -printf '%h\n' | sort -u \
        > /etc/ld.so.conf.d/mlc-llm.conf \
    && ldconfig

⚠️ 注意:千万别把 CUDA stubs 目录加进来!否则运行时 --gpus 注入的真驱动会被 stub 顶掉,GPU 反而不可用了。


坑 ④:【核心坑】undefined symbol: TVMFFIErrorSetRaisedFromCStrParts

这是最惨的一个坑,花了我半天时间!

PC 上交叉编译完了,scp 到 Jetson 上运行:

Failed to load dynamic shared library ... 
undefined symbol: TVMFFIErrorSetRaisedFromCStrParts

排查三步法(这套思路推荐收藏):

第一步:确认符号确实缺失
nm -D /path/to/libtvm.so | grep TVMFFIErrorSetRaisedFromCStrParts

输出为空 → 符号确实不在,确认 ABI 不兼容。

第二步:排除路径问题
find / -name 'libtvm_ffi*'

输出为空 → 容器里根本没有独立的 tvm-ffi 库,不是路径问题。

第三步:考古实际版本
cd /opt/mlc-llm && git log -1
cd 3rdparty/tvm && git log -1

输出:

  • mlc-llm = d2118b3(2025-05-01,main 分支快照)
  • TVM = 9c894f78(2024-02-22,旧代 FFI,没有 tvm-ffi 子模块

真相大白dustynv/mlc:0.20.0-r36.4.0 这个版本号有误导性!

镜像/源码实际 TVM 版本
dustynv 容器旧代 FFI TVM(9c894f78)
GitHub v0.20.0 tag新代 tvm-ffi TVM(b628d91f)

两者同名不同源,C ABI 完全不兼容!

解决:放弃用 --branch v0.20.0,改为钉 commit:

ARG MLC_COMMIT=d2118b3c9d56da6d1e66dfe2667f650020417010
RUN git clone https://github.com/mlc-ai/mlc-llm /opt/mlc-llm \
    && cd /opt/mlc-llm \
    && git checkout ${MLC_COMMIT} \
    && git submodule update --init --recursive --depth 1

同时调整:

  • Python 3.13 → 3.10(旧代 TVM Cython 不支持 3.13)
  • 删除 tvm-ffi 安装步骤(这个版本根本没有这个子模块)

关键教训版本号对齐 ≠ 二进制 ABI 对齐!可靠手段是两端 git log -1 对 commit hash。


坑 ⑤:Jetson 上装不了官方 wheel

想给 Jetson 换一个与 v0.20.0 tag 同源的运行时来回避坑 ④,结果发现:

curl https://mlc.ai/wheels/ | grep -ci aarch64
# 输出:0

结论:官方 wheel 索引根本没有 aarch64 包!Jetson 上无法用 pip 安装任何官方 MLC 运行时,dustynv 容器是唯一选择 → 只能让编译端迁就运行端(就是坑 ④ 的修复方向)。


坑 ⑥:scp 传输目录姿势不对

上传后 Jetson 上 mlc-chat-config.json 不存在,脚本回退去连 HF(网络不通,卡死)。

原因:我 scp 的是目录内容而不是目录本身:

# 错误姿势(平铺了)
scp dist/Qwen2.5-1.5B-Instruct-q4f16_1-MLC/* zy@jetson:~/work/mlc/dist/

# 正确姿势(保留目录结构)
scp -r dist/Qwen2.5-1.5B-Instruct-q4f16_1-MLC zy@jetson:~/work/mlc/dist/

30 个 params_shard_*.bin 被平铺在 dist/ 下,脚本找不到配置文件。

解决:重新传正确结构;jetson-run.sh 放宽探测为 find ~/work/mlc/dist -name '*sm87.so',平铺也能找到。


坑 ⑦:老源码的依赖四连

钉回 d2118b3 后,切回了 cmake + setup.py 流程(新一代用的是 scikit-build-core),连续踩了四个依赖缺失:

依赖报错原因解决
cmakecmake: not found以前在 pip 隔离环境apt install cmake ninja-build
setuptoolsNo module named 'setuptools'CMake 裸调 setup.pyuv pip install setuptools cython
Python.hfatal error: Python.hdistutils 查找 /usr/include/python3.10apt install python3.10-dev
Rust lintdangerous_implicit_autorefs新版 rustc 标准变了sed 改为显式引用

老一代源码的坑:真正的打包入口在 python/ 子目录,根目录的 pyproject.toml 只是代码风格配置(isort/black)。从根目录 pip install 会得到 UNKNOWN-0.0.0 空包!

关键:依赖安装层要放在编译层之前且带自检(如 ls Python.h),在 25 分钟的 TVM 全量编译前拦住,别浪费重编时间。


坑 ⑧:.so 加载成功,但显存不够!

终于解决了符号问题,.so 能加载了,结果启动报错:

temporary buffer size (10771 MB) exceeds available memory

原因链(源码实锤 config.cc:845-849):

  1. gen_config 没限参数时,Qwen2.5 默认 prefill_chunk_size=8192context_window_size=32768
  2. 预填充函数按 8192 token 一次算 logits(151936 词表),单函数工作区 ~5.3GB
  3. 该工作区在编译时写入 .soModelMetadata(memory_usage),运行时再 ×2 保险 → temp_buffer=10616MB
  4. 运行时改不了:改 mlc-chat-config.json 无效,--overrides 也无效(库模式以 .so 元数据为准)

解决:gen_config 加参数重新编译:

--prefill-chunk-size 2048 --context-window-size 4096 --max-batch-size 1

重编后 temp_buffer 降到 ~2.7GB,8GB 显存能跑下了。

判定方法:启动日志里 temp_buffer = XXXX 一行纹丝不动,说明这是编译期烘焙的值。


标准操作流程(抄作业专用)

PC 端 - 镜像构建与交叉编译

cd scripts/pc-crosscompile

# 环境体检
./setup-pc.sh check

# 全量构建(1-2小时,增量只重跑尾部)
nohup bash -c 'echo y | ./setup-pc.sh build' > build.log 2>&1 &
tail -f build.log
# 成功标志: "TVM LLVM codegen: OK" + "mlc_llm CLI OK"

# 交叉编译(下载→转换→编译,约 20-30min)
./crosscompile.sh
# 产物: dist/Qwen2.5-1.5B-Instruct-q4f16_1-MLC/ + dist/libs/*sm87.so

传输(注意目录结构)

ssh zy@192.168.4.52 'mkdir -p ~/work/mlc/dist'
scp -r dist/Qwen2.5-1.5B-Instruct-q4f16_1-MLC zy@192.168.4.52:~/work/mlc/dist/
scp -r dist/libs zy@192.168.4.52:~/work/mlc/dist/

Jetson 端 - 运行验证

# 快速验证(应打印"使用交叉编译产物(免 JIT)")
~/work/mlc/jetson-run.sh chat

# 启动 OpenAI 兼容服务
~/work/mlc/jetson-run.sh serve

# 切换到 3B 模型(需要压上下文)
MODEL_SIZE=3B MAX_SEQ_LEN=2048 ~/work/mlc/jetson-run.sh serve

# 验证当前加载的模型
curl -s http://192.168.4.52:8000/v1/models

性能实测数据(都给我看清楚了!)

生成性能对比

指标模型JIT 版交叉编译版评价
吞吐1.5B60.05 tok/s59.42 tok/s持平(误差 1%)
3B33.11 tok/s32.26 tok/s持平(误差 2.6%)
TTFT1.5B0.077s0.077s持平(热身后)
3B0.175s0.098s快 44%!
GPU 利用率-96-99% @1013MHz98-99% @1013MHz同为算力瓶颈

资源占用对比

指标模型JIT 版交叉编译版评价
整机 RAM1.5B4.44GB4.52GB+0.08GB(可忽略)
3B5.02GB5.25GB+0.23GB(配置差异)
功耗-20.2-21.4W19-21W持平
GPU 温度-56.8-60°C60-62°C持平

运维对比(这才是重点!)

维度JIT 版交叉编译版改善
首次启动~2 分钟/模型秒级~120x
编译地点Jetson 8GB(OOM 风险)PC 31GB(安全)规避风险
部署复制每台设备各自编译.so + 权重拷贝即用标准化
网络依赖首次需 HF 下载全离线自由
3B 适配运行时 overrides 压参编译期定型一致性好

总结与建议

✅ 推荐使用交叉编译的场景

  • 需要在多台 Jetson 设备上部署同一模型
  • 网络受限环境(离线部署、内网环境)
  • 启动时间敏感(秒级启动)
  • 8GB 设备跑 3B 模型(规避 JIT OOM)

⚠️ JIT 方案更合适的场景

  • 快速原型验证(单设备、模型频繁切换)
  • 不稳定的开发环境(无需维护编译产物)

🎯 关键方法论(可复用到其他项目)

1. 版本对齐要以二进制为准

版本号 ≠ ABI,可靠手段:

  • 两端 git log -1 对 commit hash
  • .so 加载失败先 nm -D 查符号归属
2. fail-fast 验证必须覆盖真实运行路径

构建层内的临时环境变量(LD_LIBRARY_PATH)不会进镜像运行时!验证判据应该是"容器 run 起来能跑",而不只是"构建层内能跑"。

3. 诊断三件套(收藏!)
# 符号在不在,定 ABI 归属
nm -D <lib>.so | grep <symbol>

# 实际版本考古
git log -1
pip show <package>

# 库文件实际在哪
find <dir> -name '*.so'
4. 修复要放进正确的层
  • 加依赖放独立 RUN 层(不破坏编译缓存)
  • 运行时路径问题固化进镜像(ld.so.conf.d),不要指望运行命令里补环境变量
5. 上游 API 过时很快

网上教程的验证命令对着钉住的 commit 源码核一遍再用;用「功能探测」(get_global_func)替代「配置读取」更耐版本变化。

6. 回退链要有日志

脚本「找不到 A 就去 B」的回退必须打印明确提示,否则上游路径问题会被掩盖成下游网络问题。


后续优化方向

  1. EAGLE 投机解码:可进一步提速 1.5-1.8x(需额外 136MB 显存)
  2. 量化探索--quantize-embedding 可节省 0.3-0.5GB,需评估精度损失
  3. 批处理优化:当前 max_batch_size=1,多并发场景可考虑调大

产物清单

~/work/mlc/dist/  (Jetson Orin)
├── Qwen2.5-1.5B-Instruct-q4f16_1-MLC/     # 1.2GB 权重
├── Qwen2.5-3B-Instruct-q4f16_1-MLC/       # 1.7GB 权重
└── libs/
    ├── Qwen2.5-1.5B-...-sm87.so           # 11MB 交叉编译引擎
    └── Qwen2.5-3B-...-sm87.so             # 16MB 交叉编译引擎

写在最后

两天踩 8 个坑,虽然过程曲折,但结果很香:

性能没损失,3B 模型反而快了 44%
启动秒开,再也不用等 2 分钟编译
离线部署,拷贝就能跑
规避 OOM,编译在 PC 上压力小太多

如果你也在折腾 Jetson 部署,希望这篇记录能帮你少走弯路!
完整代码地址https://github.com/X32/tensorRT_ONNX


相关资源

  • MLC-LLM 项目: https://github.com/mlc-ai/mlc-llm
  • TVM 项目: https://github.com/apache/tvm
  • dusty-nv Jetson 容器: https://github.com/dusty-nv/jetson-containers
  • Qwen2.5 模型: Qwen Team @ Alibaba Cloud

本文原创,转载请注明出处。
如有问题欢迎留言讨论,看到必回!

点赞 👍 收藏关注 🐻,下期更新 EAGLE 投机解码实战!


本文基于真实实验记录整理,数据来源:DOC/log/run_step_20260814_19_25.logDOC/log/pc_compress_20260815_11_5.logbenchmark-result.txt

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值