AI 纪元 追踪人工智能最新进展,深度解读前沿技术

Rust 接管端侧调度,先看清 TensorRT-LLM 的边界

端侧 LLM 会为几毫秒反复抠性能,但瓶颈未必来自 Python。真正该查的是调用次数、同步、内存搬运和硬件后端。Rust 适合收紧关键路径;TensorRT-LLM 只能用于其支持的平台,不能被泛化成手机 NPU 的统一接口。

0 次阅读 约 2620 字 5 分钟 AI 生成
Macro photography of a precision mechanical gear assembly with a blurred background, symbolizing optimized system performance.
Macro photography of a precision mechanical gear assembly with a blurred background, symbolizing optimized system performance.

端侧推理里,几毫秒不是尾数。《端侧跑 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,并绑定目标硬件实际支持的后端。

目标很简单:让每次调用、拷贝和同步都能解释,也能测量。

相关阅读