场景:Jetson Orin 8GB 部署 Qwen2.5-1.5B/3B 大模型
前言:为什么我要折腾交叉编译?
最近在搞边缘设备上的大模型部署,手头有一块 Jetson Orin 8GB 开发板。本来用 MLC-LLM 的 JIT(Just-In-Time)编译方案跑起来了,但每次启动都要在设备上编译 2 分钟,还得挂 swap 防 OOM,这谁受得了?
而且网络不稳定的时候,第一次启动直接卡死在下载 HuggingFace 权重上…
痛点有三:
- ❌ 启动太慢(2 分钟编译)
- ❌ 容易 OOM(8GB 内存编译不够用)
- ❌ 离线不了(每次都要联网下权重)
于是我想:能不能在 PC 上把模型编译好,直接拷贝到 Jetson 上跑?
答案是:能! 但过程踩了 8 个坑,花了我两天时间…
先说结论(不看过程的可以直接跳最后)
✅ 性能对比(实测数据)
| 指标 | JIT 版 | 交叉编译版 | 评价 |
|---|---|---|---|
| 1.5B 吞吐 | 60 tok/s | 59 tok/s | 持平(误差 1%) |
| 3B 首次响应 | 0.175s | 0.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),连续踩了四个依赖缺失:
| 依赖 | 报错 | 原因 | 解决 |
|---|---|---|---|
| cmake | cmake: not found | 以前在 pip 隔离环境 | apt install cmake ninja-build |
| setuptools | No module named 'setuptools' | CMake 裸调 setup.py | uv pip install setuptools cython |
| Python.h | fatal error: Python.h | distutils 查找 /usr/include/python3.10 | apt install python3.10-dev |
| Rust lint | dangerous_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):
gen_config没限参数时,Qwen2.5 默认prefill_chunk_size=8192、context_window_size=32768- 预填充函数按 8192 token 一次算 logits(151936 词表),单函数工作区 ~5.3GB
- 该工作区在编译时写入
.so的ModelMetadata(memory_usage),运行时再 ×2 保险 → temp_buffer=10616MB - 运行时改不了:改
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.5B | 60.05 tok/s | 59.42 tok/s | 持平(误差 1%) |
| 3B | 33.11 tok/s | 32.26 tok/s | 持平(误差 2.6%) | |
| TTFT | 1.5B | 0.077s | 0.077s | 持平(热身后) |
| 3B | 0.175s | 0.098s | 快 44%! | |
| GPU 利用率 | - | 96-99% @1013MHz | 98-99% @1013MHz | 同为算力瓶颈 |
资源占用对比
| 指标 | 模型 | JIT 版 | 交叉编译版 | 评价 |
|---|---|---|---|---|
| 整机 RAM | 1.5B | 4.44GB | 4.52GB | +0.08GB(可忽略) |
| 3B | 5.02GB | 5.25GB | +0.23GB(配置差异) | |
| 功耗 | - | 20.2-21.4W | 19-21W | 持平 |
| GPU 温度 | - | 56.8-60°C | 60-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」的回退必须打印明确提示,否则上游路径问题会被掩盖成下游网络问题。
后续优化方向
- EAGLE 投机解码:可进一步提速 1.5-1.8x(需额外 136MB 显存)
- 量化探索:
--quantize-embedding可节省 0.3-0.5GB,需评估精度损失 - 批处理优化:当前
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.log、DOC/log/pc_compress_20260815_11_5.log及benchmark-result.txt。

1135

被折叠的 条评论
为什么被折叠?



