CUDA系统拆解-17-vLLM、TensorRT-LLM 与 Continuous Batching:CUDA 为什么最终连到推理系统

本文是「CUDA系统拆解」系列第 17 篇。
系列导读:CUDA系统拆解-00-导读:从编程模型到 AI 推理系统的学习路线
上一篇:CUDA系统拆解-16-CUTLASS、Triton、cuBLAS 与 FlashAttention:高性能实现都在做什么
下一篇:CUDA系统拆解-18-NCCL、多GPU与通信隐藏:规模扩展怎么不被通信拖垮

1. 这篇解决什么问题

这一篇要讲清的不是“某个框架怎么用”,而是现代 LLM 在线推理到底在解决什么系统问题。核心有 6 件事:

  • 为什么在线 LLM serving 和离线前向不是同一个问题。
  • 为什么 prefill 更偏 compute-bound,而 decode 更偏 memory-bound
  • 为什么传统静态 batching 不够用。
  • continuous batching / in-flight batching 到底在解决什么调度问题。
  • paged KV cache / paged attention 到底在解决什么显存和访存问题。
  • 为什么调度、kernel 和显存管理必须一起看,而不能拆开看。

如果这篇只记住一句话,那就是:

现代 LLM 推理系统的核心竞争力,很多时候不在“模型会不会跑”,而在“动态请求怎么调度、KV cache 怎么管理、decode 阶段怎么把 GPU 尽量喂满”。

2. 先记住的核心结论

  • 在线 LLM serving 面对的是动态到达、变长、生命周期不同的请求,和离线固定 batch 前向完全不是一类问题。
  • prefill 通常更偏 compute-bound,因为计算密集、更像大 GEMM;decode 通常更偏 memory-bound,因为每步计算小,但要频繁读取历史 KV cache。
  • 静态 batching 假设请求整齐、同步、同生命周期,这和真实在线请求分布不匹配。
  • continuous batching / in-flight batching 的目标是持续维护活跃请求集合,让新请求进入、已完成请求退出,从而减少 GPU 空转。
  • paged KV cache 的意义不是“一个存储小技巧”,而是把 KV cache 从大块连续内存改成小块分页管理,降低显存浪费并支持动态调度。
  • paged attention 不只是内存管理,还要求 attention kernel 适配“逻辑连续、物理分块”的 KV 访问方式。
  • vLLM 和 TensorRT-LLM 的共同点,是都在围绕调度、KV cache、decode 吞吐和低延迟做系统设计;差别更多在工程栈、优化深度和部署取向。

3. 正文讲解

3.1 为什么需要专门的 LLM 推理框架

如果只是离线跑一批固定 shape 的 Transformer 前向,普通深度学习框架加标准库也能做。但在线 LLM serving 面对的不是这种条件。

在线请求有几个根本特点:

  • 请求是动态到达的,不会整齐地等你凑满一个 batch
  • prompt 长度不同,输出长度也不同
  • 一部分请求还在 prefill
  • 一部分请求已经进入 decode
  • 每个请求的 KV cache 都在持续增长
  • 有的请求很快结束,有的请求会跑很久

所以问题不再只是“算子快不快”,而是:

  • 当前该让哪些请求上 GPU
  • 这些请求的 KV cache 放在哪里
  • 哪些 kernel 该在这一轮执行
  • 如何在吞吐和延迟之间做折中

这就是为什么 vLLM、TensorRT-LLM 这类框架存在。它们不是简单把模型封装起来,而是在做在线推理的系统组织。

3.2 Prefill 和 decode 为什么必须分开理解

LLM 推理通常分成两个性质很不一样的阶段。

prefill 阶段:

  • 一次性处理整个 prompt
  • attention 和线性层计算量都比较大
  • 更接近大矩阵计算
  • 通常更偏 compute-bound
  • 更容易利用 GEMM、Tensor Core 和高吞吐 kernel

decode 阶段:

  • 每次只生成一个新 token
  • 新增计算量相对较小
  • 但每一步都要读取历史所有 token 的 KV cache
  • 很多时间花在读 KV 和组织访存上
  • 通常更偏 memory-bound

这是 LLM 推理里最重要的系统直觉之一。
如果不把 prefill 和 decode 分开看,就很容易误判瓶颈。

3.3 为什么静态 batching 不够用

最朴素的 batching 思路是:

  • 等一批请求凑齐
  • pad 到相近长度
  • 一起执行
  • 整批结束后再处理下一批

这在离线推理里还能工作,但在线 serving 问题很多:

  • 请求到达时间不同,不能一直等
  • 请求长度差异大,padding 浪费明显
  • decode 是逐 token 推进,不同请求进度不同
  • 有的请求已经完成,有的请求还要继续跑
  • KV cache 的增长和回收都不是同步的

也就是说,静态 batching 假设的是“整齐、同步、同生命周期”的任务;而在线 LLM 请求恰恰是“异步、变长、生命周期不同”的任务。

3.4 continuous batching / in-flight batching 是什么

continuous batching 的直觉定义是:

不再让一整批请求同生共死,而是在每个调度步里动态维护一个活跃请求集合。

它的典型行为是:

  • 新请求可以持续加入
  • 已完成请求可以立刻退出
  • GPU 每一轮都尽量被可运行请求填满
  • 系统不必等整批结束再开启下一批

TensorRT-LLM 常用 in-flight batching 这个说法,本质上和 continuous batching 是同类思想:请求在系统运行过程中动态进入、退出和重排。

它解决的核心问题是:

  • 提升 GPU 利用率
  • 减少短请求被长请求拖住
  • 减少“整批等待”带来的空转

但它也会带来新复杂度:

  • 每一轮 batch shape 都可能变化
  • prefill 和 decode 可能要混合调度
  • KV cache 管理更复杂
  • kernel 输入组织更难
  • CUDA Graph 的适用范围也会受影响

所以 continuous batching 不是一个“小技巧”,而是整个 serving 系统的调度骨架。

3.5 为什么 KV cache 是在线推理的核心难点

在 decode 阶段,每生成一个 token,都要把新的 K/V 存下来,供后续 token 使用。因此每个请求的 KV cache 会随着生成过程不断增长。

如果用最朴素的方法管理 KV cache,通常会想到:

  • 给每个请求分一段连续的大显存
  • 然后顺着往后写

这在动态在线系统里问题很大:

  • 你不知道每个请求最后会生成多长
  • 按最大长度预留会浪费很多显存
  • 请求结束后空间回收不够灵活
  • 如果空间不够,扩容和迁移代价很高

这和操作系统里的动态内存管理问题很像。真正困难的不是“能不能存”,而是“能不能在动态请求场景下高效分配、增长、回收和复用”。

3.6 paged KV cache / paged attention 到底是什么

paged KV cache 的核心思想是:

不要求一个请求的 KV cache 必须物理连续,而是把它切成固定大小的 blocks/pages,再通过映射关系把逻辑位置对应到物理位置。

这样做有几个直接好处:

  • 显存分配粒度更细,浪费更少
  • 请求增长时只需要继续分配新块
  • 请求结束后回收更灵活
  • 更适合和 continuous batching 一起工作

但这还只是内存管理的一半。
真正的难点在于:attention kernel 也得跟着变。

因为现在历史 KV 不再是一整段连续内存,而是分散在多个 block 里。于是 kernel 读取历史 K/V 时需要:

  • 先根据逻辑位置找到对应物理 block
  • 再在 block 内定位偏移
  • 按 block 组织访存和计算

所以更准确地说:

  • paged KV cache 解决的是存储和生命周期管理问题
  • paged attention 解决的是适配这种分页布局后的 attention 访存与计算问题

这就是为什么它不只是“显存管理策略”,而是“显存布局 + kernel 访问模型”一起设计。

