端侧推理里,几毫秒不是尾数。《端侧跑 LLM 的核心问题瓶颈》说得很直白:通用平台相对芯片原生能力,往往会多出“几 ms”的损耗。
可这笔账不能直接记在 Python 名下。一次推理慢,可能卡在主机调度、设备同步、内存搬运,也可能是计算图被切得太碎。
几毫秒先记到调用链上
torch.inference_mode() 关闭的是梯度记录,不会自动消除主机与设备之间的同步。GIL 会约束 Python 字节码线程,但不少原生运行时会在执行计算时释放 GIL。只看到 Python 外层有一个 for 循环,还不足以判断瓶颈在哪里。
关键要看每个 Token 经过了什么:
- 一轮解码要从 Python 进入运行时多少次;
- 是否频繁创建 Tensor、转换 dtype 或调整 layout;
.item()、日志读取等操作是否触发设备同步;- 输入输出缓冲区有没有重复分配和拷贝;
- NPU 空闲时,主机线程究竟在等锁、组装参数,还是等待驱动返回。
如果每个 Token 只调用一次完整的编译图,Python 可能只是入口,换成 Rust 未必有明显收益。若一轮解码被拆成许多细碎调用,语言和 FFI 的成本才会被放大。
对齐值别直接写死
Rust 的优势之一,是能明确管理缓冲区的布局和生命周期。但把对齐值统一写成 128 字节并不稳妥。不同设备、驱动和运行时可能有各自要求,对齐参数应来自硬件文档或运行时接口。
下面这个结构只负责按指定值分配内存:
use std::alloc::{alloc, dealloc, Layout};
use std::ptr::NonNull;
struct AlignedBuffer {
ptr: NonNull<u8>,
layout: Layout,
}
impl AlignedBuffer {
fn new(size: usize, align: usize) -> Self {
let layout = Layout::from_size_align(size, align)
.expect("invalid size or alignment");
let ptr = NonNull::new(unsafe { alloc(layout) })
.expect("allocation failed");
Self { ptr, layout }
}
fn as_mut_ptr(&mut self) -> *mut u8 {
self.ptr.as_ptr()
}
}
impl Drop for AlignedBuffer {
fn drop(&mut self) {
unsafe { dealloc(self.ptr.as_ptr(), self.layout) }
}
}
对齐只是第一步。若设备异步读取缓冲区,AlignedBuffer 必须存活到任务真正结束;后端不接受这块内存时,仍然会发生拷贝。Rust 能把所有权写清楚,却不会自动带来零拷贝。
TensorRT-LLM 有自己的硬件边界
Rust 是主机侧的控制语言,TensorRT-LLM 则有明确的硬件和运行时支持范围。通过 FFI 调用原生接口,可以省掉 Python 入口,却不能凭空增加一个 NPU 后端。
在 TensorRT-LLM 已支持的 NVIDIA 平台上,Rust 可以负责引擎生命周期、请求队列和缓冲区管理。面对骁龙 Hexagon NPU 或其他移动 NPU,第一步应是核对厂商运行时及其原生接口。即便宿主处理器采用 ARM 架构,也不代表 TensorRT-LLM 能直接驱动手机里的 NPU。
因此,更准确的方案是“Rust 接管调度层,并绑定目标硬件实际支持的运行时”。底层恰好支持 TensorRT-LLM 时再使用它,不要把两者固定绑成一套通用方案。
Rust 先接最热的那一段
《技术大神用纯 Rust 从零撸 GPT 项目》提到,相关项目采用了接近 PyTorch 的 API,并覆盖 CPU、CUDA、WASM 后端。这说明 Rust 已经可以承载更完整的模型代码,也说明后端能力需要逐个平台实现,不能从语言本身推导出来。
迁移时没必要把整套工具链推倒重来。更稳妥的切法是:
- Python 保留模型实验、转换和结果校验;
- Rust 负责常驻进程、请求队列、缓冲区池和关键解码循环;
- 算子执行交给目标设备真正支持的运行时。
先改最热的循环。只有 profile 证明收益稳定,再继续扩大 Rust 的范围。
测试时只改一个变量
若 Python 方案使用 PyTorch/XNNPACK,另一组使用 ONNX Runtime/OpenVINO,Rust 方案又换成 TensorRT-LLM,那么语言、后端、算子融合和内存实现同时发生了变化。最终差异不能全部归因于 Rust。
有效对比应固定模型、量化方式、计算图、硬件状态和推理后端,只替换主机侧调度。冷启动、首 Token、稳定生成阶段也要分开记录,同时给出预热方式、重复测试记录和采样工具。
“首 Token 降低 27%”“NPU 利用率达到 88%”“功耗下降 15%”这类数字,只有在设备状态、统计口径和原始日志齐全时才有意义。材料不足就不该用它们撑结论,NPU 利用率尤其要注明来自哪个性能计数器。
36Kr 对 Astral 创始人 Charlie Marsh 的报道用“AI 优化快 10 倍,但手写能快 100 倍”作标题。Charlie Marsh 所在团队做出了 Ruff、uv,这句话可以当作性能工程的提醒,却不能直接当成端侧推理的通用比例。
真正值得动 Rust 的信号很具体:解码循环反复返回 Python、缓冲区持续重建、设备同步清晰可见,或者多模型调度让主机线程长期忙碌。找不到这些现象,就先保留 Python;找到了,再把对应热路径交给 Rust,并绑定目标硬件实际支持的后端。
目标很简单:让每次调用、拷贝和同步都能解释,也能测量。