DAY 33 · AI INFRA ROADMAP · 60 DAYS

把推理请求装进 动态车流

Day 31-32 已经跑通 vLLM 与 PagedAttention。Day 33 进入服务调度: 为什么传统静态 batching 不适合生成式模型,Continuous Batching 如何在每个 decode step 动态加入新请求, 以及 TGI、vLLM、SGLang 这类推理引擎如何围绕吞吐、TTFT、TPOT、公平性和 KV Cache 做取舍。

DURATION 2.5 h THEORY 60 min HANDS-ON 90 min METRICS TTFT · TPOT · throughput · queue time STACK vLLM · TGI · SGLang · Scheduler

思维导图

OVERVIEW
DAY 33 · Continuous BatchingSTATIC · IN-FLIGHT · SCHEDULER · TTFT · TPOT01 · MODEL静态 batching02 · LOOPin-flight batching03 · MIXprefill/decode04 · ENGINETGI / vLLM / SGLang·收齐一批再跑·等最慢请求结束·padding 浪费严重·尾延迟放大·每步重新组 batch·完成即释放 slot·新请求可插入·iteration-level scheduling·prefill 计算重·decode 显存带宽重·chunked prefill·抢占与公平性·TGI: waiting ratio·vLLM: token budget·SGLang: policy/cache·SLO 驱动调参DELIVERABLES调度循环图静态/动态对比表引擎调度矩阵压测观察记录
FIG · Continuous batching 的核心:每个 iteration 都重新决定谁进入 GPU,谁等待,谁释放 KV

为什么静态 Batching 不够用

25 MIN

传统深度学习服务常把请求攒成固定 batch,一起 forward,一起返回。 对分类、embedding、rerank 这种一次 forward 就结束的任务,这很自然。 但 LLM 生成是多轮 decode:每个请求 prompt 长度不同、输出长度不同、结束时间不同。 如果还用静态 batch,整批请求会被最长输出拖住,短请求已经完成也不能让新请求进来。

Static batchingbatch 形成后固定到结束
req A
PD1D2EOSidleidle
req B
PD1D2D3D4EOS
req C
waitwaitwaitwaitwaitP
Continuous batching完成就释放,等待请求可插入
req A
PD1D2EOSfreefree
req B
PD1D2D3D4EOS
req C
waitwaitwaitPD1D2
P = prefill,D = decode step。Continuous batching 的关键不是“batch 很大”,而是“batch 成员可以在生成过程中变化”。
维度静态 batchingContinuous / in-flight batching服务影响
组批时机请求到达后等待窗口,收够一批再执行。每个调度 iteration 都重新选活跃序列和等待请求。GPU 更少空转,吞吐通常更高。
请求生命周期一批请求绑定到整段生成结束。某条序列完成后立即释放 slot 与 KV blocks。短请求不必被长输出拖住。
可变长度padding 和等待更明显。按 token budget / sequence budget 动态控制。对聊天场景更友好,但调度器更复杂。
公平性批内公平,批间可能排队很久。需要策略处理长 prompt、长输出、老请求和新请求。调得不好会牺牲 TTFT 或尾延迟。

调度循环放在哪里

35 MIN

LLM serving 的执行循环可以简化为:接收请求,分词,为 prompt 做 prefill, 然后一次 decode 一个 token。Continuous batching 把调度器插在每个 iteration 前面: 它看 waiting queue、running set、KV Cache 预算、token budget、SLO 约束,再决定本轮 GPU 做什么。

HTTP 请求prompt · params等待队列waiting requestsSchedulerseq budget · token budget · KV blocksGPU Forwardprefill chunk / decode tokenRunning Setactive sequencesKV Cacheblocks · pagesToken Outputstream · EOS · stopSCHEDULER QUESTIONS1. 本轮放多少 prefill token? 2. 已运行请求是否继续 decode? 3. 新请求何时插入? 4. KV 不够时抢占谁?
FIG · 调度器每轮根据队列、运行集合和 KV Cache 状态重建 batch

一个最小调度伪代码

while server_is_running:
    new_requests = receive_http_requests()
    waiting_queue.extend(tokenize(new_requests))

    # 释放已结束序列的 KV blocks,把 token 流式返回给客户端
    finished = [seq for seq in running if seq.is_finished()]
    free_kv_blocks(finished)
    running.remove(finished)

    token_budget = max_num_batched_tokens
    seq_budget = max_num_seqs - len(running)

    # 选择一部分 waiting 请求做 prefill,也可能只做 chunked prefill
    batch = choose_prefill_requests(waiting_queue, token_budget, seq_budget)

    # 已经 prefill 完的 running 请求每轮通常 decode 1 个 token
    batch += choose_decode_requests(running, token_budget)

    outputs = model_forward(batch)
    update_kv_cache(outputs)
    stream_tokens(outputs)
