为什么生产推理我默认 vLLM:从 NVFP4、Kernel 到 CUDA Graph

从 Qwen3.8-Flash-Next 的 CPU Offload 到 27B 的全 GPU Serving,解释为什么生产推理更看重 NVFP4 Kernel、Compile、CUDA Graph 与调度执行链。

📚 双 RTX 5060 Ti 部署 Qwen3.8 系列 · 4/6

💡 导读

llama.cpp 和 vLLM 并不是简单的“谁更快”。前者把个人硬件能触碰到的模型规模往上推,后者则更适合在模型可以完整驻留 GPU 后继续追求 Serving 效率。这篇从 Qwen3.8-Flash-Next 的 CPU Offload 讲到 NVFP4、Kernel、Compile 和 CUDA Graph,也解释为什么我的 27B 生产部署默认从 vLLM 开始。

前面三篇已经解决了三个问题:

27B 怎么装进双 16GB
两张 GPU 到底怎么并行
TP2 为什么能加速、又为什么达不到理想 2×

到了真正部署服务时,我现在有一个比较明确的默认选择:

只要是 NVIDIA GPU、模型能够合理地完全驻留 GPU,并且目标是长期提供推理服务,我会优先从 vLLM 开始。

原因并不只是 PagedAttention,也不只是 KV Cache。

真正让我倾向 vLLM 的,是它把整个 GPU 推理执行链放在同一个系统里优化:

NVFP4 / FP8 等低精度格式
Optimized GEMM / Attention Kernel
torch.compile / Inductor
CUDA Graph
TP / PP
Scheduler / Continuous Batching
Cache / Prefix Cache

这些东西单独拿出来都只是一个优化点。

组合起来以后,才是一套真正面向 GPU Serving 的推理引擎。

不过在谈 vLLM 之前,先要把一个经常被混在一起的问题说清楚:

“模型能不能跑”和“这个模型适不适合做高性能 GPU Serving”,不是同一个问题。

Qwen3.8-Flash-Next 恰好是一个非常好的例子。


1. Qwen3.8-Flash-Next:CPU Offload 为什么依然有价值

写这组文章的时候,Qwen3.8-27B 才开放权重大约两周。

然后 2026 年 8 月 26 日,Qwen 又开放了 Qwen3.8-Flash-Next。

它的体量已经完全不是 27B 这一级别:

Language Model:125B 参数,约 6B activated
+
51B n-gram embedding
+
4B MTP

总体参数规模大约到了 180B 量级。

直接看权重文件更直观:

  • 官方 BF16:约 360GB
  • 官方 FP8:约 186GB
  • Unsloth UD-Q4_K_XL GGUF:约 111GB

这时“显存不够”已经不是双 16GB 能不能稍微规划一下的问题了。

甚至假设有:

8 × RTX 3090 24GB = 192GB VRAM

这种对个人用户已经非常夸张的配置,仅从总量上看,官方 FP8 的约 186GB 权重就已经快把聚合显存吃满了。

而真正运行模型显然还要给:

  • Runtime
  • Cache
  • Activation
  • Workspace
  • CUDA Graph
  • 通信 Buffer

留下空间。

更不用说 8 张 3090 的拓扑、供电、散热和整机成本,本身就已经脱离绝大多数个人开发者的现实环境。

我当然也不会为了试一个刚发布的模型,先准备一台 8×3090 的机器。

这时候 llama.cpp 的另一种价值就非常明显了:

它可以把“GPU 显存不够”这个硬边界,扩展成“GPU VRAM + System RAM 能不能一起把模型跑起来”。

例如一个 100GB 级 GGUF,可以让一部分权重留在 GPU,一部分权重放在系统内存,通过 CPU + GPU 的混合路径运行。

原本的问题是:

模型 > 总 VRAM → 根本放不下

现在变成:

GPU VRAM
+
System RAM
→ 有机会真正运行起来

对于:

  • 刚发布的超大模型
  • 能力测试
  • 本地研究
  • 个人消费级硬件
  • 显存容量不规则的机器

这种能力非常有价值。

所以我并不认同一种简单的判断:

“CPU Offload 很慢,所以没有意义。”

它当然有意义。

它改变的是:

“我能够接触和测试多大的模型”。

但另一方面,也不能把 RAM 当成“更便宜的显存”。

如果 Forward 需要频繁从系统内存取热点权重,那么整个性能模型就会受到:

CPU 内存带宽
+
CPU-GPU 数据路径
+
PCIe

限制。

