Skip to main content

为什么 SGLang 用 Rust 做 VL 模型前处理,而不是继续依赖 Python?

在分析 sgl-model-gateway 的多模态代码后,可以比较明确地看出:SGLang 在视觉模型(VL/VLM)前处理上选择 Rust,并不是单纯为了“追求更快”,而是一次围绕 架构边界、部署复杂度、性能稳定性、结果一致性 做出的工程权衡。

这篇文章总结了我们的讨论和结论,重点回答几个问题:

  • 为什么 SGLang 要把 resizenormalizecrop 等前处理放到 Rust 里?
  • Rust 做这些事情真的不会比 Python 慢吗?
  • image crate 是不是纯 CPU?
  • 为什么不进一步用 GPU 或 VPU 来加速图像处理?
  • 这种设计在什么场景下成立,什么场景下又可能需要重新评估?

一、先说结论

核心结论

SGLang 在 sgl-model-gateway 中使用 Rust 实现视觉前处理,核心目标不是“把前处理做到极致性能”,而是:

  • 让网关层成为一个独立的、无 Python 依赖的高性能服务
  • 在 Rust 进程内完成图片获取、解码、预处理和多模态请求组装
  • 保证预处理结果与 HuggingFace processor 数值一致
  • 减少跨语言/跨进程开销,提高部署一致性和系统可维护性

性能结论

从工程实践上看,这样做通常不会更慢,很多情况下反而更合适。原因不是 Rust 的 resize 一定比 Python 快很多,而是:

  • Python 前处理底层本来也常常依赖 C/C++ 库,不是纯 Python 在算
  • 这些前处理操作本身计算量不大,通常不是 VLM serving 的主要瓶颈
  • 在服务架构中,I/O、图片解码、网络传输、GPU 推理 往往比 normalize/resize 更耗时
  • 如果为了这部分轻量前处理引入 Python 依赖、GPU 路径或额外硬件栈,收益通常不如代价大

二、SGLang 的代码里到底是怎么设计的?

sgl-model-gateway/src/multimodal/vision/mod.rs:1 的模块说明可以直接看出设计意图:

  • 这是一个 Pure Rust vision processing module
  • 目标是 match HuggingFace processor outputs
  • 同时 without requiring Python dependencies

也就是说,这一层不是在做“实验性替代”,而是在明确建设一个 不依赖 Python 的视觉预处理子系统

代码结构