TTFT = queue_time + prefill_time + first_decode_time

Continuous batching 如果让 waiting queue 等太久,TTFT 会升高;如果频繁插入 prefill,又可能干扰已有 decode。

TPOT ≈ decode_iteration_time / active_decode_sequences

decode batch 变大通常提升吞吐,但单轮时间也会变长。线上要看 p95/p99 TPOT,不是只看总 tokens/s。

Prefill 与 Decode 的混排

30 MIN

Continuous batching 真正难的地方在于 prefill 和 decode 的资源画像不同: prefill 是大矩阵计算,一次处理 prompt 中很多 token;decode 每轮只生成一个 token, 但要反复读写 KV Cache,更容易受显存带宽和 cache 管理影响。 调度器必须决定:先让等待请求尽快拿到首 token,还是保护正在输出的请求保持稳定 TPOT。

Prefill Heavy

长 prompt 会吃掉 token budget

一个 8K prompt 的 prefill 可能挤占整轮预算,让已经在 decode 的请求等待。chunked prefill 会把长 prompt 切成小块。

Decode Steady

稳定输出依赖短 iteration

聊天流式体验主要看 token 是否持续出来。decode iteration 太长,用户会感觉输出一卡一卡。

KV Budget

调度不是只算 compute

每个活跃序列都占 KV Cache。新请求能不能进来,不只看 batch size,还看剩余 KV blocks。

策略偏向优点风险
Prefill-first尽快接纳新请求,降低 queue time。TTFT 可能更好,等待队列不容易堆积。长 prompt 可能让已有 decode 变慢,TPOT 波动。
Decode-first保护已开始输出的请求。流式输出稳定,用户体感更顺。新请求排队更久,TTFT 尾延迟上升。
Chunked prefill把长 prompt 切成多个调度片段。更好地混合 prefill 与 decode,降低长 prompt 对短请求的阻塞。调度开销上升,不同引擎参数与默认行为不同。
PreemptionKV 预算不足时暂停或重算部分序列。避免整体 OOM,允许更多请求进入系统。重算会浪费计算,尾延迟可能抖动。
判断调度策略好坏时,至少同时看 TTFT、TPOT、E2E latency、tokens/s、queue length 与 KV cache utilization。

TGI / vLLM / SGLang 调度器对比

45 MIN

RoadMap 要求横向比较三类引擎。不要把它们看成“谁绝对更快”, 而要看各自的抽象:怎样控制批大小,怎样处理等待队列,怎样混排 prefill/decode, 以及有没有围绕 KV Cache、prefix cache、结构化生成做额外优化。

引擎调度关键词常见旋钮适合观察什么
TGIcontinuous batching、waiting/served ratio、batch token limits。--max-batch-prefill-tokens
--max-batch-total-tokens
--waiting-served-ratio
--max-waiting-tokens
观察排队窗口、prefill token 上限和等待请求插入时机如何影响 TTFT。
vLLMPagedAttention、token budget、sequence budget、chunked prefill、preemption。--max-num-seqs
--max-num-batched-tokens
--gpu-memory-utilization
--enable-chunked-prefill
观察 KV Cache blocks、running/waiting 队列、prompt/generation throughput 与 preemption。
SGLangruntime scheduler、Radix Cache、prefix-aware workload、chunked prefill。--schedule-policy
--max-running-requests
--max-total-tokens
--chunked-prefill-size
观察共享 prefix、高并发 agent/workflow 请求、cache 命中与调度策略的耦合。
TGI

参数语义直观

TGI 的 launcher 参数很适合学习 batching 上限与等待策略。重点记录 waiting queue 与 served batch 的关系。

vLLM

KV 管理与调度深度绑定

vLLM 的调度效果必须结合 PagedAttention 看:KV blocks 是能否接纳新请求的硬约束。

SGLang

程序化请求与前缀缓存

SGLang 在复杂 prompt/program、多轮调用、共享 prefix 场景下更值得关注调度与 cache 命中。

注意

参数名会随版本变化

这一天的目标不是背参数,而是理解参数背后的资源:序列数量、token 数量、KV Cache、等待队列和 SLO。实际实验前以当前版本官方文档和 --help 为准。

用 vLLM 做一个小实验

60 MIN