GPU 本地 GDDR 能提供数百 GB/s 级带宽,而系统 RAM 与 CPU↔GPU 的 PCIe 路径在有效带宽、延迟和访问方式上都明显不同。

Offload 越重,Decode 越不可能维持纯 GPU Resident 模型的性能特征。

所以我现在更愿意这么定义它:

CPU / RAM Offload 很擅长扩大“能跑的模型范围”,但通常不是扩大“高性能生产 Serving 范围”的办法。

这也正好解释了 llama.cpp 和 vLLM 为什么不是简单的“谁替代谁”。


2. 对 27B 来说,我反而会优先追求 GPU Resident

Flash-Next 这种百 GB 级模型,RAM Offload 很合理。

但回到前面一直测试的 Qwen3.8-27B,情况完全不同。

它不是“无论如何都放不下”。

通过合适的量化,双 16GB 本来就有机会让权重完整驻留 GPU。

这时候我的优化顺序会变成:

选择质量可以接受的量化
尽量让权重完整 GPU Resident
给 Cache / Context 留空间
选择 TP / PP
再优化 Kernel、Compile、CUDA Graph 和调度

这也是为什么前面最终更倾向 NVFP4。

FP8 约 31GB:

32GB 总 VRAM
-
约 31GB 权重
几乎没有运行余量

NVFP4 约 23GB:

32GB 总 VRAM
-
约 23GB 权重
还有约 9GB 名义空间

第一篇主要解释的是 NVFP4 的容量价值。

但到了 vLLM 这一篇,NVFP4 还有另一半更重要的意义:

它不只是“存储格式”,还可以直接参与低精度计算。


3. NVFP4:容量只是第一层价值

如果一个 4bit 量化只解决存储,那么执行路径可能是:

4bit Weight
读取 / 解码
反量化到更高精度
再执行通用 GEMM

这依然能节省:

  • 权重显存
  • Weight Traffic

但真正更有意思的是硬件原生低精度路径:

NVFP4 Weight
+
低精度 Activation / Scale
FP4 适配的 Tensor Core / MMA 路径
NVFP4 GEMM

这时量化同时改变了两件事:

Memory Traffic ↓ + 低精度计算吞吐 ↑

NVIDIA 的 NVFP4 本身就是为 Blackwell 低精度计算设计的格式,而不是单纯“把 FP16 截成 4bit”。

所以:

NVFP4 在 Blackwell 上的价值,不能只用“省了多少 GB 显存”解释。

如果框架真的走到了合适的 FP4 Kernel,它还会改变 GEMM 本身的执行路径。


RTX 5060 Ti 属于 Blackwell,这件事对 NVFP4 很重要

RTX 5060 Ti 属于 Blackwell 消费级 GPU,CUDA Compute Capability 为 12.0,也就是常说的 SM120。

这意味着这张卡并不是“只能保存 NVFP4,然后全部转回旧精度算”。

在合适的软件栈下,它有对应的 Blackwell 低精度计算能力可以利用。

于是可以形成一条完整链路:

RTX 5060 Ti / SM120
NVFP4 Checkpoint
NVFP4-aware Kernel
Blackwell 低精度 Tensor Core 路径

但这里有一个很重要的前提:

模型文件写着 NVFP4,不代表执行时一定走到了最优 NVFP4 Kernel。

这也是我开始真正关心 vLLM Kernel Dispatch 的原因。


“支持 NVFP4”和“NVFP4 跑得快”是两回事

假设两个框架都能成功加载:

Qwen3.8-27B NVFP4

这只能证明格式兼容。

真正执行时,可能存在完全不同的路径。

例如:

路径 A
NVFP4
额外转换 / 反量化
较通用的 GEMM

和:

路径 B
NVFP4
专用 NVFP4 GEMM
适合当前 Blackwell GPU 的 Kernel

模型格式一样,最终性能完全可能不同。

vLLM 当前的 NVFP4 执行路径会根据平台与可用组件选择对应 GEMM backend,例如 FlashInfer / CUTLASS 一类实现,并在不同硬件能力下选择合适的 fallback。

所以现在看量化模型,我会同时问两个问题:

这个框架能不能加载它?

以及:

加载以后,Linear 最终 Dispatch 到哪个 Kernel?

第二个问题对性能往往更重要。


4. 为什么 Kernel 对单流 Decode 特别重要?

一颗 token 的生成不是“一次巨大的 GPU 运算”。

更像是几十层不断重复:

Attention
Linear
Norm
Linear
Activation
Linear
下一层

前面 TP2 的单流实测大约是:

33.5 token/s

也就是一颗 token:

≈ 30ms

