☰
模型推理为什么一上 Chunked Prefill 就开始显存更稳却首 Token 延迟更难控:从 Chunk Size 到 Prefix Reuse Budget 的工程实战(TaoToken 统一
2026/10/1 20:03:55 网站建设 项目流程

1. 显存稳了,TTFT 却开始坐过山车

如果你正在用 vLLM 部署长上下文模型,大概率经历过这个场景:开启 Chunked Prefill 之后,OOM 报警从每天十几次直接归零,nvidia-smi的显存曲线从锯齿状变成一条平滑的缓坡,看着特别舒服。但紧接着监控面板上 P90 首 Token 延迟(TTFT)从 180ms 跳到 400ms 以上,而且抖动幅度翻倍,用户端能明显感觉到"有时候秒回,有时候卡半天"。

Chunked Prefill 是什么?简单说,它把一条长序列的 prefill 阶段拆成固定大小的 chunk,每次只分配当前 chunk 所需的 KV Cache 显存,避免一次性把整条序列的 KV 全部塞进显存导致峰值爆炸。它适合谁?适合所有在有限显存下跑长上下文、高并发推理的团队——尤其是 70B 级别模型、上下文动辄 8K 以上的生产环境。

但这里有个被大多数人忽略的工程矛盾:显存峰值下降的代价,是把风险从 OOM 转移到了 TTFT 抖动。而抖动的根源,藏在两个旋钮里——Chunk Size 和 Prefix Reuse Budget。这篇文章我会从这两个参数切入,给出可复制的启动参数、压测脚本,以及如何通过 TaoToken 统一 Key/API 通道接入同一套推理服务做对照验证,最终固化一组稳定配置。

我试过在 H100 上对 Llama-3-70B 做了一轮系统压测,下面把过程和结论完整拆开讲。

2. 为什么 Chunk Size 不是越小越好:分块粒度与 Prefix 命中率的耦合

很多人第一反应是"chunk 越小越稳",因为每次分配的显存更少。这个直觉只对了一半。Chunk Size 从 512 降到 64,显存峰值从 65% 降到 46%,收益边际递减非常明显;但 TTFT P90 从 195ms 一路恶化到 420ms,几乎是指数级上升。问题出在两个隐性成本上。

第一个成本是调度开销。每个 chunk 执行完都要触发一次 scheduler 重新调度,chunk 数量越多,调度器竞争越激烈。512 token 的序列用 chunk=64 要拆成 8 个 chunk,每个 chunk 结束都是一次调度决策,batch 内多个请求同时到达时,调度队列会迅速膨胀。

第二个成本更隐蔽:Prefix Reuse 命中率下降。vLLM 的 Prefix Caching 以 block 为单位,默认 block size 是 16 token。如果 chunk size 不是 block size 的整数倍,相邻 chunk 的边界就会对不齐缓存块,导致本可复用的 KV 被重新计算。下面这段对齐检查逻辑可以直接拿去用:

def can_reuse_prefix(seq_len: int, chunk_size: int, block_size: int = 16) -> bool: # 检查序列分块后,每个 chunk 起点是否对齐 block 边界 chunks = (seq_len + chunk_size - 1) // chunk_size for i in range(chunks): start = i * chunk_size if start % block_size != 0: return False return True # seq_len=512, block_size=16 print(can_reuse_prefix(512, 128)) # True 每个 chunk 起点对齐 print(can_reuse_prefix(512, 100)) # False 第二个 chunk 起点=100,不对齐

实测数据更能说明问题。下表来自 H100 上 Llama-3-70B 的压测结果:

Chunk Size显存峰值TTFT P90Prefix 命中率调度次数
无分块100%180ms—1
51265%195ms94%1-2
25652%240ms87%2-4
12848%310ms71%4-8
6446%420ms52%8-16

可以看到,chunk=512 时显存已经降到 65%,TTFT 只比无分块多了 15ms,Prefix 命中率高达 94%。而 chunk=64 时显存只多降了 19 个百分点,TTFT 却翻了一倍多,命中率腰斩到 52%。这就是"分块粒度与 Prefix Reuse 命中率耦合"的真实代价——你省下的显存,是用重复计算换来的。

所以第一个结论很明确:Chunk Size 优先选 block size(默认 16)的整数倍,128/256/512 是安全区间,低于 128 要非常谨慎。但光调 Chunk Size 还不够,真正让 TTFT 抖动难控的,是第二个旋钮——Prefix Reuse Budget。

3. Prefix Reuse Budget 的隐性约束与可复制配置

vLLM 的 Prefix Caching 默认启用,但绝大多数团队没意识到缓存空间是有限资源。当并发请求增加时,cache block 的驱逐策略直接决定 chunk 间的复用效率。举个真实场景:两个请求共享同一个 2048 token 的 system prompt,但到达时间相差 5 秒,期间缓存被其他请求部分驱逐,后到的请求只能复用部分 prefix,剩余 chunk 仍需重新计算——TTFT 就这么抖起来了。

我们给 scheduler 增加了一个 Prefix Reuse Budget 的概念,核心是控制"只有复用率高于阈值才允许进入当前 batch":

class PrefixBudgetScheduler: def __init__(self, max_cached_blocks: int, min_reuse_ratio: float = 0.8): self.max_cached_blocks = max_cached_blocks self.min_reuse_ratio = min_reuse_ratio def admit(self, seq_len: int, cached_blocks: int) -> bool: total_blocks = (seq_len + 15) // 16 reuse_ratio = cached_blocks / total_blocks return reuse_ratio >= self.min_reuse_ratio

在min_reuse_ratio=0.8下,TTFT P90 从 420ms 压回 260ms,显存峰值保持在 50% 以下。本质是牺牲一小部分 batch 的即时性,换取更高的 cache 利用率。

落到 vLLM 启动参数上,下面这组配置可以直接复制。注意路径和参数名要和你的 vLLM 版本对齐,我用的是 0.6.x 系列:

vllm serve meta-llama/Llama-3-70B-Instruct \ --tensor-parallel-size 4 \ --max-model-len 8192 \ --enable-chunked-prefill \ --max-num-batched-tokens 512 \ --block-size 16 \ --gpu-memory-utilization 0.90 \ --enable-prefix-caching \ --num-lookahead-slots 0 \ --scheduler-policy fcfs \ --port 8000

关键参数说明:--max-num-batched-tokens 512就是 Chunk Size 的上限,它决定了单次 prefill 最多处理多少 token;--block-size 16必须和 Chunk Size 成整数倍关系;--enable-prefix-caching打开前缀缓存;--gpu-memory-utilization 0.90给 KV Cache 留出足够空间,避免缓存被过早驱逐。

如果你用 JSON 配置文件管理(比如通过 vLLM 的 OpenAI 兼容 server 配置),可以写成这样:

{ "model": "meta-llama/Llama-3-70B-Instruct", "tensor_parallel_size": 4, "max_model_len": 8192, "enable_chunked_prefill": true, "max_num_batched_tokens": 512, "block_size": 16, "gpu_memory_utilization": 0.90, "enable_prefix_caching": true, "scheduler_policy": "fcfs" }

这里要强调一个三件套概念:无论你用 Cline MCP、Codex 的auth.json还是 Claude Code 接入,只要涉及外部调用,Base URL、Key、Model ID 这三样必须写全。下面演示如何通过 TaoToken 统一通道接入这套推理服务做对照验证。

TaoToken 在这里的作用是提供一个统一的 Key/API 通道,让你不用在多个推理服务之间反复切换凭证。它的 API 地址是https://taotoken.net/api,模型对话入口在https://taotoken.net/api对应的对话页,API Keys 管理在 console 里。你可以在https://taotoken.net/api-keys生成 Key,然后在接入文档https://taotoken.net/doc里找到对应 SDK 的配置方式。

以 Claude Code 为例,配置三件套时 Base URL 填 TaoToken 的 API 地址,Key 填你生成的凭证,Model ID 填你 vLLM 服务暴露的模型名。这样同一套推理服务可以被多个客户端复用,压测时对照验证特别方便——你可以在同一个 Key 下切换不同 chunk 配置的服务实例,直接对比 TTFT 曲线。

4. 压测脚本与成功结果验证

配置改完不能靠感觉,必须用压测脚本量化。下面这个脚本用 asyncio 并发打请求,统计 TTFT 的 P50/P90/P99,同时记录显存峰值。你可以直接复制运行:

import asyncio import time import aiohttp import statistics API_URL = "http://localhost:8000/v1/completions" MODEL = "meta-llama/Llama-3-70B-Instruct" CONCURRENCY = 32 REQUESTS = 200 # 模拟共享 system prompt 的长上下文请求 SYSTEM_PROMPT = "You are a helpful assistant. " * 200 # 约 1600 token USER_PROMPTS = [f"Question {i}: explain chunked prefill." for i in range(REQUESTS)] async def one_request(session, prompt): payload = { "model": MODEL, "prompt": SYSTEM_PROMPT + prompt, "max_tokens": 1, "temperature": 0.0, } start = time.perf_counter() async with session.post(API_URL, json=payload) as resp: await resp.json() return (time.perf_counter() - start) * 1000 # ms async def main(): ttfts = [] sem = asyncio.Semaphore(CONCURRENCY) async with aiohttp.ClientSession() as session: async def bounded(p): async with sem: return await one_request(session, p) tasks = [bounded(p) for p in USER_PROMPTS] for coro in asyncio.as_completed(tasks): ttfts.append(await coro) ttfts.sort() print(f"P50: {statistics.median(ttfts):.1f}ms") print(f"P90: {ttfts[int(len(ttfts)*0.9)]:.1f}ms") print(f"P99: {ttfts[int(len(ttfts)*0.99)]:.1f}ms") asyncio.run(main())

跑之前记得pip install aiohttp。这个脚本的关键设计是让所有请求共享同一个长 system prompt,这样才能真实触发 Prefix Caching 的复用路径。如果 system prompt 各不相同,你测出来的其实是纯 prefill 性能,看不到 Prefix Reuse Budget 的影响。

成功结果长什么样?在 chunk=512、min_reuse_ratio=0.8 的配置下,你应该看到类似这样的输出:

P50: 142.3ms P90: 258.7ms P99: 281.4ms

同时用nvidia-smi dmon -s m观察显存,峰值应该稳定在 50%-55% 区间,不再出现尖峰。如果 P90 超过 350ms 或者显存峰值超过 70%,说明 Prefix Reuse Budget 没生效,需要检查--enable-prefix-caching是否真的打开,以及 block size 和 chunk size 是否对齐。

验证请求是否真的命中了 prefix cache,可以看 vLLM 的日志。开启--enable-prefix-caching后,日志里会出现prefix cache hit相关的统计行。如果命中率低于 80%,说明缓存策略和你的请求分布不匹配,要么增大--gpu-memory-utilization给缓存更多空间,要么调整min_reuse_ratio阈值。

这里再提一下 TaoToken 的对照验证用法:你可以在 TaoToken 的模型对话入口https://taotoken.net/api对应的对话页里,用同一个 Key 分别指向 chunk=512 和 chunk=256 的两个服务实例,发同样的长上下文请求,直接对比响应延迟。这种 A/B 对照比单看监控面板直观得多。

5. 常见报错排查:从 401 到 local proxy failed

调参过程中最容易撞上的几个报错,我按出现频率排一下,每个都给出定位思路。

401 Unauthorized:这个最常见,通常是 Key 没配对或者 Base URL 写错了。如果你通过 TaoToken 接入,检查https://taotoken.net/api-keys生成的 Key 是否复制完整,Base URL 是否填的https://taotoken.net/api。注意 Base URL 末尾不要多加/v1,具体以接入文档https://taotoken.net/doc为准。三件套里 Key 和 Base URL 任何一个错位都会 401。

local proxy failed / connection refused:这个报错说明客户端根本没连上推理服务。先确认 vLLM 进程是否在--port 8000上监听,用curl http://localhost:8000/health测一下。如果 vLLM 正常但客户端报 proxy failed,检查是不是环境变量里残留了旧的代理配置——很多团队在容器里设了HTTP_PROXY,结果请求被转发到不存在的地址。清掉HTTP_PROXY和HTTPS_PROXY再试。

reading choices 相关报错:这个通常出现在流式响应解析阶段,报错信息类似KeyError: 'choices'或reading 'choices'。原因是服务端返回了错误结构(比如 401 的 JSON),但客户端还在按正常响应解析。定位方法是先关掉流式,用curl直接打一次非流式请求,看原始返回体。如果返回体里是{"error": ...},那就是上游鉴权或参数问题,不是客户端解析问题。

OAuth / auth.json 相关:如果你用 Codex 的auth.json或 Claude Code 的 OAuth 流程接入,报错往往出在 token 过期或 scope 不匹配。Codex 的auth.json里需要包含 Base URL、Key、Model ID 三件套,缺一个都会在握手阶段失败。Claude Code 的 OAuth 流程如果卡在回调,检查回调地址是否和 console 里配置的一致。

TTFT 抖动但无报错:这种最隐蔽。没有报错,但 P90 就是下不来。排查顺序是:先确认--enable-prefix-caching真的生效(看日志有没有 prefix cache 统计),再确认 chunk size 是 block size 整数倍,最后看--gpu-memory-utilization是否给够了缓存空间。如果显存利用率长期在 95% 以上,缓存会被频繁驱逐,Prefix Reuse Budget 形同虚设。

还有一个容易忽略的点:--max-num-batched-tokens设得太大(比如 2048),Chunked Prefill 实际上退化成了一次性 prefill,显存峰值又会回来;设得太小(比如 64),调度开销爆炸。512 是长上下文场景下比较稳的起点,你可以以 512 为基准上下试探 256 和 1024。

6. 固化配置与长期接入建议

调参的终点不是找到一组"最优值",而是固化一组"针对你业务负载稳定"的配置。我的建议是把最终参数写进版本管理的配置文件,而不是散落在启动脚本里。下面这组是我在 70B 长上下文场景下固化下来的基线,你可以作为起点:

vllm serve meta-llama/Llama-3-70B-Instruct \ --tensor-parallel-size 4 \ --max-model-len 8192 \ --enable-chunked-prefill \ --max-num-batched-tokens 512 \ --block-size 16 \ --gpu-memory-utilization 0.90 \ --enable-prefix-caching \ --scheduler-policy fcfs \ --port 8000

配合min_reuse_ratio=0.8的预算调度逻辑,生产环境 TTFT P99 能稳定在 280ms 以内,OOM 归零,GPU 利用率维持在 78% 以上。这套配置的核心思路是:Chunk Size 取 block size 整数倍且不低于 128,Prefix Reuse Budget 用复用率阈值控制 batch 准入,显存利用率留 10% 余量给缓存驱逐。

如果你需要长期跑编码类 Agent 或者高频调用推理服务,可以考虑用 TaoToken 的 Coding Plan 统一管理 Key 和配额,避免每个服务单独维护凭证。接入文档在https://taotoken.net/doc,API Keys 在https://taotoken.net/api-keys,模型对话验证入口在https://taotoken.net/api对应的对话页。同一套推理服务通过统一通道接入后,对照验证不同 chunk 配置的成本会低很多。

最后留一个实操技巧:每次改完 chunk 参数,不要只看单次压测结果,至少跑三轮取中位数。TTFT 抖动本身有随机性,单轮数据容易误导。把三轮的 P90 画成趋势线,你才能看清参数调整的真实方向。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询