延续 Day32 的 vLLM 服务。今天不追求模型质量,只观察调度: 同一模型、同一 prompt/output 分布下,改变 max-num-seqsmax-num-batched-tokens 与 chunked prefill,记录 TTFT、TPOT、吞吐和队列日志。

启动三组服务配置

# A. 保守:低并发,更容易看清单请求延迟
vllm serve Qwen/Qwen2.5-7B-Instruct \
  --served-model-name qwen2.5-7b \
  --max-model-len 4096 \
  --gpu-memory-utilization 0.85 \
  --max-num-seqs 16 \
  --max-num-batched-tokens 4096

# B. 吞吐:提高活跃序列上限
vllm serve Qwen/Qwen2.5-7B-Instruct \
  --served-model-name qwen2.5-7b \
  --max-model-len 4096 \
  --gpu-memory-utilization 0.90 \
  --max-num-seqs 64 \
  --max-num-batched-tokens 8192

# C. 长 prompt:尝试 chunked prefill,参数名以当前 vLLM 版本为准
vllm serve Qwen/Qwen2.5-7B-Instruct \
  --served-model-name qwen2.5-7b \
  --max-model-len 8192 \
  --gpu-memory-utilization 0.90 \
  --max-num-seqs 64 \
  --max-num-batched-tokens 8192 \
  --enable-chunked-prefill

压测脚本:混合短/长 prompt

import asyncio
import statistics
import time
from openai import AsyncOpenAI

client = AsyncOpenAI(api_key="EMPTY", base_url="http://localhost:8000/v1")

SHORT = "用三句话解释 continuous batching。"
LONG = "请阅读以下模拟日志并总结调度瓶颈。" + (" waiting=12 running=48 kv=82%; " * 220)

async def one(i, prompt, max_tokens):
    t0 = time.perf_counter()
    first = None
    chunks = 0
    stream = await client.chat.completions.create(
        model="qwen2.5-7b",
        messages=[{"role": "user", "content": prompt}],
        temperature=0.0,
        max_tokens=max_tokens,
        stream=True,
    )
    async for chunk in stream:
        delta = chunk.choices[0].delta.content or ""
        if delta:
            chunks += 1
            if first is None:
                first = time.perf_counter()
    t1 = time.perf_counter()
    return {
        "id": i,
        "ttft_ms": (first - t0) * 1000 if first else None,
        "e2e_ms": (t1 - t0) * 1000,
        "chunks": chunks,
        "tpot_ms": ((t1 - first) * 1000 / max(chunks - 1, 1)) if first else None,
    }

def pct(values, p):
    values = sorted(v for v in values if v is not None)
    return values[int((len(values) - 1) * p)]

async def main(concurrency=32, long_ratio=0.25):
    tasks = []
    for i in range(concurrency):
        is_long = (i / concurrency) < long_ratio
        tasks.append(one(i, LONG if is_long else SHORT, 160))
    res = await asyncio.gather(*tasks)
    print({
        "concurrency": concurrency,
        "p50_ttft_ms": pct([r["ttft_ms"] for r in res], 0.50),
        "p95_ttft_ms": pct([r["ttft_ms"] for r in res], 0.95),
        "p50_tpot_ms": pct([r["tpot_ms"] for r in res], 0.50),
        "p95_e2e_ms": pct([r["e2e_ms"] for r in res], 0.95),
        "avg_chunks": statistics.mean(r["chunks"] for r in res),
    })

asyncio.run(main(concurrency=32, long_ratio=0.25))

记录模板

experiment:
  model: Qwen/Qwen2.5-7B-Instruct
  gpu: "A10 24GB / L40S 48GB / A100 80GB ..."
  engine: vllm
  version: "..."
  workload:
    concurrency: [8, 16, 32, 64]
    long_prompt_ratio: [0.0, 0.25, 0.5]
    max_tokens: 160
  server_args:
    max_num_seqs: 64
    max_num_batched_tokens: 8192
    gpu_memory_utilization: 0.90
    chunked_prefill: true
  metrics:
    p50_ttft_ms:
    p95_ttft_ms:
    p50_tpot_ms:
    p95_e2e_ms:
    output_tokens_per_s:
    kv_cache_usage:
    running_waiting:
    preemption_count:

常见调度症状