在这个时间尺度里,每层多一点:

  • 量化转换
  • 不合适的 GEMM Shape
  • Memory Access
  • Kernel Launch
  • 同步

乘上几十层以后,就会变成很明显的最终差距。

所以现代 LLM 推理框架真正优化的不是某一个“神奇开关”。

而是:

大量重复的小执行路径能不能全部落到更合适的 Kernel 上。

这也是为什么“模型能加载”离“模型跑得好”还差很远。


5. CUDA Graph:Kernel 快了以后,还要减少 Launch

即使每个 CUDA Kernel 本身已经很快,单 token Decode 还有另一个问题。

普通执行更接近:

CPU → Launch Kernel A
CPU → Launch Kernel B
CPU → Launch Kernel C
CPU → Launch Kernel D
...

GPU 做完一个小任务,CPU 再继续提交后面的工作。

对于非常大的矩阵,这点 Launch Overhead 往往不突出。

但 Decode 每颗 token 都要重复大量小操作。

这时“启动 GPU 工作”本身也开始变得有成本。

CUDA Graph 的思路就是把一串 GPU 操作提前 Capture:

普通模式
CPU → A
CPU → B
CPU → C
CPU → D

变成:

CPU
Replay Graph
A → B → C → D

从而减少重复的 CPU 调度与 Kernel Launch 开销。

所以对单 token Decode 来说:

CUDA Graph 不是锦上添花的小功能,而是很自然的一层低延迟优化。


生产 Serving 的 CUDA Graph 不是简单的 ON / OFF

如果只是跑一个固定 Shape Benchmark,Capture 一张 Graph 很容易理解。

但真正 Serving 时,Batch 是动态的:

有时全是 Decode
有时有 Prefill
有时是 Prefill + Decode 混合
Batch Size 也一直变化

所以实际问题不是:

“支不支持 CUDA Graph?”

而是:

不同 Runtime Batch 到底应该走哪种 Graph 模式?

vLLM 当前把 CUDA Graph 分成多个运行模式,例如:

NONE
PIECEWISE
FULL
FULL_DECODE_ONLY
FULL_AND_PIECEWISE

其中在支持对应能力的 V1 配置里,纯 Decode 可以使用更完整的 Graph,而 Prefill / Mixed Batch 可以走 Piecewise 路径,再由 Runtime 根据当前 Batch 类型选择。

这件事对我来说很重要。

因为 CUDA Graph 已经不再只是:

Benchmark 时手动打开的优化开关

而开始变成:

Serving Runtime 自己的一部分执行策略。


CUDA Graph 上面还有 Compile / Inductor

再往上一层还有:

torch.compile / Inductor

vLLM V1 并不是简单把整个模型丢给一个默认 torch.compile(model) 就结束。

它会围绕自身 Serving 执行方式处理:

  • Graph capture / partition
  • Compilation cache
  • Shape specialization
  • Piecewise compilation
  • Inductor
  • 自定义优化 pass

于是整个执行链可以粗略理解成:

Model
Graph / Compile
Inductor / Custom Pass
Optimized Kernel
CUDA Graph
Runtime Replay

走到这一层以后,再把 vLLM 理解成“一个用了 PagedAttention 的 API Server”已经明显不够了。

它真正做的是:

把模型执行、Kernel、Graph 和 Serving Runtime 连在一起。


6. TP、NVFP4、Kernel、CUDA Graph 应该放在一条链里看

前面几篇是分开讲的:

NVFP4
→ 让权重更小

TP2
→ 两张 GPU 同时参与同一层

Kernel
→ 当前矩阵到底怎么执行

CUDA Graph
→ 减少 Launch / CPU 调度

但生产部署里它们其实是一条链。

以这次双 5060 Ti 为例:

Qwen3.8-27B NVFP4
权重完整 GPU Resident
TP2
两卡同时处理权重分片
NVFP4 / Attention Kernel
Compile
CUDA Graph

任何一层没有走到理想路径,最后的 token/s 都会受到影响。

所以我现在不太喜欢只问:

  • NVFP4 能快多少?
  • CUDA Graph 能快多少?
  • TP 能快多少?

更值得看的问题是:

这几个优化组合以后,整条执行链有没有真正工作。


7. vLLM 的优势最后还会延伸到 Serving 层

GPU 执行链做好以后,生产服务还有另一类问题:

请求 A:正在 Decode
请求 B:100K Prefill
请求 C:刚到达
请求 D:和上一轮共享大量 Prefix

这时单纯让一个 GEMM 再快一点已经不够。

