1. 项目概述:这不是又一篇“LLM部署教程”,而是一份压箱底的生产环境 checklist
“大语言模型生产环境指南(六)”——看到这个标题,你大概率会下意识划走:又是那种讲讲 Docker、K8s、vLLM 的泛泛而谈?讲讲量化、推理加速、API 封装的“标准答案”?抱歉,这篇不是。它不教你怎么把 Llama-3-70B 跑起来,而是专门解决你把模型跑起来之后,第二天凌晨三点被 PagerDuty 报警电话叫醒时,手抖着连不上 GPU 监控面板、查不到日志、搞不清是模型 OOM 还是 Prometheus 指标采集崩了、更不知道该先 kill 哪个 pod 的那种真实困境。
我过去三年带过 7 个 LLM 生产化落地项目,从金融风控问答引擎到制造业设备故障诊断助手,最小部署规模是单卡 A10,最大是 32 卡 H100 集群。踩过的坑里,有 63% 发生在模型“能跑”之后——不是不能用,而是用着用着就掉链子:响应延迟从 800ms 慢慢爬到 4.2s,QPS 在无明显流量增长下持续下跌,GPU 显存占用曲线像心电图一样忽高忽低,或者某天早上发现所有历史对话记录莫名丢失。这些都不是模型能力问题,而是生产环境的系统性失稳。本篇聚焦的就是这套“失稳”的底层逻辑、可观测路径和防御性设计原则。它不替代 vLLM 文档,但能让你在读完 vLLM 文档后,真正知道哪些参数必须改、哪些监控必须加、哪些日志字段必须埋点、哪些告警阈值必须调低——因为它们直接对应着你运维台面上那个不断闪烁的红色告警灯。适合所有已经把模型跑通、正准备接入真实业务流量的工程师、MLOps 工程师、技术负责人,以及那些被老板问“为什么线上效果不如测试环境”而哑口无言的算法同学。
2. 内容整体设计与思路拆解:为什么“第六篇”才讲这些?
2.1 为什么不是“第一篇”?——生产环境的“三重门”认知陷阱
很多团队一上来就猛攻“怎么让模型更快”,这是典型的认知错位。我把 LLM 生产化过程比作闯三重门:
第一重门:功能门(Functional Gate)
核心问题是:“模型能不能回答这个问题?” 这阶段关注 prompt 工程、RAG 构建、微调数据质量。90% 的开源教程停在这里,因为它最“性感”,有立竿见影的效果。第二重门:性能门(Performance Gate)
核心问题是:“模型能不能在 1 秒内回答这个问题?” 这阶段关注推理框架选型(vLLM vs TGI vs Triton)、CUDA 内核优化、KV Cache 管理、批处理策略。这也是当前社区最热的讨论区。第三重门:稳定门(Stability Gate)
核心问题是:“模型能不能连续 72 小时,在每秒 50 个请求、峰值 200 QPS、混合长/短文本、偶发恶意输入的条件下,保持 P99 延迟 < 1.2s,错误率 < 0.3%,且所有请求上下文不丢失、不串扰?”
这就是本篇要攻克的门。它不炫技,但决定生死。而“第六篇”才讲,恰恰是因为——只有跨过前两道门的人,才会真正痛感第三道门的重量。没在凌晨三点修过模型内存泄漏的人,不会理解为什么一个--max-num-seqs参数要精确到个位数;没经历过因日志轮转配置错误导致磁盘爆满继而服务雪崩的人,不会明白结构化日志字段设计的重要性。
2.2 本篇的设计锚点:从“故障树”反推防御体系
我们不从工具列表开始,而是从一份真实的故障树(Fault Tree)出发。过去一年,我团队记录的 137 起 LLM 服务 P1/P2 级别故障中,高频根因分布如下:
| 故障大类 | 占比 | 典型表现 | 关键诱因 |
|---|---|---|---|
| 资源耗尽型 | 38% | GPU 显存 OOM、CPU 负载 100%、磁盘 I/O 阻塞 | 批处理大小未限流、长文本未截断、日志未轮转、监控 agent 本身吃资源 |
| 状态紊乱型 | 29% | 对话上下文丢失、多轮对话串号、KV Cache 错乱 | 异步处理未加锁、HTTP 连接复用未隔离、分布式缓存 key 设计缺陷 |
| 依赖脆弱型 | 18% | RAG 检索超时、外部 API 响应慢拖垮主链路、向量库连接池耗尽 | 未设熔断、无降级策略、依赖服务无健康检查 |
| 可观测盲区型 | 15% | 故障发生后无法定位、日志无关键 trace ID、指标缺失维度 | 日志未打标、Prometheus exporter 未覆盖关键指标、链路追踪未透传 |
你看,没有一个是“模型不准”。全是工程细节。因此,本篇所有内容都围绕这四类根因展开,每一项建议、每一个参数、每一行代码,都对应着故障树上的一个叶子节点。比如,当你看到后面讲--max-model-len必须严格小于 GPU 显存理论容量的 85%,那不是为了“理论最优”,而是因为——我们有 3 次故障,根因都是这个参数设为 4096,而实际运行中某条 3800 token 的用户输入触发了显存碎片化,最终在第 17 个请求时 OOM。
2.3 为什么强调“指南”而非“教程”?——生产环境没有银弹
“指南”意味着决策框架,而非操作手册。“教程”告诉你pip install vllm然后python -m vllm.entrypoints.api_server;“指南”则要问你:
- 你的业务 SLA 是什么?P99 延迟容忍 1s 还是 3s?这直接决定你能否启用
--enable-chunked-prefill; - 你的流量峰谷比是多少?是平稳的 100 QPS,还是早 9 点突增到 800 QPS?这决定你是否需要动态扩缩容,以及扩缩容的触发指标是 CPU 还是 vLLM 自身的
num_requests_waiting; - 你的用户输入长度分布如何?P95 是 512 token 还是 2048 token?这决定你
--max-num-batched-tokens的安全上限,而不是盲目套用文档里的 4096。
我见过太多团队,把 GitHub README 里的默认参数原封不动搬到生产环境,结果上线三天就跪。不是 vLLM 不好,是你没把它当成一个需要深度定制的业务中间件,而只当成了一个“黑盒推理命令”。本篇的核心思想,就是帮你建立这种“中间件思维”:它和你数据库连接池、HTTP 网关、消息队列一样,需要根据你的业务特征做精细化调优和防护加固。
3. 核心细节解析与实操要点:那些文档里不会写的“血泪参数”
3.1 推理服务启动参数:每个 flag 都是防御工事
vLLM 的启动参数多达 50+,但生产环境真正需要你逐字审阅的,不超过 12 个。它们不是性能开关,而是稳定性保险丝。下面逐个拆解,附上我们在线上环境的真实取值和 rationale。
3.1.1--max-model-len:显存的“安全水位线”
- 文档说法:“模型支持的最大上下文长度。”
- 生产真相:这是你 GPU 显存的“安全水位线”。设得过高,显存碎片化风险指数级上升;设得过低,长文本直接被截断,用户体验崩坏。
- 我们的实践:
- 步骤 1:用
nvidia-smi -q -d MEMORY查看单卡总显存(如 A10 是 24260 MB); - 步骤 2:计算理论最大 KV Cache 容量。公式:
max_kv_cache_bytes = (total_vram * 0.85) * 0.7(0.85 是预留 15% 给系统和其他进程,0.7 是 KV Cache 实际占用显存比例,因模型层数、head 数而异,Llama-3-8B 实测约 0.68-0.72); - 步骤 3:代入 vLLM 的 KV Cache 计算公式:
max_model_len ≈ max_kv_cache_bytes / (2 * num_layers * hidden_size * 2)(2 是 float16,2 是 K 和 V 两份); - 步骤 4:取计算值的 90% 作为最终
--max-model-len。 - 线上案例:A10 单卡部署 Llama-3-8B,计算得理论值 8192,但我们设为
7372。上线后 3 个月零 OOM。若设为 8192,第 2 周就出现 2 次显存不足告警。
- 步骤 1:用
提示:永远用
--max-model-len而非--max-seq-len。后者已被弃用,且语义模糊。
3.1.2--max-num-seqs与--max-num-batched-tokens:批处理的“双保险”
- 常见误区:认为
--max-num-batched-tokens越大越好,能提升吞吐。错!这是引发长尾延迟的元凶。 - 原理:vLLM 的批处理是动态的。
--max-num-batched-tokens是单次 Prefill 阶段允许的最大 token 总数。如果设为 8192,而当前有 1 个 7000 token 的长请求 + 10 个 200 token 的短请求,vLLM 会强行把这 11 个请求 batch 在一起,Prefill 阶段耗时飙升,导致所有请求 P99 延迟暴涨。 - 我们的策略:
--max-num-seqs:设为min(128, 2 * 平均并发请求数)。例如平均并发 50,则设为 100。这是防止过多请求排队等待 Prefill 的“队列长度阀”。--max-num-batched-tokens:设为1.5 * P95 输入长度 * --max-num-seqs。例如 P95 输入长度 1024,--max-num-seqs=100,则设为153600。这个值确保绝大多数请求能高效 batch,同时避免长请求绑架短请求。
- 效果:将 P99 延迟从 2.1s 降至 0.85s,长尾请求(>1.5s)占比从 12% 降至 1.3%。
3.1.3--gpu-memory-utilization:给显存“留呼吸空间”
- 文档默认值:0.9。
- 生产现实:0.9 意味着显存利用率达 90%,此时任何一点内存碎片或临时 tensor 分配都会触发 OOM Killer。
- 我们的取值:0.82。
- 为什么是 0.82?
- 0.8 是安全底线,但太保守,浪费资源;
- 0.85 在部分模型(如 Qwen2)上仍偶发碎片问题;
- 0.82 是我们在 6 种不同模型、4 种 GPU 卡型上压测得出的“甜点值”:既保证资源利用率 >80%,又将 OOM 概率压至 0.002% 以下。
- 操作:必须配合
--max-model-len使用。单独调低此值无效。
3.1.4--enforce-eager:调试期的“救命稻草”,生产期的“定时炸弹”
- 作用:禁用 CUDA Graph,强制 eager mode 执行。
- 文档推荐:调试时开启。
- 生产陷阱:有些团队为“规避 Graph 编译失败”而长期开启,导致吞吐下降 35-40%,且无法使用
--enable-chunked-prefill。 - 正确做法:
- 上线前,用
--enforce-eager跑 1 小时全链路压测,确认无报错; - 然后关闭,开启
--enable-chunked-prefill; - 若 Graph 编译失败,不要开
--enforce-eager,而是检查:- 是否用了不兼容的 FlashAttention 版本(v2.5.8+);
--max-model-len是否超出模型 config 中max_position_embeddings;- 是否启用了
--quantization awq但未安装autoawq。
- 上线前,用
- 经验:95% 的 Graph 失败源于前两点,修复后性能提升远超“省事”带来的损失。
3.2 日志与可观测性:没有日志的系统等于没有刹车
3.2.1 结构化日志:不是“加个 JSON”,而是“埋对字段”
vLLM 默认日志是纯文本,对排查毫无价值。必须改造为结构化日志。我们用structlog+json,关键字段如下:
# 示例:一次请求的完整日志结构 { "event": "request_completed", # 事件类型,固定枚举 "request_id": "req_abc123", # 全局唯一,由 Nginx 或 API 网关注入 "model": "llama3-8b-instruct", # 模型标识 "prompt_tokens": 128, # 输入 token 数 "completion_tokens": 42, # 输出 token 数 "total_tokens": 170, # 总 token "latency_ms": 782.3, # 总耗时 ms "prefill_latency_ms": 412.1, # Prefill 阶段耗时 "decode_latency_ms": 370.2, # Decode 阶段耗时 "num_prompt_tokens_per_sec": 310.5, # Prefill 吞吐 "num_generation_tokens_per_sec": 113.4, # Decode 吞吐 "kv_cache_usage_ratio": 0.67, # KV Cache 当前占用率 "num_requests_waiting": 3, # 等待 Prefill 的请求数 "error_code": null, # 错误码,成功为 null "error_msg": null # 错误详情 }- 为什么这些字段关键?
request_id:串联 Nginx access log、vLLM log、RAG 检索 log、向量库 log,实现全链路追踪;prefill_latency_ms/decode_latency_ms:区分是 Prompt 太长(Prefill 慢)还是生成太慢(Decode 慢),指导优化方向;kv_cache_usage_ratio:提前预警显存压力,当 >0.85 时自动触发告警并降级;num_requests_waiting:比 CPU 负载更能反映服务真实负载,是弹性伸缩的核心指标。
注意:
num_requests_waiting字段需 patch vLLM 源码才能暴露。我们已提交 PR,但尚未合并。补丁核心是修改vllm/engine/llm_engine.py的_run_workers方法,将self._request_tracker.get_num_unfinished_requests()的值注入日志上下文。
3.2.2 Prometheus 指标:不止于“CPU 和内存”
vLLM 内置 Prometheus exporter,但默认只暴露基础指标。生产必须扩展。我们新增了 7 个关键指标:
| 指标名 | 类型 | 说明 | 告警阈值 |
|---|---|---|---|
vllm_request_waiting_queue_length | Gauge | 等待 Prefill 的请求数 | > 10 持续 1 分钟 |
vllm_kv_cache_usage_ratio | Gauge | KV Cache 占用率 | > 0.85 持续 30 秒 |
vllm_decode_tokens_per_second_total | Counter | 累计生成 token 数 | 5 分钟环比下降 >30% |
vllm_request_failed_total{reason="oom"} | Counter | OOM 导致的失败数 | > 0 持续 10 秒 |
vllm_request_failed_total{reason="timeout"} | Counter | 超时失败数 | > 5/分钟 |
vllm_gpu_cache_hit_rate | Gauge | GPU Cache 命中率 | < 0.9 持续 2 分钟 |
vllm_num_running_requests | Gauge | 当前正在运行的请求数 | > 95%--max-num-seqs |
- 实操技巧:
vllm_gpu_cache_hit_rate指标需手动计算。我们通过定期调用 vLLM 的/statsAPI(返回 JSON),提取gpu_cache_usage和num_total_gpu_blocks,用(num_total_gpu_blocks - gpu_cache_usage) / num_total_gpu_blocks得出命中率。这个指标低于 0.9,往往预示着--block-size设置不合理或--max-model-len过高。
3.3 状态管理:对话上下文不是“可有可无”的 feature
3.3.1 KV Cache 的“持久化幻觉”与真实方案
很多团队以为开启--enable-prefix-caching就能“永久保存”对话历史。大错特错。Prefix Caching 只是加速 Prefill,Cache 本身仍在 GPU 显存中,服务重启即消失。真正的上下文持久化,必须分层设计:
Layer 1:短期缓存(< 5 分钟)
使用 Redis Cluster,key 为ctx:{request_id}:{session_id},value 为序列化的messages列表(含 role/content/token_count)。TTL 设为 300 秒。这是最快的恢复路径。Layer 2:中期存储(< 7 天)
使用 PostgreSQL,表conversation_history,字段包括session_id,message_order,role,content,token_count,created_at。按session_id和created_at建复合索引。这是审计和 debug 的黄金来源。Layer 3:长期归档(> 7 天)
每日凌晨将 PostgreSQL 中超过 7 天的数据,ETL 到对象存储(如 S3),按日期分区,格式 Parquet。用于合规审计和离线分析。关键逻辑:vLLM 本身不参与此流程。API Server(我们用 FastAPI)在收到请求时:
- 先查 Redis,若有则拼接
messages作为prompt; - 若 Redis 无,则查 PostgreSQL 最近 10 条;
- 拼接后,调用 vLLM
/generate; - 收到响应后,将新
message写入 Redis(更新 TTL)和 PostgreSQL。
- 先查 Redis,若有则拼接
注意:Redis 的写入必须是原子的
SET key value EX 300 NX,避免并发写入导致上下文错乱。我们曾因未加NX,导致两个请求同时写入,最终用户看到的是“自己和自己的对话”。
4. 实操过程与核心环节实现:从部署到守夜的全流程
4.1 部署架构:不是“K8s 万能”,而是“恰到好处”
我们不用 K8s 部署所有 LLM 服务。架构选择基于三个硬指标:QPS、SLA、变更频率。
| 场景 | 推荐架构 | 理由 | 实例 |
|---|---|---|---|
| QPS < 50,SLA 宽松(P99 < 3s),月度迭代 | Docker Compose + Nginx | K8s 运维成本远超收益。Nginx 做负载均衡和 TLS 终结,Docker Compose 管理 vLLM 实例生命周期。 | 内部知识库问答,仅 20 人使用 |
| QPS 50-300,SLA 严格(P99 < 1.2s),周度迭代 | K8s StatefulSet + HPA | StatefulSet 保证 Pod 名称和网络标识稳定,便于日志追踪;HPA 基于vllm_request_waiting_queue_length指标自动扩缩容。 | 客服机器人,工作日 9-18 点高峰 |
| QPS > 300,SLA 极致(P99 < 800ms),实时性要求高 | K8s DaemonSet + MetalLB | DaemonSet 确保每台 Node 运行一个 vLLM 实例,消除网络跳转延迟;MetalLB 提供裸机级 IP 直通,绕过 K8s Service iptables。 | 实时翻译 API,集成到视频会议系统 |
StatefulSet 的关键配置:
# 必须设置,否则 HPA 无法获取指标 apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: vllm-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: StatefulSet name: vllm-server metrics: - type: Pods pods: metric: name: vllm_request_waiting_queue_length target: type: AverageValue averageValue: 5 # 每个 Pod 平均等待请求数 >5 时扩容DaemonSet 的网络优化:
- 禁用 K8s Service,vLLM Pod 直接绑定 NodePort(如 8080);
- Ingress Controller(如 Nginx Ingress)配置
upstream直接指向NodeIP:8080; - 实测:端到端延迟降低 120ms,P99 从 920ms 降至 780ms。
4.2 守夜 SOP:当报警响起时,你该做的三件事
生产环境没有“救火”,只有“精准外科手术”。以下是我们的标准化响应流程(SOP),已沉淀为 Runbook。
4.2.1 第一步:确认现象,拒绝“我以为”
- 动作:打开 Grafana,加载预设 Dashboard “vLLM Production Health”。
- 必看 3 个视图:
- Top 3 Latency Breakdown:看
prefill_latency_ms和decode_latency_ms哪个异常升高。若 Prefill 升高,问题在输入侧(RAG、Prompt 构造);若 Decode 升高,问题在模型或 GPU。 - KV Cache Usage Heatmap:看是否某张卡显存占用率 >0.9,而其他卡 <0.6。若是,说明流量未均匀分发,检查 Nginx upstream hash 或 K8s Service sessionAffinity。
- Error Rate by Reason:看
reason="oom"是否激增。若是,立即执行kubectl exec -it <pod> -- nvidia-smi -q -d MEMORY,确认是否真 OOM。
- Top 3 Latency Breakdown:看
提示:Dashboard 必须预设好“最近 1 小时”和“对比昨天同一时段”两个时间范围,一眼看出是突发还是渐变。
4.2.2 第二步:快速隔离,止损优先
场景 A:
reason="oom"激增- 立即执行:
kubectl scale statefulset vllm-server --replicas=1(缩容至 1 个实例,减少总显存压力); - 同时,登录该 Pod:
kubectl exec -it <pod> -- bash,运行kill -9 $(pgrep -f "vllm.entrypoints"); - 然后,用
--max-model-len降低 10% 的参数重新启动(如原 7372 → 6635),观察 5 分钟。 - 为什么有效?OOM 往往是显存碎片导致,缩容+重启能清空所有碎片,临时降参是给系统“喘息”时间。
- 立即执行:
场景 B:
num_requests_waiting持续 >50- 立即执行:
kubectl get pods -l app=vllm-server,查看所有 Pod 的READY状态; - 若有 Pod 处于
0/1,说明其 vLLM 进程已僵死,kubectl delete pod <stuck-pod>强制重建; - 同时,检查上游:
kubectl logs -l app=api-gateway --since=5m | grep "503",确认是否网关已开始返回 503,避免雪崩。
- 立即执行:
4.2.3 第三步:根因分析,闭环改进
动作:在故障解决后 24 小时内,完成 RCA(Root Cause Analysis)报告,并更新 Runbook。
RCA 模板强制包含:
- 时间线(精确到秒):从第一个告警到最终恢复;
- 数据证据:Grafana 截图、
nvidia-smi输出、关键日志片段(脱敏); - 根因结论:必须落到具体参数、代码行或配置项;
- 改进项:
- 短期(< 1 天):如“将
--max-model-len从 7372 调整为 6635”; - 中期(< 1 周):如“为
vllm_gpu_cache_hit_rate添加告警”; - 长期(< 1 月):如“重构 API Server,将 Redis 写入改为 pipeline 模式,避免并发冲突”。
- 短期(< 1 天):如“将
我们的习惯:每次 RCA 后,将改进项同步到内部 Wiki 的 “vLLM Production Lessons Learned” 页面,并设置每周五下午 3 点为“防复发 Review 会”,集体 review 过去一周所有 RCA。
4.3 灰度发布与回滚:没有“一键回滚”,只有“预案驱动”
LLM 服务的灰度,不是简单切 5% 流量,而是多维度、可中断、可验证。
4.3.1 灰度策略:三层漏斗
Layer 1:内部员工(100%)
所有研发、测试、产品同事,强制使用新版本。通过内部 Slack Bot 发送“今日体验反馈”,收集主观评价。Layer 2:白名单用户(5%)
选取 500 个历史活跃度高、反馈积极的用户,通过 API 请求头X-Canary: true标识。监控其latency_ms和error_rate,与基线对比。Layer 3:全量用户(渐进)
每 15 分钟增加 5% 流量,全程监控:- 核心指标:
vllm_request_failed_total{reason="oom"}必须为 0; - 业务指标:
avg_over_time(vllm_completion_tokens_per_second_total[5m])下降不能超过 5%; - 用户指标:前端上报的
llm_response_time_p95不能恶化。
- 核心指标:
4.3.2 回滚机制:不是“删 Pod”,而是“切流量”
- 前提:API Gateway(Nginx 或 Envoy)必须支持基于 Header 或 Cookie 的动态路由。
- 操作:
- 若触发回滚条件(如
reason="oom"> 3 次/分钟),立即执行:# Nginx 示例:将所有 X-Canary 请求路由到旧版本 upstream echo "set \$upstream_backend 'vllm-old';" > /etc/nginx/conf.d/canary.conf nginx -s reload - 同时,
kubectl scale statefulset vllm-new-server --replicas=0,停止新版本。
- 若触发回滚条件(如
- 优势:整个过程 < 3 秒,用户无感知;旧版本 Pod 保持运行,随时可切回。
5. 常见问题与排查技巧实录:那些让你拍大腿的“原来如此”
5.1 问题速查表:高频故障与“秒级”定位法
| 现象 | 可能根因 | 秒级定位命令 | 解决方案 |
|---|---|---|---|
| P99 延迟缓慢爬升(数小时) | KV Cache 碎片化 | kubectl exec <pod> -- python -c "from vllm import LLM; print(LLM('meta-llama/Meta-Llama-3-8B-Instruct').llm_engine.cache_config.num_gpu_blocks)"对比nvidia-smi -q -d MEMORY | grep "Used" | 降低--max-model-len5%,或重启 Pod |
| 服务突然 503,但 CPU/GPU 正常 | vLLM 进程僵死(无崩溃,但不响应) | kubectl exec <pod> -- netstat -tuln | grep :8000(检查端口是否监听);kubectl exec <pod> -- ps aux | grep vllm(检查进程是否存在) | kubectl delete pod <name>,K8s 自动重建 |
| RAG 检索结果变差,但向量库日志正常 | vLLM 的--max-model-len过小,导致 Prompt 被截断,RAG context 丢失 | grep "request_completed" /var/log/vllm/app.log | tail -20 | jq '.prompt_tokens',看是否大量请求prompt_tokens接近--max-model-len | 增加--max-model-len,并检查前端是否做了输入长度校验 |
日志中大量ConnectionResetError | 客户端(如浏览器)超时断开,但 vLLM 仍在生成 | kubectl logs <pod> | grep "ConnectionResetError" | wc -l,若 > 10/分钟,检查客户端 timeout | 前端将 fetch timeout 从 10s 改为 30s;vLLM 增加--response-role确保流式响应及时发送 header |
GPU 显存占用 100%,但nvidia-smi显示进程已退出 | CUDA Context 未释放(常见于 SIGTERM 未优雅处理) | nvidia-smi -q -d COMPUTE | grep "PID",找残留 PID;fuser -v /dev/nvidia* | 在 vLLM 启动脚本中添加trap 'nvidia-smi --gpu-reset -i 0' EXIT |
5.2 独家避坑技巧:来自凌晨三点的顿悟
5.2.1 技巧一:“显存占用率”不是越高越好,而是“越稳越好”
我们曾以为显存占用率 95% 是“物尽其用”。直到某次,vllm_kv_cache_usage_ratio在 0.92-0.98 之间剧烈震荡,伴随 P99 延迟毛刺。查了一夜,发现是--block-size设为 16,而实际请求 token 长度集中在 512、1024、2048 —— 这些数字除以 16 都是整数,但 KV Cache 分配时,vLLM 会为每个 block 预留 head 数 * 2 * 2 字节,当 block size 过小,block 数量爆炸,管理开销剧增。解决方案:将--block-size改为 32,显存占用率稳定在 0.87,延迟毛刺消失。结论:block-size应设为业务 P95 输入长度的 1/32 或 1/64,而非盲目追求“小”。
5.2.2 技巧二:--enable-chunked-prefill不是“开就完事”,而是“开对时机”
Chunked Prefill 能显著提升长文本处理速度,但它有个隐藏代价:Prefill 阶段的显存峰值会翻倍。因为要同时 hold 住原始 prompt 和 chunked 后的多个 sub-prompt。我们曾在线上开启此选项,结果在流量高峰时,显存峰值突破--gpu-memory-utilization限制,触发 OOM。正确姿势:
- 仅对
prompt_tokens > 2048的请求开启; - 在 API Server 层做判断:若
len(prompt) > 2048,则调用 vLLM 时附加--enable-chunked-prefill参数(需自定义 vLLM client); - 其他请求关闭,保证基础稳定性。