RUNBOOK
现象可能原因观察指标处理方向
tokens/s 很高,TTFT 也很高调度器为了吞吐攒了太多请求或长 prompt 抢占 prefill 预算。queue time、waiting count、p95 prompt length。降低等待窗口或 batch token 上限,启用/调小 chunked prefill。
TTFT 尚可,TPOT 抖动prefill 插入过频繁,decode iteration 被长 prompt 打断。per-iteration latency、generation throughput。限制单轮 prefill token,给 decode 留稳定预算。
频繁 preemptionKV Cache 不足,长上下文或高并发超过预算。KV cache usage、preemption count、recompute time。降低 max model len / max running seqs,增加显存预算,评估 KV 量化。
短请求被长请求拖慢缺少长度感知调度或 prefix/cache 预算被长请求占用。按 prompt_len 分桶的 TTFT/E2E latency。做负载分级、独立实例、短请求优先或 prefix-aware routing。
GPU 利用率低但排队多批 token 上限太低、网络/HTTP 层瓶颈、tokenizer 或客户端压测不足。GPU util、CPU util、request rate、batch size。提高 token/seq budget,排除客户端瓶颈,增加 tokenizer worker 或多副本。

常见疑问

FAQ
Q1 Continuous batching 和动态 batch size 是一回事吗? +
A

不完全是。动态 batch size 只是 batch 大小可变;Continuous batching 强调生成过程中每个 iteration 都能增删序列。请求不需要等当前整批全部结束,完成的序列释放资源后,新请求可以进入后续 iteration。

Q2 为什么 batch 越大 TTFT 可能越差? +
A

更大的 batch 往往意味着请求在队列里等更久,单轮 prefill/decode 也可能更长。吞吐提升和首 token 延迟之间存在经典取舍,线上要用 SLO 限制 batch 上限。

Q3 chunked prefill 一定会提升性能吗? +
A

不一定。它主要缓解长 prompt 对 decode 的阻塞,但会增加调度次数和一些额外开销。短 prompt 为主的负载里收益可能不明显,长上下文混合负载里更值得测试。

Q4 为什么调度器必须关心 KV Cache? +
A

LLM decode 的每个活跃序列都要保留历史 KV。调度器即使有 compute 空闲,如果没有足够 KV blocks 接纳新序列,也不能盲目放入请求,否则会 OOM 或触发昂贵的抢占/重算。

今日检查清单

10 ITEMS
  • 能解释静态 batching 为什么不适合可变长度生成任务。
  • 能画出 Continuous batching 的 iteration-level scheduling 循环。
  • 能说明 prefill 与 decode 的资源画像差异。
  • 能解释 TTFT、TPOT、E2E latency、tokens/s 分别反映什么。
  • 能说明 max-num-seqsmax-num-batched-tokens 的区别。
  • 能说明 chunked prefill 解决什么问题,以及可能带来什么开销。
  • 能对比 TGI、vLLM、SGLang 在调度参数和优化重点上的差异。
  • 能设计短 prompt、长 prompt、混合负载三组压测。
  • 能按 prompt 长度分桶分析 TTFT 与 E2E latency。
  • 能写出一个调度排错 runbook:排队高、TPOT 抖动、preemption、GPU 低利用率。

复盘问题

7 QUESTIONS
  1. 用自己的话解释 static batching、dynamic batching、continuous batching 的区别。
  2. 为什么生成式服务不能只看平均 latency?TTFT 和 TPOT 各自对应用户体感中的哪一段?
  3. prefill-first 与 decode-first 策略分别会牺牲什么?
  4. 一个长 prompt 请求进入系统时,它会如何影响短 prompt 请求的 TTFT?
  5. 如果 vLLM 日志中 waiting 长期很高,你会优先检查哪些参数?
  6. 为什么 prefix cache/RadixAttention 会影响调度策略,而不是单纯的 cache 优化?
  7. 给定一个线上聊天服务 SLO,你会怎样设置吞吐优先和延迟优先两套调度配置?

推荐阅读

5 ITEMS
Paper

Orca: A Distributed Serving System for Transformer-Based Generative Models

理解 iteration-level scheduling 和 selective batching 的经典论文,适合把 Continuous Batching 的思想放回系统设计语境。

vLLM

vLLM Engine Arguments

重点看 scheduler/cache 相关参数,如 max-num-seqsmax-num-batched-tokens、chunked prefill 与 KV 预算。

TGI

Text Generation Inference Launcher

查看 TGI 的 batching、waiting、prefill token 上限等服务参数,用来对比 vLLM 的调度抽象。

SGLang

SGLang Documentation

关注 runtime、Radix Cache、调度策略与结构化生成相关能力。Day34 会继续深入 RadixAttention。

Benchmark

vLLM Benchmark Examples

用官方 benchmark 思路规范负载分布、并发、prompt/output 长度和指标记录。

Serving 调度的本质,是在 GPU、KV Cache、队列和用户耐心之间做实时资源分配。
DAY 33 · Continuous Batching · AI Infra RoadMap