做推理基建久了,很多掘友应该听过这套说法:上vLLM,打开PagedAttention,显存碎片直接原地消失,GPU利用率直接拉满起飞。最开始我也是被这套宣传洗脑,压测环境跑出来数据漂漂亮亮,感觉这下可以把显存吃干榨尽,结果上生产跑个一两天,就开始遇到各种邪门现象。
理想环境和真实业务中间隔了一道鸿沟
先说原理,PagedAttention借鉴操作系统虚拟内存思路,把整块KV‑Cache切为固定大小的gpu block,不再给每个请求分配连续显存块,依靠block table做间接寻址,以此解决传统推理框架外部显存碎片化问题。
在基准测试集ShareGPT里面效果确实亮眼,外部碎片几乎清零,很多博客文章讲到这里就戛然而止,很少往下聊**内部碎片(internal fragmentation)**这个坑。
简单白话解释一下:block_size一般默认16 token。假如一个对话生成只输出10个token,依然要占完整一个block,剩下6个token位置直接空着,这块显存不能借给别的请求使用。这就是内部碎片,每个请求尾巴都会吃掉一部分闲置显存空间。
测试集里面大多是长会话样本,单请求token数很大,单块内闲置占比很低,碎片问题几乎看不见。一旦业务场景大量短query,比如RAG抽取、工具调用、短问答,成千上万并发请求堆上来,零零碎碎的尾部空位就会不断累积。外部碎片消灭了,但内部碎片悄悄蚕食你的VRAM。
线上会出现哪些迷惑性故障现象
最折磨人的就是那种渐进式恶化,刚重启实例一切正常,吞吐、TTFT都很漂亮,跑大半天之后慢慢变坏,并不是一上来直接OOM炸服务。
- 显卡看
nvidia‑smi显存占用还没顶满,但是block池已经快要耗尽,调度器被迫触发preemption抢占,把正在运行的请求踢掉做重计算recompute。业务侧看到就是P99延迟随机飙升,普通请求时不时卡顿一下,日志翻一遍又看不到明确OOM报错,排查很容易走偏,以为是带宽或者CUDA kernel问题。 - 盲目调大
gpu_memory_utilization参数,直接拉到0.95。很多新手觉得这个值越高越好。这个参数是预留给KV block池的显存占比,没有预留足够空间给prefill峰值、NCCL通信buffer以及中间activation张量。遇到一波长prompt突刺流量,瞬间直接爆OOM实例crash,整套推理服务直接重启。一般生产环境我习惯先从0.85起步,压测充分之后再慢慢往上试探,不要直接抄网上示例配置。 - 调大block_size试图减少block管理开销,结果适得其反。block越大,每一块里面的内部碎片就越可观。短请求占比高的业务,盲目开大block_size等于变相持续浪费显存资源。
这里插一句黑话,很多人压测只看稳态吞吐量,忽略碎片带来的隐性损耗。压测脚本都是理想化流量,现实业务是长短请求大杂烩,这就是本地压测和生产环境之间最大的一道代沟。
可以落地的几手调优手段,没有银弹
- 业务侧尽量统计真实请求序列长度分布,搞清楚你的业务到底长会话多还是短query占大头。如果短请求占比很高,就要对内部碎片留余量评估,不要拿论文指标直接当做上线依据。
- 监控不要只盯着GPU显存使用率,务必把vLLM暴露出来的metrics拉起来:
vllm_num_gpu_blocks_used、vllm_preemption_count。preemption计数持续上涨是非常重要告警信号,比单纯看显存百分比更有参考价值。一旦这个指标疯狂涨,说明block池已经吃紧,要么降并发上限,要么扩容显存资源。 - 开启
chunked‑prefill,避免个别超长prefill请求垄断GPU资源,把其他请求全部堵在队列里面。但也要留意,这个特性会引入调度逻辑开销,上线前要做对比压测。 - prefix‑caching可以开启,利用公共system prompt做KV块复用,降低新请求prefill开销,但也要注意引用计数ref‑cnt异常泄漏的潜在风险,版本迭代时不时踩过小坑。
这里要澄清,不是说PagedAttention不行,恰恰相反它是目前工业界非常靠谱的方案。只是网上很多教程把它塑造成解决一切显存问题的万能解药。它解决的主要是外部碎片,内部碎片问题依然客观存在,需要工程师自己去做权衡取舍。
做推理基建久了越来越有体会,很多组件都没有完美版本,论文、开源demo给你的往往是最优case,我们做工程就是不断和各种边角case博弈。
有时候线上出问题,排查半天,最后只是各种细碎损耗叠加出来的结果,并不是某个重大bug。重启实例可以临时缓解碎片带来的症状,但重启属于治标不治本,不能当做常态化解决方案。
平时我排障的时候,一般先看metrics,再看流量分布,再怀疑组件bug,顺序别搞反。
欢迎评论聊聊你们线上vLLM踩过哪些稀奇古怪坑。