还需要:

  • Scheduler
  • Continuous Batching
  • Chunked Prefill
  • Paged Cache / Block 管理
  • Prefix Cache

来决定:

GPU 当前这一轮到底应该算谁,以及哪些东西根本不用重新算。

这也是 vLLM 让我更愿意拿来做长期 Serving 的另一半原因。

但这部分我不会在这一篇展开。

因为第六篇会专门讨论:

100K Context 装得下以后,Prefix Cache、Chunked Prefill、Continuous Batching 和 MTP 到底分别解决什么问题。

这里先记住一点就够了:

vLLM 的优化不只发生在单颗 token 的 Kernel 内,也发生在请求之间。


8. 那 llama.cpp 呢?

这并不是说 llama.cpp 没有 GPU Kernel、Batching 或 CUDA Graph 一类优化。

它同样是一套非常成熟、而且极其重要的推理 Runtime。

我认为二者真正的区别,不应该写成:

llama.cpp 慢 / vLLM 快

这种过度简单的判断。

更准确的定位是:

llama.cpp 更擅长把“可部署硬件范围”做得非常宽

例如:

  • GGUF
  • 大量量化格式
  • CPU-only
  • CPU + GPU
  • RAM Offload
  • 异构 GPU
  • 不同显存容量
  • 本地桌面与个人设备

像 Flash-Next 这种模型,如果个人硬件根本不可能全 GPU Resident,那么 llama.cpp 的 CPU + GPU 路线反而可能是最现实的入口。

vLLM 更适合在“模型可以 GPU Resident”以后继续追求 Serving 效率

例如:

  • TP / PP
  • 高性能量化 Kernel
  • Compile
  • CUDA Graph
  • Continuous Batching
  • Scheduler
  • Prefix Cache
  • 高吞吐 API Serving

所以我更愿意用一句话概括两者:

llama.cpp 把个人硬件能触碰到的模型上限往上推;vLLM 则在模型能够 GPU Resident 以后,把 NVIDIA GPU Serving 的性能上限继续往上推。

这两个价值并不冲突。


9. 为什么我的 27B 生产部署默认选 vLLM?

回到这套实际硬件:

2 × RTX 5060 Ti 16GB + Qwen3.8-27B

我们已经找到一种方式,让模型能够:

NVFP4
+
完整 GPU Resident
+
给 Cache 留空间

这时候我就没有太强的理由主动把热点权重再搬回 CPU。

我的关注点已经从:

怎么把模型塞进去?

转向:

怎么让两张 GPU 同时工作?
怎么让 NVFP4 走正确 Kernel?
怎么减少 Kernel Launch?
怎么提高 Serving 吞吐?
怎么避免重复 Prefill?

而这些问题正好是 vLLM 的主场。

所以对这类场景,我现在的默认选择是:

先用 vLLM,把完整 GPU Serving 路线跑通。

如果模型大到根本做不到合理 GPU Resident,再回到 llama.cpp / GGUF / RAM Offload 的容量路线。


10. 我的生产推理优化顺序

现在让我重新部署一个 27B 服务,我大概会按这个顺序:

1. 选择模型与可接受的量化

2. 尽量让权重完整 GPU Resident

3. 确认量化格式真的有当前 GPU 的有效计算路径

4. 选择合适的 TP / PP

5. 确认 GEMM / Attention Kernel

6. Compile / CUDA Graph

7. Context / Cache 规划

8. Scheduler / Continuous Batching

9. Prefix Cache

10. 最后再评估 MTP-based Speculative Decoding

其中最重要的变化是:

我已经不把“模型成功启动”当成部署完成。

真正想要的是:

  • 模型质量合理
  • 权重尽量 GPU Resident
  • 低精度硬件能力被正确利用
  • Kernel 走在合适路径
  • GPU 尽量少空转
  • 重复计算尽量少做
  • 多卡和多请求都能有效调度

这时候 NVFP4 就不只是“省了几 GB 显存”。

vLLM 也不只是“KV Cache 管得比较好”。

它们最终都属于同一件事:

把有限的 GPU 资源尽可能转化成真正可用的 Token。

接下来就进入剩余显存里最大的一块变量之一:长上下文 Cache。

下一篇:

《200K Context 到底需要多少显存?从 KV Cache 算到 Qwen3.8 的真实部署》


系列导航: ← 上一篇:两张 RTX 5060 Ti 为什么没有两倍推理速度?显存带宽、PCIe 与 P2P · 下一篇:200K Context 到底需要多少显存?从 KV Cache 算到 Qwen3.8 的真实部署 →

使用 Hugo 构建
主题 StackJimmy 设计