资讯详情

大模型推理引擎vLLM(7):Chunked-Prefills 分块预填充机制与 TaoToken 统一 Key 通道实践

📅 2026/10/12 10:20:10 | 华诺云谱 👁 阅读
大模型推理引擎vLLM(7):Chunked-Prefills 分块预填充机制与 TaoToken 统一 Key 通道实践
1. 从一次线上抖动说起为什么长 Prompt 会拖慢所有人先说一个我真实遇到过的场景。某次内部压测一台 A100 上跑着 vLLM 服务模型是 8B 级别的指令模型。平时短问答的 TBT相邻 token 间隔稳定在 30ms 左右结果一接入文档摘要任务单条 prompt 动辄 6000 到 8000 token整个服务的 TBT 直接飙到 200ms 以上短请求的用户体感就是卡住了。查日志发现问题不在显存也不在模型本身而在调度一个超长 prefill 请求占满了整轮迭代所有正在 decode 的请求都得排队等它算完。这就是 vLLM 里 Chunked-Prefills分块预填充要解决的核心问题。它是什么一句话把一条长 prompt 的 prefill 计算切成若干小块分散到多轮迭代里完成每轮只处理一部分 token并和正在进行的 decode 请求混在同一个 batch 里执行。它能做什么降低首 token 延迟TTFT的抖动、压低 TBT、提升 GPU 整体吞吐。适合谁所有在单卡或多卡上部署 vLLM 做在线推理、并且请求长度分布很不均匀的团队。如果你只是本地跑个 demo短 prompt 为主那默认配置基本够用。但只要你的服务里混着长文档、RAG 拼接上下文、代码补全这类长输入Chunked-Prefills 的调参就直接决定你的服务质量。这篇我会把机制讲清楚然后给出一套可复制的 vLLM 启动参数、分块配置项、请求验证脚本最后用 TaoToken 统一 Key 通道把多模型推理服务接起来方便你在同一套代码里切换模型做对比验证。先明确几个后面会反复用到的术语避免混淆。TTFT 是从发出请求到生成第一个 token 的时间主要反映 prefill 阶段性能。TBT 是连续两个输出 token 之间的间隔反映 decode 速度。TPOT 是所有输出 token 的平均生成时间也叫 ITL。整体延迟约等于 TTFT 加上 TPOT 乘以生成 token 数。Chunked-Prefills 优化的就是 TTFT 和 TBT 之间的那个矛盾prefill 想一次算完吞吐高decode 想尽快出字延迟低两者抢同一块 GPU。传统做法里prefill 是计算密集型矩阵乘矩阵算力利用率高但显存带宽用不满decode 是内存密集型每次只处理一个 token大部分时间花在读取 KV cache 和模型权重上算力大量闲置。把两者混在一个 batch 里prefill 就能搭便车用掉 decode 没吃满的算力decode 也能和 prefill 共享一次权重读取。这就是混合 batch 的收益来源也是 Chunked-Prefills 的立足点。2. Chunked-Prefills 机制拆解与 vLLM 里的关键参数要理解分块预填充得先看它替代了什么。最早的 Static Batching 是凑一批一起跑全跑完才换下一批最慢的请求拖死所有人。后来 Continuous BatchingOrca 提出的 iteration-level scheduling改成每轮迭代动态决定 batch 组成谁生成完 EOS 就立刻换新请求进来GPU 利用率大幅提升。但 Continuous Batching 有个隐患它按先来先服务拼 batch一个 batch 里塞进多少 prefill、多少 decode 完全不受控。如果某轮恰好进来几条长 prompt整个 batch 就变成 compute-bounddecode 请求被硬生生拖慢。Sarathi-Serve 的思路是给每轮迭代的 prefill token 数量设一个上限也就是 token budget在 vLLM 里对应max_num_batched_tokens。调度器优先安排正在进行的 decode每个只吃 1 个 token对延迟最敏感然后用剩余的 token 预算去填 prefill填满一个请求的 chunk 再填下一个预算用完就停。这样每轮迭代的计算量近似均衡decode 的 TBT 几乎不受输入 prompt 总长度影响。这里有个容易搞混的点chunk size 不是把 prompt 切成多大的块而是每轮迭代允许处理的最大 token 总数它包含 decode token 加 prefill token。比如max_num_batched_tokens2048某一轮可能是 3 个 decode 请求各 1 token加上一个 prefill 请求的 2045 个 token合计 2048。下一轮这个 prefill 请求继续吃剩下的部分。所以调这个参数本质是在调每轮迭代的延迟上限。分块带来的开销有两个。一是 chunk 变小后算术强度下降GPU 利用率降低prefill 整体变慢但实测开销通常控制在 1.25 倍以内。二是 Attention 的重复读取每个 chunk 做 Attention 时都要重新读之前所有 chunk 的 KV cache第一个 chunk 的 KV 会被读 N 次。不过 Attention 在整个前向里占比小线性层才是大头所以端到端影响有限。调 chunk size 时有个坑必须避开tile quantization。GPU 矩阵乘法按 tile常见 128分块如果维度刚好超过 tile 整数倍一点点比如从 256 到 257延迟可能从 69.8ms 跳到 92.33ms涨 32%。所以选max_num_batched_tokens时尽量让它和搭载的 decode token 数之和落在 tile size 的整数倍附近比如 2048、4096 这种。在 vLLM v1 里Chunked-Prefills 默认开启。核心调优参数就是max_num_batched_tokens设小如 2048能压低 TBT适合交互式场景设大如 16384能改善 TTFT适合批处理追求极致吞吐时小模型加大显存可以设到 8192 以上。下面这张表是我实测下来比较有用的对照。参数作用偏交互低 TBT偏吞吐高 QPSmax_num_batched_tokens每轮迭代 token 上限20488192~16384max_model_len单请求最大序列长度按业务设别虚高按业务设enable_chunked_prefill是否开启分块预填充默认 True默认 Truegpu_memory_utilization显存占用比例0.85~0.90.9~0.95需要提醒的是max_model_len设太大会白白吃掉 KV cache 显存。比如某些模型默认 131072光 KV cache 就要 16GiB 以上显存不够时 vLLM 会直接报错拒绝启动。按你真实的最长输入来设别图省事。3. 可复制的 vLLM 启动配置与 TaoToken 统一 Key 接入这一节给你能直接抄的配置。先装环境我用 uv 管理虚拟环境干净利落。# 安装 uv curl -LsSf https://astral.sh/uv/install.sh | sh source $HOME/.local/bin/env # 创建虚拟环境 uv venv vllm-demo --python 3.12 --seed source vllm-demo/bin/activate # 安装 vLLM 和 benchmark 依赖 uv pip install vllm pandas datasets启动一个带 Chunked-Prefills 调优的 OpenAI 兼容服务。注意--max-num-batched-tokens是核心--enable-chunked-prefill在 v1 默认开显式写上更清楚。python -m vllm.entrypoints.openai.api_server \ --model deepseek-ai/DeepSeek-R1-Distill-Llama-8B \ --served-model-name ds-r1-8b \ --host 0.0.0.0 \ --port 8000 \ --max-model-len 8192 \ --max-num-batched-tokens 2048 \ --gpu-memory-utilization 0.9 \ --enable-chunked-prefill服务起来后本地是http://127.0.0.1:8000/v1。但如果你要在一个客户端里同时对比多个模型、多个推理后端逐个改 base_url 和 key 很烦。这时候用 TaoToken 的统一 Key 通道就顺手了一个 API Key一套 OpenAI 兼容协议切换模型只改 model 字段。官网入口在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 。下面是一个 Python 客户端配置把本地 vLLM 和 TaoToken 通道放在一起对比。注意 Base URL、Key、Model ID 三件套要写全。import os from openai import OpenAI # 本地 vLLM 服务 local_client OpenAI( base_urlhttp://127.0.0.1:8000/v1, api_keyEMPTY, # vLLM 本地默认不校验 ) # TaoToken 统一 Key 通道 tao_client OpenAI( base_urlhttps://taotoken.net/api/v1, api_keyos.environ[TAOTOKEN_API_KEY], ) def ask(client, model, prompt, max_tokens256): resp client.chat.completions.create( modelmodel, messages[{role: user, content: prompt}], max_tokensmax_tokens, temperature0.2, ) return resp.choices[0].message.content if __name__ __main__: long_prompt 请总结下面这段技术文档的要点 分块预填充通过限制每轮迭代的 prefill token 数量来均衡计算负载。 * 200 print(本地 vLLM:, ask(local_client, ds-r1-8b, long_prompt)[:120]) print(TaoToken:, ask(tao_client, claude-sonnet-4-5, long_prompt)[:120])如果你用 Claude Code 这类工具做长上下文编码TaoToken 的接入配置可以写成 settings 片段。Base URL 填https://taotoken.net/apiKey 用你的统一 KeyModel ID 按需选。这样同一套配置能在不同模型间切换方便你验证 Chunked-Prefills 在不同模型上的表现差异。{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: 你的_TAOTOKEN_API_KEY, ANTHROPIC_MODEL: claude-sonnet-4-5 } }需要拿 Key 的话去控制台创建https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API Keys 管理页在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。4. 验证请求与吞吐/首 token 延迟对比实测配置写完必须验证不然你不知道分块到底有没有生效。先发一个最简单的请求确认服务活着。curl http://127.0.0.1:8000/v1/chat/completions \ -H Content-Type: application/json \ -d { model: ds-r1-8b, messages: [{role: user, content: 用一句话解释什么是分块预填充}], max_tokens: 64 }返回里能看到choices[0].message.content就说明通了。接下来做吞吐对比用 vLLM 自带的 benchmark 脚本。先下数据集。git clone https://github.com/vllm-project/vllm.git cd vllm/benchmark wget https://huggingface.co/datasets/anon8231489123/ShareGPT_Vicuna_unfiltered/resolve/main/ShareGPT_V3_unfiltered_cleaned_split.json跑两组对比一组max-num-batched-tokens2048一组 8192其他不变。# 小 chunk偏交互 python benchmark_throughput.py \ --backend vllm \ --model deepseek-ai/DeepSeek-R1-Distill-Llama-8B \ --dataset ShareGPT_V3_unfiltered_cleaned_split.json \ --max-model-len 8192 \ --max-num-batched-tokens 2048 \ --num-prompts 1000 # 大 chunk偏吞吐 python benchmark_throughput.py \ --backend vllm \ --model deepseek-ai/DeepSeek-R1-Distill-Llama-8B \ --dataset ShareGPT_V3_unfiltered_cleaned_split.json \ --max-model-len 8192 \ --max-num-batched-tokens 8192 \ --num-prompts 1000我实测下来2048 那组输出大概是Throughput: 4.85 requests/s, 2007.66 total tokens/s, 962.92 output tokens/s总 prompt token 21 万多输出 token 19 万多。8192 那组总吞吐会更高但如果你同时用脚本测 TBT会发现 2048 的 TBT 明显更稳。这就是权衡吞吐和延迟不可能同时拉满得按业务选。想单独测 TTFT 和 TBT可以写个带时间戳的脚本用流式接口记录首 token 到达时间和相邻 token 间隔。import time from openai import OpenAI client OpenAI(base_urlhttp://127.0.0.1:8000/v1, api_keyEMPTY) def measure(prompt, max_tokens128): start time.time() first_token_time None last_time None gaps [] stream client.chat.completions.create( modelds-r1-8b, messages[{role: user, content: prompt}], max_tokensmax_tokens, streamTrue, ) for chunk in stream: if chunk.choices[0].delta.content: now time.time() if first_token_time is None: first_token_time now else: gaps.append(now - last_time) last_time now ttft (first_token_time - start) * 1000 tbt sum(gaps) / len(gaps) * 1000 if gaps else 0 print(fTTFT{ttft:.1f}ms, 平均TBT{tbt:.1f}ms, token数{len(gaps)1}) long_prompt 请详细分析以下内容 分块预填充把长 prompt 拆成小块与 decode 混合调度。 * 300 measure(long_prompt)跑长 prompt 时你会看到 TTFT 因为分块而略微上升要分多轮才 prefill 完但 TBT 保持平稳不会像没开分块那样突然飙高。这正是 Chunked-Prefills 想要的效果牺牲一点点首 token 时间换来整体延迟的稳定。5. 常见报错排查401、local proxy failed 与 reading choices接入和压测过程中报错基本集中在几个地方。我按真实遇到的顺序列一下。401 Unauthorized。用 TaoToken 通道时最常见。先确认 Key 有没有带对前缀再确认 Base URL 是不是https://taotoken.net/api/v1注意结尾的/v1OpenAI SDK 会自己拼/chat/completions别重复。如果本地 vLLM 报 401检查是不是误传了真实 Key本地服务 api_key 填EMPTY即可。还有一种情况是环境变量没生效os.environ[TAOTOKEN_API_KEY]取到空字符串打印一下确认。local proxy failed / connection refused。这个通常是本地 vLLM 服务没起来或者端口被占。先curl http://127.0.0.1:8000/v1/models看服务在不在。如果启动时报显存不足把--gpu-memory-utilization降到 0.85或者调小--max-model-len。注意别把max_model_len设得比模型实际支持还大vLLM 会直接拒绝。reading choices 相关报错比如KeyError: choices或解析响应失败。这多半是返回体不是标准 OpenAI 格式或者请求被中间层拦截返回了 HTML 错误页。先打印原始resp看内容。如果是流式请求注意chunk.choices可能为空比如最后一个 usage chunk要加if chunk.choices判断。用 TaoToken 通道时确认 model 字段填的是平台支持的 Model ID填错会返回错误结构。OAuth / 认证类报错。如果你用 Claude Code 或类似工具报 OAuth 相关错误检查 settings 里的ANTHROPIC_BASE_URL和ANTHROPIC_API_KEY是否成对配置。Base URL 用https://taotoken.net/apiKey 用统一 KeyModel ID 写全。三件套缺一个都会认证失败。改完配置记得重启工具进程很多工具只在启动时读一次环境变量。分块没生效的排查。如果 TBT 还是抖先确认启动日志里有没有 chunked prefill 相关字样再确认max_num_batched_tokens是不是被设得过大等于没分块。另外如果单条 prompt 本身就比max_num_batched_tokens小那它一轮就 prefill 完了自然看不到分块效果得用超长 prompt 测。6. 把统一 Key 通道用起来多模型对比与长期编码调完参数最后一步是让这套东西真正好用。我的做法是本地 vLLM 跑主力模型做高频推理TaoToken 统一 Key 通道负责模型对比和长上下文任务。这样一套客户端代码改个 model 字段就能在本地模型和云端模型之间切换验证 Chunked-Prefills 在不同规模模型上的表现差异。如果你要长期做编码或 Agent 类任务可以考虑 Coding Plan把统一 Key 用在持续性的开发流程里https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。想直接对话验证模型效果用模型对话入口https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。Claude Code 的接入说明在 https://taotoken.net/claudecode?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。回到 Chunked-Prefills 本身我踩过最大的坑是盲目把max_num_batched_tokens往大调以为吞吐越高越好结果交互场景 TBT 抖得没法用。后来固定成 2048 起步再根据压测的 TBT 曲线微调找到那个既不违反延迟目标、又能吃满吞吐的点。另一个坑是忘了 tile 对齐2048 和 4096 这种值比 2000、3000 稳得多。你可以先按 2048 跑一轮 benchmark记录 TTFT 和 TBT再换 4096 跑一轮对比之后基本就有感觉了。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