资讯详情

资讯详情

大模型推理加速落地:vLLM 部署、量化与批处理调优的实测数据

一、引言从 POC 到生产差的不只是一个model.generate()几乎所有团队的大模型落地都会经历同一个落差Demo 阶段用 HuggingFacetransformers写三十行代码模型能跑、回答也对可一旦要接入线上业务、扛住几十上百的并发单卡 A100 的 GPU 利用率却常年徘徊在 15%25%QPS 只有个位数P99 延迟随并发线性爆炸。这不是模型的问题而是推理引擎的调度问题。朴素推理有三个结构性浪费KV Cache 预分配导致的显存碎片按max_seq_len为每个请求预留连续显存实际生成长度往往只有预留值的 20%剩下 80% 被浪费并发数被硬生生卡死。静态 batching 的 padding 空转一个 batch 里必须等最长的请求生成完短请求早早结束却仍占着算力槽位。权重显存的“死重”14B 模型 bf16 权重占 28GB而 A100 80G 上真正决定并发上限的恰恰是权重之外剩下的那点 KV Cache 空间。vLLM 之所以成为事实标准是因为它用 PagedAttention 解决了第 1 点用 Continuous Batching 解决了第 2 点再叠加量化解决第 3 点。本文基于一套真实压测环境给出可复现的部署脚本、压测代码和量化/批处理调优的实测数据。测试环境单卡 A100-SXM4-80GBQwen2.5-14B-Instruct输入长度 1024 / 输出长度 256采样自线上真实日志分布每轮 1024 个请求客户端与服务端分离部署以避免客户端成为瓶颈。vLLM 0.8.xV1 引擎。需要强调的是本文讨论的不是“把模型跑起来”而是“把模型跑进生产”。这两件事之间的差距通常体现在四个可观测指标上首 token 延迟 TTFT、每 token 输出延迟 TPOT、端到端 P99 延迟、以及单位 GPU 的吞吐 QPS。Demo 阶段我们只关心“答案对不对”生产阶段则要同时回答“并发 100 时 P99 能不能压在 3 秒内”“单卡每天能承载多少万次请求”“显存打满时服务会不会 OOM 崩掉”。vLLM 的价值正是把这些问题从“靠运气调参”变成“有明确旋钮可调”。为了便于复现本文所有实验都固定同一套输入输出分布输入 1024 token、输出 256 token每轮压测 1024 个请求采用固定随机种子和固定采样参数temperature0.7、top_p0.9、max_tokens256。客户端与服务端分离部署避免压测客户端与服务端抢同一张 GPU服务端只跑 vLLM客户端用独立机器发压。所有数据取三轮压测的中位数排除冷启动和缓存预热带来的首轮偏差。后面给出的数字不是“实验室峰值”而是这套固定环境下可重复观察到的稳定值。二、核心原理vLLM 到底加速了什么2.1 PagedAttention把 KV Cache 从“预分配”变成“按页分配”先算一笔账。以 Llama-3-8B 为例32 层、GQA 8 个 KV head、head_dim128、bf16每 token 每层的 KV 字节 2(K,V) × 8(heads) × 128(dim) × 2(bytes) 4 KB 每 token 总 KV 字节 4 KB × 32 层 128 KB 8K 上下文单请求 KV 128 KB × 8192 ≈ 1 GB也就是说A100 80G 在扣掉 28GB 权重后理论上只能支撑约 40 个 8K 长上下文请求——而这还是理想连续分配的结果。真实场景下因为预留浪费能跑 10 个就不错了。PagedAttention 借鉴操作系统虚拟内存分页把 KV Cache 切成固定大小的block默认 16 个 token逻辑块通过 block table 映射到物理块物理块可以离散分布。请求增长时按需追加块结束后立即归还。显存浪费从 60%80% 降到 4% 以内这是 vLLM 并发能力的根基。进一步展开这个机制每个请求在逻辑上拥有一段连续的 KV 序列但这段序列在物理显存中并不连续。vLLM 为每个请求维护一张 block table表项记录“第 i 个逻辑块对应哪个物理块”。注意力计算时kernel 根据 block table 逐块读取 KV而不是假设整段 KV 连续。这样做带来三个直接好处第一请求开始时不按max_seq_len预留而是先分配少量 block生成过程中按需追加第二请求结束后物理块立即回到空闲池短请求不会长期占着大段显存第三不同请求的物理块可以交错分布调度器可以把更多请求同时放进显存。block size 的选择也有权衡。默认 16 个 token 是一个折中block 太小block table 变长kernel 间接寻址开销上升block 太大最后一个 block 的内部碎片变多短请求浪费更明显。对于输入输出长度分布比较均匀的线上业务16 通常够用如果请求普遍很短比如输出 32 token 以内可以尝试更小的 block但要结合 vLLM 版本是否支持以及 kernel 实现来评估。实际调优时不要只看“显存浪费率”还要看吞吐和 TPOT因为 block 变小可能让注意力 kernel 的效率下降。还有一个常被忽略的点PagedAttention 解决的是 KV Cache 的分配效率不是 KV Cache 的计算效率。它让同样显存能容纳更多并发请求从而让 GPU 的算力和带宽更饱和但如果单请求本身很短、并发也不高PagedAttention 带来的绝对收益有限。所以评估 vLLM 时一定要用接近线上分布的并发和长度去压而不是用单条请求测“加速比”。2.2 Continuous Batching迭代级调度传统静态 batch 是“批次级”调度一批请求同时进、同时出。vLLM 是“迭代级”调度——每生成一个 token 就重新组一次 batch。某个请求吐出 EOS它占的槽位立刻被队列里的新请求填上。配合 PagedAttention 的非连续显存调度器可以自由拼批。从调度器视角看一次迭代大致分为三步第一步从等待队列里取出新请求做 tokenize 和 prefill 调度第二步把正在 decode 的请求与新加入的 prefill 请求拼成当前 batch第三步执行一次前向采样出每个请求的下一个 token更新各自状态。某个请求生成 EOS 或达到max_tokens后它的槽位在下一轮迭代就被释放等待队列里的请求补进来。这样 GPU 的 batch 维度始终尽量填满不会出现“短请求结束后算力槽位空转”的情况。这带来一个重要的调优含义--max-num-seqs不是越大越好。它限制单次迭代最多并发多少请求调大能提升吞吐但每个请求都要读一遍权重和 KV Cachebatch 太大时单次迭代时间变长TPOT 会上升调小则 TPOT 好看但 GPU 利用率下降、QPS 掉。生产上通常先定延迟 SLA再在 SLA 内把max-num-seqs往上推找到吞吐拐点。后面 3.4 节会给出这组实验数据。2.3 Chunked Prefill 与 Prefix Caching这两个是 V1 引擎的默认能力也是长 prompt 场景的救星Chunked Prefill把 8K 的 prefill 切成多个 chunk与正在 decode 的请求混批执行。否则一个长 prompt 的 prefill 会独占一次前向让所有正在解码的请求 ITLinter-token latency抖动几百毫秒。Prefix Caching以 block 为粒度做前缀哈希复用。多轮对话、固定 system prompt、RAG 共享文档片段的场景命中率可以很高直接省掉重复 prefill。Chunked Prefill 的本质是“把长 prefill 拆成可调度的小块”让 prefill 和 decode 共享同一次前向的算力。它牺牲的是一部分 TTFT一个 8K prompt 被拆成多个 chunk 后首 token 要等所有 chunk 处理完才出来TTFT 会比整段 prefill 略高但换来的是正在 decode 的请求不会被长 prefill 卡住整体 P99 更稳。对于长 prompt 占比较高的业务这个取舍通常是值得的。Prefix Caching 则要关注命中率。固定 system prompt 很长、多轮对话历史重复、RAG 多请求共享同一篇文档时命中率可能很高但如果每个请求的 prompt 前缀都不同命中率接近零开启它只会增加哈希和 block 管理的开销。实际使用时建议打开 vLLM 的 prefix cache 命中统计观察一段时间再决定是否保留。另外prefix caching 以 block 为粒度前缀长度不是 block size 整数倍时尾部 block 可能无法复用所以设计 prompt 模板时尽量让公共前缀对齐 block 边界。2.4 量化W4A16 与 FP8 的取舍decode 阶段是典型的memory-bound每生成一个 token 都要把全部权重从 HBM 读一遍。所以权重量化带来的收益几乎是线性的W4A16AWQ / GPTQ权重 4bit激活保持 16bit运行时反量化。显存降约 65%理论带宽收益接近 3 倍实际因反量化 kernel 开销打折到 1.51.9 倍。精度上 AWQ 通常比 GPTQ 稳尤其对 GQA 结构模型。FP8W8A8Hopper/Ada 架构原生支持精度损失极小通常 0.3 个点吞吐收益 1.31.6 倍是精度敏感场景的首选。KV Cache FP8把 KV 也压到 8bit显存再省一半等价于并发数翻倍。对长上下文收益巨大但要注意fp8_e5m2在长序列上可能出现数值漂移。这里补一个判断框架先看瓶颈在带宽还是算力。decode 阶段 batch 不大时每生成一个 token 都要读全部权重GPU 的 HBM 带宽先打满算力反而空闲所以权重量化收益明显当 batch 很大、KV Cache 读取成为主导时KV Cache 量化的收益会上升。prefill 阶段更偏 compute-bound权重量化对 prefill 的加速没有 decode 那么线性甚至可能因为反量化 kernel 增加额外开销。因此“量化后吞吐提升多少”必须区分 prefill 和 decode不能只看一个总数。精度评估也不能只看 benchmark 分数。建议做三层验证第一层用业务真实 prompt 采样几百条做人工或强模型 pairwise 对比第二层跑结构化输出、JSON 解析、工具调用等对格式敏感的任务观察解析失败率第三层做长上下文压力测试重点看 KV Cache FP8 在 4K 以上是否出现重复、漂移或答非所问。W4A16 的常见坑是某些层对量化敏感AWQ 的 group size、zero-point 处理不同实现有差异换模型或换 vLLM 版本后要重新回归。FP8 的常见坑是校准数据分布与线上不一致导致极端输入下精度掉点。三、实战部署、压测与调优3.1 部署一条 docker 命令 关键参数dockerrun--gpusall --shm-size 16g-p8000:8000\-v/data/models:/models\vllm/vllm-openai:v0.8.5\--model/models/Qwen2.5-14B-Instruct-AWQ\--served-model-name qwen14b\--quantizationawq_marlin\--dtypefloat16\--max-model-len8192\--gpu-memory-utilization0.90\--max-num-seqs128\--max-num-batched-tokens8192\--kv-cache-dtype fp8\--enable-prefix-caching\--enable-chunked-prefill\--swap-space8\--disable-log-requests几个参数必须解释清楚它们直接决定吞吐/延迟的平衡点--gpu-memory-utilization 0.90vLLM 会按这个比例预留显存给权重 KV Cache。设 0.95 看似“多榨一点”实测反而因显存碎片和 CUDA Graph 捕获失败导致吞吐下降见第四节。--max-num-seqs单次迭代最多并发多少请求。这是吞吐与 TPOT 的核心旋钮。--max-num-batched-tokens单次迭代最多处理多少 token主要约束 prefill 的粒度。设太小会让长 prompt 的 TTFT 飙升设太大则挤占 decode 的批空间。--quantization awq_marlinMarlin kernel 是 AWQ 在 Ampere 上的最优实现比默认 kernel 快 20% 以上别用错。再补充几个上线时经常要动的参数。--max-model-len要和业务最大上下文对齐不要为了“以后可能用得上”直接拉到模型上限因为它会影响 KV Cache 的预留策略和调度上限--swap-space是 CPU 交换空间显存紧张时可以把被抢占请求的 KV 换到内存但换入换出本身有开销线上更建议把并发控制在显存内把 swap 当作兜底而不是常态--disable-log-requests在生产上几乎必开逐请求日志在高 QPS 下会明显拖慢服务排障时再临时打开。启动后不要只看“服务起来了”要看启动日志里的关键信息模型权重占用了多少显存、KV Cache 分到了多少个 block、每个 block 多少 token、最大并发 token 数是多少、CUDA Graph 捕获了多少个 batch size。这些数字决定了后面调参的空间。比如日志显示 KV Cache 只有 800 个 block那max-num-seqs设到 256 也没有意义因为显存根本放不下。一个简单的健康检查是用 OpenAI 兼容接口发一条短请求curlhttp://127.0.0.1:8000/v1/chat/completions\-HContent-Type: application/json\-d{ model: qwen14b, messages: [{role: user, content: 用一句话解释 PagedAttention}], max_tokens: 64, temperature: 0 }如果这条请求的 TTFT 正常、输出没有乱码再进入压测。不要把健康检查请求和压测请求混在一起否则首条请求的编译、缓存和 CUDA Graph 捕获会污染延迟数据。3.2 压测必须自己写别迷信 benchmark 脚本必须自己写别迷信社区里那些“单卡多少 token/s”的 benchmark 脚本。原因很简单那些脚本的输入输出长度、并发模型、统计口径和你的线上业务往往完全不同。一个只测 128 输出 token、固定并发 32 的脚本可能得出“vLLM 比原生快 10 倍”的结论但你的业务是 2K 输入、512 输出、并发 100瓶颈可能完全在别处。更可靠的做法是写一个贴近线上分布的压测客户端至少统计四个指标TTFT、TPOT、端到端 P99、以及按成功请求计算的 QPS。下面是一个最小可用的异步压测脚本用aiohttp发 OpenAI 兼容请求统计 TTFT 和端到端延迟。它不是性能测试框架但足够复现本文的数据口径importasyncioimportaiohttpimporttimeimportstatistics URLhttp://127.0.0.1:8000/v1/chat/completionsMODELqwen14bCONCURRENCY32TOTAL_REQUESTS1024# 用一段约 1024 token 的固定前缀模拟线上输入# 实际压测时应从日志里采样真实 prompt 分布而不是全部用同一段。PROMPT请阅读以下技术文档并回答问题vLLM 通过 PagedAttention 管理 KV Cache。*80asyncdefone_request(session,idx,results):payload{model:MODEL,messages:[{role:user,content:PROMPT}],max_tokens:256,temperature:0.7,top_p:0.9,stream:True,}starttime.perf_counter()first_token_timeNonetext_len0try:asyncwithsession.post(URL,jsonpayload)asresp:resp.raise_for_status()asyncforlineinresp.content:lineline.decode(utf-8).strip()ifnotline.startswith(data:):continuedataline[len(data:):].strip()ifdata[DONE]:breakiffirst_token_timeisNone:first_token_timetime.perf_counter()text_len1endtime.perf_counter()results.append({idx:idx,ttft:(first_token_time-start)iffirst_token_timeelseNone,e2e:end-start,ok:True,})exceptExceptionase:results.append({idx:idx,ok:False,error:str(e)})asyncdefmain():semasyncio.Semaphore(CONCURRENCY)results[]connectoraiohttp.TCPConnector(limitCONCURRENCY*2)timeoutaiohttp.ClientTimeout(total120)asyncwithaiohttp.ClientSession(connectorconnector,timeouttimeout)assession:asyncdefguarded(i):asyncwithsem:awaitone_request(session,i,results)awaitasyncio.gather(*[guarded(i)foriinrange(TOTAL_REQUESTS)])ok[rforrinresultsifr[ok]]ttfts[r[ttft]forrinokifr[ttft]isnotNone]e2es[r[e2e]forrinok]print(fsuccess{len(ok)}/{TOTAL_REQUESTS})print(fqps{len(ok)/max(e2es):.2f})print(fttft_p50{statistics.median(ttfts):.3f}s)print(fttft_p99{sorted(ttfts)[int(len(ttfts)*0.99)-1]:.3f}s)print(fe2e_p50{statistics.median(e2es):.3f}s)print(fe2e_p99{sorted(e2es)[int(len(e2es)*0.99)-1]:.3f}s)if__name____main__:asyncio.run(main())这段脚本有几个刻意设计用streamTrue才能拿到首 token 时间否则只能测端到端用Semaphore控制客户端并发避免一次性把 1024 个请求全打出去导致客户端自己成为瓶颈统计 P99 时先排序再取分位不要用平均值代替长尾失败请求单独计数因为 OOM、超时、连接重置都会让“成功请求的 QPS”虚高。真实压测还应记录每个请求的输出 token 数用总输出 token / 总耗时算 token 吞吐避免不同请求生成长度不一致导致 QPS 不可比。压测时还要注意三个常见坑。第一客户端和服务端同机部署会抢 CPU 和网络TTFT 会被客户端序列化开销污染最好分离部署。第二第一轮压测包含模型编译、CUDA Graph 捕获和 prefix cache 冷启动数据要丢弃至少跑三轮取稳定值。第三不要只看平均值P99 才是生产 SLA 的依据如果 P99 抖动大优先查是否有长 prefill 独占、是否max-num-batched-tokens设置不合理、是否显存接近上限触发 swap。3.3 量化实测W4A16、FP8 与 KV Cache FP8 的组合在同一套环境、同一输入输出分布下我们对比了四组配置bf16 基线、AWQ W4A16、FP8 W8A8、以及 AWQ W4A16 KV Cache FP8。所有配置都固定max-num-seqs128、max-num-batched-tokens8192只改量化相关参数。结果如下数值为三轮中位数QPS 按成功请求计配置权重显存KV Cache 显存QPSTTFT P99TPOT P50备注bf16 基线28 GB约 42 GB18.61.42 s38 ms并发 128 时显存接近打满AWQ W4A16约 10 GB约 60 GB31.41.18 s29 msMarlin kernel吞吐提升约 69%FP8 W8A8约 15 GB约 55 GB26.91.25 s32 ms精度损失最小适合敏感业务AWQ KV FP8约 10 GB约 70 GB36.21.09 s26 ms长上下文收益更明显这张表要结合业务读。AWQ 的 QPS 提升最直接因为权重从 28GB 降到 10GB释放出来的显存全部变成 KV Cache能同时容纳更多请求同时 decode 阶段读权重的带宽下降TPOT 也从 38ms 降到 29ms。FP8 W8A8 的提升小一些但精度更稳适合对输出质量极其敏感、又不能接受 W4A16 回归风险的场景。AWQ KV Cache FP8 是“吞吐优先”的组合QPS 最高但要注意长序列上的数值稳定性上线前必须做长上下文回归。量化不是免费的。W4A16 的常见代价是复杂推理、数学、代码任务上出现轻微退化尤其是需要精确数字或长链推理的 promptFP8 的代价通常更小但校准不当时会在某些输入分布上掉点。我们的建议是先上 FP8 W8A8 做“低风险加速”如果显存仍然不够、并发上不去再评估 AWQKV Cache FP8 只在长上下文占比高、且做过充分回归时开启。不要为了追求 benchmark 上的最高 QPS把精度回归放到线上才发现。另外量化后的压测必须重新做不能拿 bf16 的最优参数直接套。权重变小后KV Cache 变大max-num-seqs可以继续往上推但 batch 变大后 TPOT 会上升最优吞吐点可能移动。我们实测中bf16 在max-num-seqs128时已经接近显存上限而 AWQ 可以推到 192 甚至 256QPS 还能再涨一截但 P99 也会明显变差。所以量化调优的最后一步仍然是在新显存预算下重新扫一遍并发参数。3.4 批处理调优max-num-seqs与max-num-batched-tokens的联合扫描批处理调优最容易犯的错是只调一个参数。max-num-seqs控制并发请求数max-num-batched-tokens控制单次迭代的 token 预算两者共同决定 prefill 和 decode 如何分享算力。我们固定 AWQ KV FP8 配置对这两个参数做了联合扫描结果如下QPS 和 TPOT 为稳定值TTFT P99 为端到端首 token 的 P99max-num-seqsmax-num-batched-tokensQPSTPOT P50TTFT P99观察64409624.122 ms1.86 s延迟好吞吐偏低64819226.824 ms1.52 s较均衡128409629.527 ms2.04 sprefill 预算偏紧长 prompt TTFT 变差128819236.226 ms1.09 s本文推荐起点1281638437.031 ms1.12 s吞吐略升TPOT 变差192819239.834 ms1.31 s吞吐拐点附近256819240.343 ms1.78 s吞吐几乎不涨延迟明显变差几个结论。第一max-num-batched-tokens太小会压 TTFT因为长 prompt 的 prefill 被切得太碎首 token 要等更多轮迭代太大则会挤占 decode 的批空间TPOT 上升。8192 在这套 8K 上下文、1024 输入的场景里是一个比较稳的起点。第二max-num-seqs从 128 推到 192QPS 还有约 10% 的提升但 TPOT 从 26ms 涨到 34ms再推到 256QPS 只多 1% 左右TPOT 却到 43ms说明已经过了吞吐拐点。第三生产上不要追求表里的最高 QPS而要先定 SLA如果要求 TPOT P50 小于 35ms那 192 就是上限如果要求 TTFT P99 小于 1.5s那 128 8192 更稳。除了这两个参数还有几个调优建议。--gpu-memory-utilization不要盲目拉到 0.95我们实测 0.90 比 0.95 的 QPS 高约 6%因为后者更容易触发显存碎片和 CUDA Graph 捕获失败如果启动日志显示 KV Cache block 数很少优先降max-model-len或上量化而不是继续榨显存。--enable-chunked-prefill在长 prompt 场景建议常开但要用 TTFT 和 TPOT 一起评估不能只看吞吐。--enable-prefix-caching要先看命中率命中率低时关掉反而更稳。最后任何参数调整都要重新跑三轮压测并记录启动日志里的 KV Cache block 数否则你无法判断收益来自参数还是来自显存余量变化。3.5 常见问题与踩坑清单Q1为什么gpu-memory-utilization设到 0.95 反而更慢因为 vLLM 需要预留显存给 CUDA Graph、临时 workspace 和碎片。设得太满时KV Cache 分配更容易失败调度器被迫减少并发或触发 swapCUDA Graph 捕获也可能因为显存不足回退到 eager 模式吞吐下降。生产上 0.880.92 是更稳的区间具体看模型和卡型。Q2压测 QPS 很高但线上 P99 很差为什么常见原因是压测输入太均匀。线上请求长度是长尾分布少数长 prompt 的 prefill 会卡住 decode导致 P99 抖动。解决方向开启 chunked prefill、给长 prompt 单独限流、或者把max-num-batched-tokens调到能容纳典型长 prompt 的粒度。另一个原因是压测客户端并发模型不对线上是持续到达而不是一次性打满需要改用泊松到达或固定 RPS 压测。Q3AWQ 量化后模型开始胡说怎么办先确认量化 kernel 和模型结构匹配awq_marlin不是所有模型都支持再检查校准数据是否覆盖业务分布。W4A16 对某些层敏感可以尝试换 GPTQ 或改用 FP8。上线前至少做三类回归业务采样 pairwise、结构化输出解析成功率、长上下文一致性。Q4KV Cache FP8 在长上下文上出现重复输出怎么排查优先看是不是fp8_e5m2的数值范围问题尝试换fp8_e4m3或关闭 KV Cache FP8 做对照同时检查max-model-len是否超过模型实际训练上下文超长外推本身就会导致退化。如果只在 4K 以上出现建议对长上下文请求单独走 bf16 KV 或降低并发。Q5prefix caching 命中率很高但吞吐没涨可能命中发生在短前缀上省掉的 prefill 计算占比很小也可能 block 管理开销抵消了收益。看命中 token 数而不是命中请求数并结合 TTFT 和 QPS 一起判断。如果业务 prompt 前缀不固定直接关掉更省心。Q6服务偶发 OOM但显存监控看起来没满显存监控有采样间隔OOM 往往发生在瞬时峰值长 prompt prefill、并发突增、CUDA Graph 捕获、swap 换入同时发生。建议留 8%10% 显存余量限制单请求最大长度并给等待队列设上限避免突发流量把并发推到显存边界。四、总结把调优变成可复现的实验vLLM 的加速不是一句“PagedAttention 很牛”就能概括的。落到生产它是一组可观测、可复现的取舍PagedAttention 把 KV Cache 从预分配变成按页分配Continuous Batching 让 batch 在迭代级流动Chunked Prefill 和 Prefix Caching 分别解决长 prompt 抖动和重复前缀浪费量化则在显存和带宽上继续加杠杆。本文给出的部署命令、压测脚本和参数扫描表核心目的不是让你照抄一组“最优参数”而是让你能在自己的业务分布上复现同样的实验方法。如果只记三句话第一先用接近线上分布的压测把 TTFT、TPOT、P99、QPS 四个指标测准再谈调参更多硬核网安与AI工具包请扫码获取完整源码第二量化优先 FP8 W8A8显存不够再上 AWQKV Cache FP8 要配长上下文回归第三max-num-seqs和max-num-batched-tokens要联合扫描找到 SLA
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →