请求入口
Request entry
OpenAI API、LangChain、HTTP client 最终都会变成同一类生成请求。到 engine 之前,请求里已经带上 prompt、sampling params、max tokens、stop words、streaming 等元信息。
服务层负责网络协议、参数校验、序列化和返回格式,engine 只管调度和执行。这样接入层不会把 GPU 绑死在单个请求上。
Workflow
Request entry
OpenAI API、LangChain、HTTP client 最终都会变成同一类生成请求。到 engine 之前,请求里已经带上 prompt、sampling params、max tokens、stop words、streaming 等元信息。
服务层负责网络协议、参数校验、序列化和返回格式,engine 只管调度和执行。这样接入层不会把 GPU 绑死在单个请求上。
Scheduling & batching
核心状态至少有 waiting、running,以及必要时的 swapped。调度器每轮都要回答两个问题:谁能先拿到 block,谁继续保住当前 decode 的位置。
不是等整批结束再换下一批,而是每轮 decode 后立刻重组 batch。老请求继续 decode,新请求只要还有预算就能插进来做 prefill。
本轮 batch size、token budget、显存预算、是否复用 CUDA graph,都会在这里定下来。真正限制并发的不是“请求数”,而是本轮能塞下多少 token 和 KV block。
PagedAttention KV cache
Page Table
GPU Memory
逻辑 token 连续,但物理 KV block 不必连续,映射关系交给 block table。请求只关心“第 i 段历史在哪个 block”,不要求整个上下文是一整块连续显存。
GPU worker
真正的算力热点在这里。prefill 会把整段 prompt 过一遍 decoder stack;decode 只推进最后一个 token,但每层 attention 都要回头读历史 KV。
prefill 更像大矩阵吞吐,decode 更像小步快跑。prefill 常见瓶颈是算力吞吐,decode 更容易被 KV 访存、kernel launch 和 batch 形状变化拖住。
Output processing
temperature、top-k / top-p、repetition penalty、bad words 过滤都在这里发生。到这一步,模型执行已经结束,剩下的是从 logits 选 next token。
不同 sampling 策略决定结果的稳定性、多样性和额外开销。线上服务最常见的是 greedy 或随机采样,beam search 更适合离线场景。
next token 确认后会写回新 KV、更新 token ids、检查 stop condition。请求结束时对应的 block 会被回收,空出来的位置立刻能给 waiting queue 里的新请求。
Architecture
整段 prompt 一次通过 decoder stack,批量建立首批 KV。
每轮只新增一个 token,但会频繁读取历史 KV 并追加新 KV。
prefill 更偏吞吐;decode 更偏缓存组织、调度补位和低延迟响应。
Flow
Request ingress
请求先被切成 token,并带着 sampling params 进入 engine;这时系统已经开始估算 prefill 成本。
Continuous batching
vLLM 每轮 decode 后都能重组 batch,而不是等整批完全结束再处理下一批。
PagedAttention
KV cache 被切成固定 block,逻辑 token 连续,物理显存不需要连续。
Decoder execution
真正的高密度计算发生在 decoder block;attention 会读历史 K/V,MLP 产出新 hidden state。
Streaming
采样得到 next token 后,结果立刻回传客户端,同时新 KV 也被写回 cache。
Scheduler
Running: A prefill, B decode, C decode
Waiting: D, E
系统优先保住已有 decode 的连续性,同时把 A 的 prompt 吃进去建立首批 KV。
Running: A decode, B decode, D prefill
Waiting: E, F
B 还没结束,不能因为 D 新来就整批重排;D 只是在本轮剩余预算里补位。
Running: A decode, D decode, E prefill
Waiting: F
A 可能接近结束,空出来的 block 和 token budget 让 E 可以进场建立自己的 KV。
Running: D decode, E decode, F prefill
Waiting: queue 逐步清空
steady state 下,decode 在前面连续推进,新请求在后面持续补位,这就是 continuous batching 的实际形状。
静态 batch 必须等一整批都结束才能换下一批,短请求会被长请求拖住。continuous batching 的核心就是每轮 decode 后都允许重组。
decode 已经在生成中,停它会直接伤害 token latency。prefill 通常更重,但它更容易被切成“插空补位”的工作。
不是单纯看请求数量,而是看本轮 token budget、空闲 KV blocks、显存余量,以及当前 batch 形状是否适合复用已有执行计划。
Memory
vLLM 不是把一个请求的 KV 当成一整块连续大数组来维护,而是把历史拆成多个固定大小的 block,再通过 block table 把逻辑 token 顺序映射到这些离散 block 上。
prefill 结束后,请求历史 token 对应的 K/V 会被按 block 写入 cache。长 prompt 不会强行占成一段连续显存,而是拆成多个固定大小的块。
下一轮 decode 来了,attention 先根据 block table 找到这个请求历史 token 分别落在哪些 block,再把这些 K/V 组织成当前计算需要的视图。
本轮生成出一个新 token 后,它对应的新 K/V 会被追加到当前末尾 block;如果末尾 block 满了,就再申请一个新 block。
请求完成时只回收它占用的 block。别的请求即使夹在中间,也不用整体搬迁,所以碎片和搬运成本都更可控。
把一个请求的所有 KV 当成线性连续内存。好处是概念直观,坏处是扩容、回收、共享前缀都容易牵一发而动全身。
把 KV cache 看成很多固定大小的 block,请求只维护“逻辑位置 -> block id”的映射。扩容是加 block,不是重搬整段历史。
逻辑序列看到的是 token 顺序,底层实际拿到的是 block id 列表。attention 通过映射关系去找 K/V,而不是按连续地址整段扫描。
两个请求共享同一段前缀时,可以复用已有 block,不必把 prefix 再算一遍再存一遍。
请求结束时只需要回收自己占用的 block,不需要搬迁别的请求的 KV,这也是它比连续大缓冲区更稳的地方。
请求调度与批处理
token 级别连续推进
KV cache 分页管理
算力与访存同时优化
接入、执行、输出解耦
服务侧最终目标