3.7 为什么调度、kernel 和显存管理必须一起看

这一点是很多人最容易漏掉的。

如果你只看调度,会觉得 continuous batching 只是队列管理。
如果你只看显存,会觉得 paged KV cache 只是内存分配。
如果你只看 kernel,会觉得 attention 只是算子优化。

但在真实系统里,这三者强耦合:

  • 调度决定这一轮哪些请求进入活跃集合
  • 活跃集合决定当前 batch shape 和要读哪些 KV blocks
  • KV 布局决定 attention kernel 的访存模式
  • kernel 的开销和 shape 特征反过来影响调度策略

所以现代 LLM 推理系统本质上是在同时平衡:

  • 吞吐
  • 延迟
  • 显存利用率
  • launch overhead
  • decode 阶段的访存效率

这也是为什么“框架层术语”最终都能翻译回 GPU 语言:

  • continuous batching 是调度层在减少 GPU 空转
  • paged KV cache 是显存层在做细粒度动态管理
  • paged attention 是 kernel 层在适配分块访存
  • chunked prefill 是在平衡 compute-heavy prefill 和 memory-heavy decode

3.8 vLLM 和 TensorRT-LLM 的定位差别

从学习角度看,可以把两者这样理解:

vLLM 更像:

  • 围绕高吞吐 serving 组织的一套开源框架
  • 强调 paged attention、continuous batching、KV cache 管理
  • 很适合作为理解现代 LLM serving 机制的入口

TensorRT-LLM 更像:

  • 更深地绑定 NVIDIA 推理栈的高性能生产化路线
  • 更强调 engine 化、量化、调度、并行策略和硬件协同
  • 更适合看“如何把这些机制和 NVIDIA 优化栈压得更深”

更好的面试回答不是“谁更强”,而是:

两者都围绕同一组核心矛盾工作:动态请求调度、KV cache 管理、decode 吞吐和低延迟;差别主要在工程栈、优化深度和部署目标。

4. 和 AI 推理的关系

这一篇其实就是 AI 推理系统本体。

前面的 CUDA、访存、Tensor Core、FlashAttention 这些知识,到这里才真正汇总成系统判断:

  • 为什么 decode 常常不由 Tensor Core 峰值决定,而由 KV 读取决定
  • 为什么框架设计要围绕请求生命周期,而不是只围绕算子
  • 为什么高 QPS、低延迟和高显存利用率往往互相牵制
  • 为什么 serving 框架的竞争力很大一部分来自 scheduler、KV cache 和 kernel 组织,而不是单个 API

对于 AI infra / 推理岗位,这一篇是典型高频主线,因为它能直接暴露你有没有“从单 kernel 视角切换到系统视角”。

5. 常见误区

  • 在线 LLM serving 不是“把离线前向多跑几次”,两者面对的请求分布和系统约束完全不同。
  • decode 不是天然 compute-heavy,很多时候它更偏 memory-bound
  • continuous batching 不只是把 batch 变动态,它背后是完整的调度问题。
  • paged KV cache 不只是节省显存,它还影响 attention kernel 的访问模式。
  • 不是只要 kernel 很快,系统吞吐就一定高;如果调度和 KV 管理做不好,GPU 仍然会空转或被访存拖住。

6. 复习自测

  • 为什么在线 LLM serving 和离线固定 batch 前向不是同一个问题?
  • 为什么 prefill 更偏 compute-bound,而 decode 更偏 memory-bound
  • 静态 batching 的假设是什么?它为什么不适合真实在线请求?
  • continuous batching / in-flight batching 的目标和代价分别是什么?
  • paged KV cache 解决了哪些显存管理问题?为什么它天然适合动态请求?
  • 为什么 paged attention 不能只理解成“内存分页”,而必须连同 kernel 访问方式一起理解?
  • 如果一个系统 decode 吞吐很差,你应该只盯着单个 attention kernel,还是把调度、KV cache 和 kernel 一起查?为什么?

系列导航