多模态视觉前处理主要位于:

  • sgl-model-gateway/src/multimodal/vision/transforms.rs
  • sgl-model-gateway/src/multimodal/vision/preprocessor_config.rs
  • sgl-model-gateway/src/multimodal/vision/image_processor.rs
  • sgl-model-gateway/src/multimodal/vision/processors/*.rs

它的分层很清晰:

  • transforms.rs:基础变换,如 resizenormalizecenter_crop
  • preprocessor_config.rs:解析 HuggingFace 的 preprocessor_config.json
  • image_processor.rs:抽象统一处理接口和输出结构
  • processors/*.rs:不同模型族的具体处理逻辑,如 LLaVA、Qwen-VL、Phi3-Vision、LLaMA4 Vision 等

支持的模型族

这个 Rust 视觉前处理并不是只覆盖一个模型,而是已经支持了多类主流视觉模型,包括:

  • LLaVA
  • LLaVA-NeXT
  • Qwen2-VL / Qwen2.5-VL / Qwen3-VL
  • Phi3-Vision
  • Phi4-Vision
  • LLaMA4 Vision
  • Pixtral / Mistral3

这说明它不是一个 demo,而是服务于实际多模型网关能力。


三、为什么要用 Rust,而不是继续走 Python 处理链?

1. 网关层和推理层职责分离

SGLang 的整体思路不是“所有事情都在 Python 里做”,而是让 Rust 的 model-gateway 承担服务网关职责。

在这个结构里,图片请求进入系统后,大致会经历:

  1. Rust 网关接收 OpenAI 风格的多模态请求
  2. Rust 网关下载图片 / 解码图片
  3. Rust 网关完成 resize、normalize、crop、tile 等预处理
  4. Rust 网关把处理后的多模态输入通过 gRPC 发给后端调度/推理系统
  5. Python 侧更多负责调度器和模型执行

这意味着:图像前处理天然属于“网关层”的一部分,不是必须依附在 Python 模型执行逻辑上的。

如果这一层仍然依赖 Python,会带来几个问题:

  • Rust 网关需要嵌入 Python 运行时,或者额外调用 Python 服务
  • 部署链条变复杂
  • 网关进程的依赖体积和故障面扩大
  • 多语言边界更多,性能和排障更复杂

从工程视角,这些代价通常大于“直接复用 Python processor”带来的便利。

2. 部署和依赖管理更简单

如果视觉前处理留在 Python:

  • 需要 Python 环境
  • 需要安装 transformers / PIL / torchvision 等依赖
  • 需要处理版本兼容
  • 需要保证各模型 processor 行为和配置版本一致

而纯 Rust 网关的价值在于:

  • 一个统一二进制或统一 Rust 服务
  • 更简单的部署方式
  • 更可控的依赖面
  • 更少的环境漂移问题

对于基础设施层组件,这是非常现实的收益。

3. Rust 更适合高并发服务边界

前处理不是只有数学变换,还包括:

  • 远程图片拉取
  • 数据 URL / base64 解析
  • 解码
  • 错误处理
  • 并发请求隔离
  • 安全限制(域名、路径、超时)

这些都属于高并发 I/O 服务问题,而 Rust 在这方面非常适合。
因此,把多模态输入接入、追踪、预处理都放在 Rust 网关内部,是一个很顺的架构选择。


四、Rust 做前处理会不会比 Python 慢?

短答案

通常不会,至少不会慢到成为问题;很多情况下整体系统反而更优。

关键原因

1. Python 的图像处理也不是“纯 Python 算”

在 Python 生态里,图片前处理一般用的是:

  • PIL / Pillow
  • torchvision
  • OpenCV
  • NumPy

这些库很多底层也是 C/C++ 实现。
所以真正比较时,并不是“Rust 原生”对“Python 脚本解释执行”,而是:

  • Rust image crate / ndarray
  • 对比 Python 封装的底层原生库

在这种情况下,Rust 不会天然吃亏。

2. 前处理通常不是 VLM 服务的主瓶颈

以一张图进入 VLM 服务为例,耗时更可能集中在:

  • 网络拉图
  • JPEG/PNG 解码
  • 模型推理(GPU)
  • token 生成与调度

resizenormalizecrop 这种操作,本质上是轻量 CPU 计算。
在单图或少图请求场景里,它们通常不是总延迟的大头。

3. 少了跨语言和跨进程开销

如果 Rust 网关自己就能:

  • 拉图
  • 解码
  • 转 tensor
  • 标准化
  • 计算 image token 数
  • 组装 multimodal inputs

那就避免了:

  • 把原始图像传给 Python
  • Python 再做一遍 processor
  • 再把结果传回来或继续下游转发

对于一个请求入口层来说,减少边界切换本身就是性能收益


五、SGLang 是怎么保证结果正确的?

这是 Rust 重写视觉前处理时最关键的问题:
不是“差不多就行”,而是要尽量和 HuggingFace 一致。

SGLang 在这方面做得比较扎实。

1. 配置层直接兼容 HuggingFace

sgl-model-gateway/src/multimodal/vision/preprocessor_config.rs 负责解析 HuggingFace 的 preprocessor_config.json

它支持很多常见格式差异,比如:

  • size 可能是 {"height": H, "width": W}
  • 也可能是 {"shortest_edge": S}
  • patch_size 既可能是整数,也可能是带高宽的结构

这意味着它不是自己拍脑袋定义一套 processor 配置,而是直接跟 HuggingFace 配置体系对齐。

2. 变换实现尽量贴近 PyTorch / PIL 语义

比如在 transforms.rs 中,不只是简单调用一个 resize,还做了不少兼容工作:

  • 映射 PIL resampling 到 Rust 图像库 filter
  • 自己实现与 PyTorch 行为对齐的 bicubic resize
  • 做标准的 mean/std normalize
  • 支持 pad、center crop、square expand 等不同模型策略

这说明作者不是只关心“能跑”,而是关心结果一致性

3. 有 golden tests 做数值对比

更重要的是,SGLang 不是停留在“理论对齐”,而是直接做了 golden tests:

  • Python/HuggingFace processor 先生成 .npz 参考结果
  • Rust 测试加载这些黄金结果
  • 比较像素值张量是否一致或在容忍误差范围内

这类测试是非常有价值的。
因为图像前处理里最容易出问题的不是代码写不出来,而是:

  • 坐标变换差 0.5
  • 插值核不同
  • 通道顺序不一致
  • 归一化先后顺序不同
  • 数据类型转换细节不一样

golden tests 能直接挡住这些“看起来差不多,实际结果偏掉”的坑。


六、image crate 是 CPU 处理吗?

是的,Rust 的 image crate` 是 CPU 处理

在当前这套实现里:

  • 图像解码:CPU
  • resize / crop / pad:CPU
  • normalize / tensor conversion:CPU
  • ndarray 张量操作:CPU

也就是说,SGLang 当前的这条视觉前处理链是标准的 CPU 预处理管线


七、为什么不使用 GPU 来做图像前处理?

这个问题看似直觉上很合理:
既然模型推理都在 GPU 上,为什么不顺便把图片 resize、normalize 也交给 GPU?

答案是:在大多数网关型 VLM serving 场景里,这通常不划算。

1. 前处理太轻,不值得为它单独上 GPU

GPU 擅长的是:

  • 大规模矩阵计算
  • 大 batch 并行张量操作
  • 高吞吐重复计算

而图像前处理里的 resizenormalize 对单张或少量图片来说,量并不大。
如果为了这点工作:

  • 把图片从 CPU 内存搬到 GPU
  • 启动 GPU kernel
  • 处理后再衔接后续流程

数据搬运和调度开销可能就把收益抵消掉了。

2. GPU 资源应该优先留给模型推理

在 LLM/VLM serving 场景里,GPU 显存和算力是最稀缺资源。
真正贵的是:

  • 模型权重
  • KV Cache
  • attention / decode 计算

如果让网关层也去占 GPU 资源做轻量前处理,从资源分配角度通常不划算。

3. 网关节点未必和推理节点同机

SGLang 当前的架构更接近:

  • Rust 网关负责接入和前处理
  • 调度/推理服务负责 GPU 模型执行

这两者可能根本不在一台机器上。
如果网关层为了前处理也要求 GPU,就会:

  • 抬高部署门槛
  • 限制拓扑灵活性
  • 让本可跑在 CPU 机器上的网关变成 GPU 依赖服务

这和网关层“轻依赖、通用部署”的目标是冲突的。


八、为什么不考虑 VPU 或其他专用硬件?

这个问题比 GPU 更“工程化”,但也更有现实意义。

VPU 的问题不在“能不能”,而在“值不值”

在某些平台上,确实存在:

  • 视频解码加速单元
  • 图像缩放硬件
  • 专用媒体处理引擎
  • NPU/VPU 加速路径

但在 SGLang 这种通用多模型网关里,引入这些东西通常有几个现实问题:

1. 可移植性大幅下降

Rust 当前方案的一个优点是:

  • 跨平台
  • 依赖相对简单
  • 不绑定特定厂商硬件 SDK

一旦引入 VPU/NVJPEG/特定媒体库,你就可能被绑定到:

  • 特定 OS
  • 特定驱动
  • 特定厂商
  • 特定编译链

对基础设施组件来说,这会明显增加维护成本。

2. 适用场景往往不是“单图请求式 VLM 服务”

VPU 类硬件更擅长的是:

  • 视频编解码
  • 连续帧流水线处理
  • 固定格式媒体管线

而 VLM serving 更常见的是:

  • 零散图片请求
  • 格式多样
  • 多模型差异化 processor
  • 通用 HTTP 服务场景

两者并不完全匹配。

3. 真正收益未必大

即使硬件路径理论上更快,也要看系统瓶颈是不是在这里。
如果整体耗时主要在:

  • 图片下载
  • 解码
  • 模型推理
  • 网络调度

那把 resize 再优化 2ms,并不会带来显著业务收益。


九、那这种观点是不是永远成立?

不能绝对化。
更准确的说法是:

对 SGLang 当前的网关场景,这种 Rust + CPU 的选择非常合理;但放到所有视觉处理场景里,不应一概而论。

什么时候 GPU/VPU 可能值得做?

以下场景就可能需要重新评估:

  • 超高吞吐批量图片服务
  • 视频流逐帧处理
  • 预处理和推理严格同机同卡
  • 预处理结果可以直接留在显存,不回 CPU
  • JPEG decode 才是真正 CPU 热点,需要硬件解码链路
  • 业务强绑定某类硬件平台,且追求极限吞吐

在这些场景里,GPU/VPU/专用 decode pipeline 就可能不是“过度设计”,而是合理优化。

也就是说:

  • 通用网关层,CPU Rust 是高性价比方案
  • 特定高吞吐媒体流水线,硬件加速可能更值得

十、我认同这个设计吗?

我的判断是:

我认同的部分

  • sgl-model-gateway 这种 Rust 网关组件来说,用 Rust 实现视觉前处理是正确方向
  • 不引入 Python 依赖,符合网关层职责边界
  • 在当前服务架构里,前处理放 CPU 是务实选择
  • 保证与 HuggingFace processor 数值一致,比“盲目追求硬件加速”更重要

我保留的部分

如果有人把这个结论扩大为:

“VL 前处理永远不值得 GPU/VPU 加速”

那我不完全认同。
这不适用于所有系统,只适用于当前这类 请求级、多模型、通用部署型 网关场景。


十一、最有价值的工程启示

从这次分析里,我觉得最值得借鉴的不是“Rust 比 Python 快”,而是下面这几点:

1. 不要只盯着单点性能,要看系统边界

SGLang 不是在单独优化一个 resize(),而是在优化:

  • 网关是否独立
  • 部署是否简化
  • 依赖是否可控
  • 多模态请求是否统一处理

这比单个算子的 micro-benchmark 更重要。

2. 一致性在基础设施里很重要

视觉前处理如果和 HuggingFace 行为不一致,会直接影响模型输出。
SGLang 用 golden tests 去对齐,这比“写一个差不多能跑的版本”有价值很多。

3. 硬件加速不是默认答案

不是所有 CPU 工作都值得迁移到 GPU/VPU。
是否值得加速,取决于:

  • 它是不是瓶颈
  • 数据搬运成本
  • 资源是否稀缺
  • 架构是否允许
  • 维护代价是否值得

十二、最终总结

SGLang 使用 Rust 来实现 VL 模型前处理,本质上是一个非常典型的基础设施工程决策:

  • 目标不是只追求局部算子更快
  • 而是把网关层做成一个稳定、独立、可部署、可并发、无 Python 依赖的系统组件
  • 在这个目标下,CPU 上的 Rust 图像处理是一个很自然、也很有性价比的方案

至于为什么不进一步用 GPU/VPU:

  • 因为当前前处理不是主要瓶颈
  • 因为 GPU 资源应优先留给推理
  • 因为网关和推理通常可分离部署
  • 因为硬件加速会显著增加复杂度和依赖面
  • 因为对一个通用多模型网关来说,这样做大多数时候不划算

所以更准确的结论应该是:

SGLang 选择 Rust + CPU 做视觉前处理,不是因为不知道 GPU/VPU 可以加速,而是因为在它当前的架构目标下,这不是最值得优化的方向。

如果你愿意,我还可以把这篇内容进一步整理成更正式的技术博客版本,比如补一张“Rust 网关多模态请求流转图”和一张“CPU 前处理 vs GPU/VPU 前处理”的对比图示。