
1. 从一次线上抖动说起为什么 Batching 才是 GPU Serving 的第一性原理如果你正在自研 LLM Serving 框架或者在 vLLM、SGLang、TGI、TensorRT-LLM 之间做选型调参大概率遇到过这种场景单请求跑得好好的一上并发吞吐就上不去TTFT 忽高忽低长 prompt 一进来整个流式输出就卡住。排查半天网络、排查半天显存最后发现问题根本不在这些地方而在调度器怎么把请求拼成 batch。Batching 就是 GPU Serving 的第一性原理。这句话听起来像口号但拆开看很实在GPU 的强项是大规模规整并行计算一次矩阵乘法里如果 batch size 太小大量计算单元就空着。把零散请求合成 GPU 喜欢的大任务这个动作就是 batching。它决定了 GPU 到底是在满负荷干活还是在用昂贵硬件跑小任务。但 LLM 的 batching 和传统模型完全不是一回事。传统模型一次输入一次输出凑一批跑完就结束。LLM 是自回归生成decode 阶段逐 token 循环一个 batch 可能要 decode 几百轮。如果还用静态 batch短输出请求早就该结束了却被长输出请求拖着一起跑GPU 上留着一堆空位。这就是为什么现代 Serving 框架都在做 continuous batching——把调度粒度从「整批请求」推进到「每个生成 step」。这篇会聚焦 vLLM 与 TensorRT-LLM 的 continuous batching 调度差异在 TaoToken 统一 Key/API 通道下对比静态 batching 与连续批处理的吞吐/延迟曲线。我会给出可复制的启动参数、压测脚本和显存占用验证步骤让你能自己复现这个结论。适合正在做推理服务调优、被 TTFT 抖动和吞吐瓶颈困扰的工程师。2. TaoToken 统一 Key 前置把多框架对比的接入成本压到最低做 vLLM 和 TensorRT-LLM 的 batching 对比最烦的其实不是压测本身而是每个框架都要单独配一套鉴权和 endpoint。vLLM 起一个 OpenAI 兼容服务TensorRT-LLM 起一个两边 Key 不一样、Base URL 不一样、模型名不一样压测脚本要改好几处很容易在接入环节就耗掉半天。TaoToken 在这里的价值是提供一个统一的 Key 和 API 通道。你可以在 https://taotoken.net/api 拿到统一的 Base URL用同一个 Key 去访问不同后端压测脚本里只需要换 model 字段不用动鉴权逻辑。这对做框架对比特别友好——变量控制住了测出来的吞吐和延迟差异才可信。先拿 Key。访问 https://taotoken.net/api-keys 创建你的 API Key建议单独建一个用于压测的 Key方便后面看用量和排查。拿到之后记下两样东西Base URL 是https://taotoken.net/apiKey 形如sk-xxxx。如果你用的是 Claude Code 这类编码工具做辅助调试可以走 https://taotoken.net/claude-code-anthropic 的接入方式如果只是想先验证模型通不通用 https://taotoken.net/chat 的模型对话页面发一条请求最快。长期跑编码和 Agent 任务的话https://taotoken.net/coding-plan 的 Coding Plan 更适合持续压测。这里要强调一点TaoToken 是统一接入通道不是让你跳过框架本身的调度逻辑。vLLM 的 continuous batching、TensorRT-LLM 的 in-flight batching 都还是在各自服务端跑的TaoToken 负责的是把请求稳定地送进去、把结果稳定地取回来。压测时如果接入层抖动你根本分不清是调度器的问题还是通道的问题所以统一 Key 这件事本身就是实验可复现的前提。配置上我建议把环境变量固定下来后面所有脚本都读同一份export TAOTOKEN_BASE_URLhttps://taotoken.net/api export TAOTOKEN_API_KEYsk-你的压测Key export TAOTOKEN_MODEL你的模型ID模型 ID 一定要和你在 TaoToken 控制台里看到的保持一致写错了会直接 404 或者 model not found这个坑后面排障章节会细说。3. 可复制配置vLLM 与 TensorRT-LLM 的 continuous batching 启动参数这一节是全文最核心的可复制部分。我会分别给出 vLLM 和 TensorRT-LLM 的启动配置重点标出和 batching 调度直接相关的参数并解释每个参数调大调小会发生什么。3.1 vLLM 的 continuous batching 关键参数vLLM 默认就开启 continuous batching真正要调的是 token budget 和并发序列数。下面是一份可以直接用的启动配置我用 JSON 形式把关键参数列出来方便你对照修改{ model: 你的模型路径或ID, served_model_name: batch-test-model, max_num_seqs: 256, max_num_batched_tokens: 8192, enable_chunked_prefill: true, gpu_memory_utilization: 0.90, swap_space: 8, block_size: 16, scheduler_policy: fcfs, disable_log_stats: false }max_num_seqs控制同时并发生成的序列数调大能提升吞吐但显存压力会上升短请求排队时间也可能变长。max_num_batched_tokens是每轮调度的 token 预算它同时约束 prefill 和 decode是防止长 prompt 撑爆 batch 的关键。enable_chunked_prefill打开后长 prompt 会被切成多个 chunk在 chunk 之间插入 decode step避免长 prefill 独占 GPU 导致流式卡顿。启动命令可以这样写vllm serve 你的模型路径 \ --served-model-name batch-test-model \ --max-num-seqs 256 \ --max-num-batched-tokens 8192 \ --enable-chunked-prefill \ --gpu-memory-utilization 0.90 \ --port 80003.2 TensorRT-LLM 的 in-flight batching 配置TensorRT-LLM 里对应的概念叫 in-flight batching核心思想和 continuous batching 一致。它的配置通常写在 build 阶段和 runtime 阶段两部分。runtime 阶段的关键参数如下{ max_batch_size: 256, max_input_len: 4096, max_output_len: 2048, max_num_tokens: 8192, enable_chunked_context: true, kv_cache_free_gpu_memory_fraction: 0.85, scheduler_policy: guaranteed_no_evict }max_num_tokens对应 vLLM 的 token budgetenable_chunked_context对应 chunked prefill。kv_cache_free_gpu_memory_fraction决定 KV Cache 能用多少显存这个值设太高容易 OOM设太低并发上不去。3.3 静态 batching 对照组怎么配为了对比你需要一个静态 batching 的对照组。最简单的做法是把 vLLM 的max_num_seqs设成固定值同时关掉 chunked prefill并让压测脚本按固定 batch 发送请求vllm serve 你的模型路径 \ --served-model-name static-batch-model \ --max-num-seqs 32 \ --max-num-batched-tokens 32768 \ --no-enable-chunked-prefill \ --port 8001这样静态组会等请求凑齐再跑长输出会拖住短输出正好用来复现第一性原理的结论。3.4 压测脚本压测脚本用 Python 写读环境变量里的 TaoToken 配置这样 vLLM 和 TensorRT-LLM 两组可以共用同一份脚本只改 model 字段import os, time, asyncio, aiohttp, statistics BASE_URL os.environ[TAOTOKEN_BASE_URL] API_KEY os.environ[TAOTOKEN_API_KEY] MODEL os.environ[TAOTOKEN_MODEL] async def one_request(session, prompt, max_tokens): payload { model: MODEL, prompt: prompt, max_tokens: max_tokens, temperature: 0.0, stream: False, } headers {Authorization: fBearer {API_KEY}} t0 time.perf_counter() async with session.post(f{BASE_URL}/v1/completions, jsonpayload, headersheaders) as resp: data await resp.json() t1 time.perf_counter() usage data.get(usage, {}) return { latency: t1 - t0, completion_tokens: usage.get(completion_tokens, 0), } async def run(concurrency, prompt, max_tokens): async with aiohttp.ClientSession() as session: tasks [one_request(session, prompt, max_tokens) for _ in range(concurrency)] results await asyncio.gather(*tasks) latencies [r[latency] for r in results] total_tokens sum(r[completion_tokens] for r in results) return { concurrency: concurrency, p50: statistics.median(latencies), p95: sorted(latencies)[int(len(latencies) * 0.95) - 1], tokens_per_sec: total_tokens / max(latencies), } if __name__ __main__: prompt 请用三百字解释 continuous batching 的原理。 for c in [1, 4, 16, 64, 128]: print(asyncio.run(run(c, prompt, 256)))跑的时候把TAOTOKEN_MODEL分别指向 vLLM 组和 TensorRT-LLM 组的模型名就能拿到两条吞吐/延迟曲线。4. 验证请求与成功结果吞吐/延迟曲线怎么读配置好之后先发一条单请求确认通道是通的curl -s https://taotoken.net/api/v1/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: $TAOTOKEN_MODEL, prompt: hello, max_tokens: 16 }返回里能看到choices[0].text和usage.completion_tokens说明接入正常。如果这里就报错先别急着压测去第 5 节排障。单请求通了之后跑压测脚本你会看到类似这样的结果。静态 batching 组在并发从 1 涨到 64 时tokens/s 先升后平p95 延迟快速上升因为长输出请求把整批拖住了。continuous batching 组在同样并发下 tokens/s 继续爬升p95 延迟上升更平缓因为每个 decode step 都在重组 batch短请求结束后位置立刻被新请求补上。显存占用验证用nvidia-smi配合 vLLM 的日志看。vLLM 启动后会打印 KV Cache 的 block 数量和可用显存压测过程中观察GPU memory usage和KV cache usage两条线。continuous batching 下 KV cache usage 会更接近你设的水位线说明显存被更充分地利用静态 batching 下经常看到显存占着但利用率不高因为 batch 生命周期固定空位没法回收。一个典型的成功结果是并发 64 时continuous batching 组 tokens/s 比静态组高 2 到 3 倍p95 TTFT 低 40% 以上KV cache usage 稳定在 80% 到 90% 之间。如果你测出来差距很小大概率是max_num_batched_tokens设得太小或者 chunked prefill 没开调度器没有真正发挥 continuous batching 的能力。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth压测过程中最容易卡住的不是调参而是接入层报错。下面这几个是我实际踩过的对照着查能省很多时间。401 UnauthorizedKey 没读到或者格式不对。检查TAOTOKEN_API_KEY是不是以sk-开头有没有多余空格环境变量有没有在同一个 shell 里 export。用 curl 单独测一次排除脚本问题。local proxy failed / connection refusedBase URL 写错了。确认是https://taotoken.net/api不要带多余路径也不要在末尾加/v1之外的斜杠。如果你本地有网络代理配置先确认它没有拦截这个域名。reading choices 报错 / KeyError: choices返回体不是标准 OpenAI 格式通常是模型 ID 写错导致返回了错误信息。打印完整 response 看error字段把TAOTOKEN_MODEL改成控制台里实际存在的模型 ID。OAuth / 鉴权跳转如果你用的是 Claude Code 或 Cline 这类工具鉴权方式可能不是 Bearer Token。Claude Code 走 https://taotoken.net/claude-code-anthropic 的接入配置Cline 的 MCP 配置里 Base URL、Key、Model ID 三件套要写全缺一个都会鉴权失败。压测时吞吐上不去但没报错先看max_num_batched_tokens是不是太小再看max_num_seqs有没有被显存限制住。用nvidia-smi看显存是不是已经打满如果打满了就调低gpu_memory_utilization或者减小max_num_seqs。TTFT 抖动大检查有没有开 chunked prefill。长 prompt 一次性 prefill 会把 decode 阻塞住表现就是流式输出突然卡顿。开了 chunked prefill 之后长 prompt 被切块decode 节奏会稳很多。Agent 链路里 P99 被放大单次调用看着正常但 Agent 一次任务触发多次 LLM 调用尾延迟会链式放大。这时候要把 P95/P99 TTFT 和 TPOT 单独记录写进调度策略SLO 违约的请求降级或者走单独队列。6. 语义一致 CTA把统一 Key 用在你的 batching 实验里回到第一性原理batching 不是可选优化而是 GPU Serving 的地基。静态 batching 适合离线任务continuous batching 才是在线 LLM 的核心。vLLM 和 TensorRT-LLM 在术语和参数上有差异但调度粒度从「整批请求」推进到「每个生成 step」这个本质是一样的。如果你想自己复现这套对比最省事的路径是用 TaoToken 的统一 Key 把接入层固定住变量只剩框架和调度参数。压测脚本和启动参数上面都给了直接复制改模型 ID 就能跑。排障和接入相关的问题去 https://taotoken.net/api-keys 拿 Key接入文档在 https://taotoken.net/doc。想先验证模型通不通用 https://taotoken.net/chat 发一条请求最快。长期跑编码和 Agent 压测https://taotoken.net/coding-plan 的 Coding Plan 更适合持续使用。最后留一个实用技巧调 batching 参数时先固定max_num_batched_tokens调max_num_seqs再反过来固定max_num_seqs调 token budget一次只动一个变量。同时盯 TTFT、TPOT、P95、KV cache usage、waiting queue 五个指标只看 GPU util 会被长 prompt 堵住系统这种假健康骗过去。