LLM 推理服务部署与 GPU 调度完全指南:从一次"vLLM 单卡 A100 跑 Qwen2-72B 5000 用户同时上线 KV Cache 爆显存全站 OOM"看懂为什么 pip install vllm 远远不够

2024 年我们给一家 AI 教育公司做 LLM 推理服务模型是 Qwen2-72B 加 Llama3-70B 业务高峰 5000 并发用户在线问答第一版直接 vLLM 单卡跑 A100-80G 性能测试老板说 AI 真快上线第一天就被现实暴打第一种最让我傻眼是 KV Cache 爆显存 4096 上下文加 32 batch 直接 OOM A100 80G 也不够第二种最难缠是 batch size 调不对静态 batch 1 GPU 利用率 12 batch 64 首 token 延迟 8 秒用户等到关页面第三种最离谱是模型加载 30 秒每次 pod 重启全站懵 K8s rolling update 期间整段空白第四种最致命是多模型混部同一张 A100 跑 Qwen 加 Llama 显存碎片化第二个模型加载失败容器 Pending 第五种最莫名其妙是 GPU 利用率高峰 95 但 token 吞吐没上去 nvidia-smi 一看是 PCIe 带宽被打满 host device 数据搬运卡死第六种最坑是 Prometheus 不会采 GPU 指标 nvidia-dcgm-exporter 没装显存爆了完全不知道用户骂到客服才发现真正能扛生产 LLM 推理的不是 pip install vllm 加 python -m vllm.entrypoints.openai.api_server 就够而是一个推理引擎选型加 KV Cache 调度加 Continuous Batching 加模型并行加 GPU 共享加量化压缩加 Prefill Decode 分离加 GPU 监控的完整工程体系

2024 年我们给一家 AI 教育公司做 LLM 推理服务 模型是 Qwen2-72B + Llama3-70B 业务高峰 5000 并发用户在线问答。第一版直接 vLLM 单卡跑 A100-80G 性能测试老板说"AI 真快"上线第一天就被现实暴打。第一种最让我傻眼是 KV Cache 爆显存 4096 上下文 + 32 batch 直接 OOM A100 80G 也不够;第二种最难缠是 batch size 调不对 静态 batch=1 GPU 利用率 12% batch=64 首 token 延迟 8 秒用户等到关页面;第三种最离谱是模型加载 30 秒每次 pod 重启全站懵 K8s rolling update 期间整段空白;第四种最致命是多模型混部 同一张 A100 跑 Qwen + Llama 显存碎片化 第二个模型加载失败容器 Pending;第五种最莫名其妙是 GPU 利用率 高峰 95% 但 token 吞吐没上去 nvidia-smi 一看是 PCIe 带宽被打满 host-device 数据搬运卡死;第六种最坑是 Prometheus 不会采 GPU 指标 nvidia-dcgm-exporter 没装 显存爆了完全不知道 用户骂到客服才发现。真正能扛生产 LLM 推理的不是 pip install vllm + python -m vllm.entrypoints.openai.api_server 就够,而是一个推理引擎选型 + KV Cache 调度 + Continuous Batching + 模型并行 + GPU 共享 + 量化压缩 + Prefill/Decode 分离 + GPU 监控的完整工程体系,任何一环失守都会让你的"AI 助手"变成"用户耐心粉碎机"。本文从踩坑视角梳理 LLM 推理服务生产化要点,vLLM/TGI/SGLang 怎么选 KV Cache 怎么管 batch 怎么动态 多 GPU 怎么并行 量化怎么不掉精度 PD 分离怎么部署 GPU 怎么监控,以及一些把 LLM 推理做扎实要避开的工程坑。

问题背景:为什么 pip install vllm 远远不够

很多团队跑 LLM 推理是直接 vLLM 单进程 + FastAPI 包一层 demo 阶段够用 上生产就崩:

  • 推理引擎选型:vLLM / TGI / SGLang / TensorRT-LLM 各有取舍 不看场景盲选必踩。
  • KV Cache 管理:PagedAttention 不开 显存碎片化 batch 上不去吞吐砍半。
  • Continuous Batching:静态 batch 等齐才能开始 长尾请求拖垮整 batch。
  • 多 GPU 并行:Tensor Parallel / Pipeline Parallel / Expert Parallel 配错通信开销爆炸。
  • 量化压缩:FP16/INT8/INT4 / AWQ / GPTQ 选错掉精度 用户骂模型变蠢。
  • GPU 监控:nvidia-smi 不够 必须 DCGM exporter + 显存告警 + token 吞吐 panel。

一 推理引擎选型与 vLLM 部署

vLLM 是 2024 年生产首选 PagedAttention + Continuous Batching 几乎是事实标准 但部署参数错一个就全废。

# 1 安装(CUDA 12.1+ Python 3.10+)
pip install vllm==0.5.4
# 注意 vllm 与 torch / cuda 版本强耦合 看 release notes

# 2 单卡启动 OpenAI 兼容 API server
python -m vllm.entrypoints.openai.api_server \
    --model Qwen/Qwen2-7B-Instruct \
    --tensor-parallel-size 1 \
    --gpu-memory-utilization 0.90 \
    --max-model-len 8192 \
    --max-num-seqs 256 \
    --max-num-batched-tokens 8192 \
    --enable-prefix-caching \
    --swap-space 16 \
    --port 8000 \
    --served-model-name qwen2-7b \
    --trust-remote-code

# 关键参数说明
# --gpu-memory-utilization: 0.90 = 给 vLLM 80G*0.9=72G(留 8G 给系统/CUDA context)
# --max-model-len: 单序列最大长度 影响 KV cache 占用 4096 是性价比起点
# --max-num-seqs: 同时活跃序列数 越大吞吐越高但首 token 延迟越大
# --max-num-batched-tokens: 单次 forward 总 token 数 控制 prefill batch size
# --enable-prefix-caching: 多个请求共享 system prompt 显著省 KV cache(节省 50%+)
# --swap-space: 显存满时 swap 到 CPU 16GB 起步

# 3 多卡 Tensor Parallel(72B 模型必须 多卡)
python -m vllm.entrypoints.openai.api_server \
    --model Qwen/Qwen2-72B-Instruct \
    --tensor-parallel-size 4 \                  # 4 张 A100-80G
    --pipeline-parallel-size 1 \
    --gpu-memory-utilization 0.92 \
    --max-model-len 32768 \
    --enable-chunked-prefill \                  # 长 prompt 分块避免阻塞
    --max-num-batched-tokens 16384 \
    --enforce-eager False \                     # CUDA graph 加速(decode 阶段 30% 提升)
    --quantization fp8 \                        # H100 上启用 FP8(2x 吞吐)
    --kv-cache-dtype fp8_e5m2

# 4 调用测试
curl http://localhost:8000/v1/chat/completions \
    -H "Content-Type: application/json" \
    -d '{
        "model": "qwen2-7b",
        "messages": [{"role":"user","content":"解释一下 PagedAttention"}],
        "max_tokens": 512,
        "temperature": 0.7,
        "stream": true
    }'

实战经验:gpu-memory-utilization 不要拉到 0.95 留 5-10% 给 CUDA workspace 否则 OOM;max-num-batched-tokens 是吞吐与延迟的核心旋钮 一般设 max-model-len 的 2-4 倍;enable-prefix-caching 在多用户共享 system prompt 场景能省 50% 以上 KV cache 强烈推荐;enforce-eager 默认 False 用 CUDA graph 加速 decode 但首次启动慢 30 秒;swap-space 是显存兜底 短突发用得上;Qwen 72B 用 tensor-parallel=4 比 8 更稳 通信开销小;chunked-prefill 在长 prompt 场景必开 否则 prefill 阶段把 decode 全堵住。

二 KV Cache 与 Continuous Batching 调度

KV Cache 是 LLM 推理显存大头 占比 60%+ 调度好了吞吐翻倍 调不好显存爆炸 OOM。

# 1 KV cache 显存估算公式
# KV cache size = 2 * num_layers * num_heads * head_dim * dtype_size * seq_len * batch_size
# Qwen2-72B 例子
# num_layers=80, num_kv_heads=8 (GQA), head_dim=128, dtype=fp16(2B)
# 单 token: 2 * 80 * 8 * 128 * 2 = 327680 B = 320 KB
# 8192 token * batch 32: 320KB * 8192 * 32 = 80 GB(单 A100 都装不下!)
# 必须 PagedAttention + 多卡 TP

# 2 PagedAttention 原理(vLLM 默认开)
# - KV cache 按 block(16 token)分页存储 类似 OS 虚拟内存
# - block 表 mapping 序列 logical -> physical block
# - 同 batch 不同序列长度 不会浪费显存(传统是 max_len * batch)
# - 显存利用率从 20% 提升到 96%

# 3 Continuous Batching(vLLM 默认开)
# 传统静态 batch:
#   batch=[req1(100t), req2(500t), req3(2000t)]
#   等 req3 跑完 整个 batch 才结束 GPU 大量 idle
# Continuous batching:
#   每 iteration 检查 完成的请求立即出 batch
#   新请求随时插入(用空出来的 KV block)
#   GPU 持续满载 吞吐 3-5x

# 4 配置示例(基于 vLLM Python API)
from vllm import LLM, SamplingParams

llm = LLM(
    model="Qwen/Qwen2-7B-Instruct",
    tensor_parallel_size=1,
    gpu_memory_utilization=0.9,
    max_model_len=8192,
    max_num_seqs=256,                          # 同时活跃序列上限
    max_num_batched_tokens=8192,               # 单 iter 总 token
    enable_prefix_caching=True,                # prefix 共享
    block_size=16,                             # PagedAttention block 大小
    swap_space=16,                             # 显存满时 CPU swap GB
    cpu_offload_gb=0,                          # 模型权重 offload(慢 慎用)
    enable_chunked_prefill=True,               # prefill 分块
    max_seq_len_to_capture=8192,               # CUDA graph 最大长度
)

# 5 KV cache 监控(vLLM metrics endpoint)
# GET http://localhost:8000/metrics
# 关键指标:
# vllm:num_requests_running         当前运行请求数
# vllm:num_requests_waiting         等待队列长度
# vllm:gpu_cache_usage_perc         KV cache 使用率(>90% 危险)
# vllm:gpu_prefix_cache_hit_rate    prefix 缓存命中率
# vllm:time_to_first_token_seconds  首 token 延迟
# vllm:time_per_output_token_seconds 单 token 延迟
# vllm:e2e_request_latency_seconds  端到端延迟

# 6 显存压力下的调度策略
# 当 gpu_cache_usage_perc > 90%:
# - vLLM 自动 preempt 低优先级序列(swap 到 CPU)
# - 等显存空出来再 swap 回 GPU 继续
# - 但 swap 来回会显著增加延迟 应避免
# 解决方案:
# - 降低 max_num_seqs(同时活跃数)
# - 启用 prefix caching 降低 KV 占用
# - 设置 request 优先级 priority(VIP 用户优先)

实战经验:KV cache 显存估算公式必须背下来 部署前先算清楚 别等 OOM 才慌;PagedAttention block_size 16 是默认值 一般不动 想优化通信可以试 32;Continuous batching 让我们吞吐从 800 token/s 提升到 4500 token/s 几乎是免费午餐;prefix caching 在 RAG / multi-turn chat 场景命中率 60%+ 直接省一半显存;gpu_cache_usage_perc 是核心告警指标 超 85% 就要扩容;swap_space 是救命稻草 但平时不应该触发 触发说明容量规划不对;chunked-prefill 让长 prompt 不会阻塞短请求 是 SLA 关键。

三 多 GPU 并行与模型分片

70B+ 模型必须多 GPU 并行 Tensor Parallel / Pipeline Parallel / Expert Parallel 选错通信开销爆炸 性能反而不如单卡 + offload。

# 1 Tensor Parallel (TP)
# 把每层的权重矩阵按列/行切分到多张 GPU
# 适合: 单层巨大的模型(MLP/Attention)
# 通信: 每层 forward 都需要 all-reduce(NCCL)
# 要求: GPU 间 NVLink 带宽高(否则通信成瓶颈)
# vLLM 默认 用 --tensor-parallel-size N

# 2 Pipeline Parallel (PP)
# 把模型按层切分 一组 GPU 算前半 另一组算后半
# 适合: 模型层数多 + GPU 间无 NVLink
# 通信: 只在 stage 边界传 activation
# 缺点: 有 pipeline bubble(idle)
# vLLM: --pipeline-parallel-size N

# 3 TP + PP 混合
# 8 卡 70B 配置(典型 H100/A100 节点)
python -m vllm.entrypoints.openai.api_server \
    --model meta-llama/Llama-3-70B-Instruct \
    --tensor-parallel-size 4 \                 # 节点内 4 卡 TP(用 NVLink)
    --pipeline-parallel-size 2 \               # 跨节点 PP(用 IB/RoCE)
    --distributed-executor-backend ray \       # 多节点用 ray
    --gpu-memory-utilization 0.92

# 4 通信拓扑检查(部署前必做)
nvidia-smi topo -m
# 输出示例:
#       GPU0  GPU1  GPU2  GPU3  GPU4  GPU5  GPU6  GPU7
# GPU0   X   NV4   NV4   NV4   SYS   SYS   SYS   SYS
# GPU1  NV4   X    NV4   NV4   SYS   SYS   SYS   SYS
# ...
# NV4 = NVLink (好) / PIX = PCIe Switch (中) / SYS = QPI 跨 NUMA (差)
# TP 必须放在 NVLink 互联的 GPU 上 否则 all-reduce 慢 10x

# 5 NCCL 调优(关键 否则通信拖慢推理)
export NCCL_DEBUG=INFO                         # 排查时开
export NCCL_IB_DISABLE=0                       # 启用 InfiniBand
export NCCL_P2P_DISABLE=0                      # 启用 P2P
export NCCL_NET_GDR_LEVEL=2                    # GPUDirect RDMA
export NCCL_SOCKET_IFNAME=eth0
export NCCL_IB_HCA=mlx5_0
export NCCL_BUFFSIZE=8388608
# 实测:NCCL 调优后 TP=4 通信延迟从 8ms 降到 2.5ms

# 6 多机部署(Ray 集群)
# head 节点
ray start --head --port=6379 --num-gpus=8
# worker 节点
ray start --address=<head_ip>:6379 --num-gpus=8
# vLLM 自动调度跨节点 TP/PP

# 7 失败排查清单
# - TP rank 卡死:NCCL 通信问题 检查 IB 网络
# - 启动 OOM:gpu-memory-utilization 调小 或减少 max-model-len
# - 吞吐不达标:topology 错了 GPU 不在同一 NVLink domain
# - 单卡利用率不均:Expert parallelism 路由不均(MoE 模型)

实战经验:TP size 必须是单卡 attention head 数的因子(GQA 8 head 时 TP=8 不行 TP=4 才对);TP 必须用 NVLink 否则不如单卡 + CPU offload;pipeline parallel 在大集群有用 8 卡以下别上 bubble 开销不划算;NCCL_NET_GDR_LEVEL 一定要开 GPUDirect RDMA 让 GPU 直接通过 IB 通信不经 CPU 内存;ray 跨节点部署比 multiprocessing 稳得多;部署前必须 nvidia-smi topo -m 看一眼 拓扑错了再多 GPU 也白搭;MoE 模型(Mixtral)要用 expert-parallel 路由不均要 expert balance loss。

[mermaid]
flowchart TD
A[用户请求] --> B[API Gateway]
B --> C[Load Balancer]
C --> D{请求路由}
D -->|短 prompt| E[Decode 优化实例]
D -->|长 prompt| F[Prefill 优化实例]
E --> G[vLLM Engine]
F --> G
G --> H[Continuous Batching 调度器]
H --> I[PagedAttention KV Cache]
I --> J[Tensor Parallel forward]
J --> K[GPU0/1/2/3 NCCL all-reduce]
K --> L[Token 输出]
L -->|stream| B
H -->|metrics| M[DCGM + Prometheus]
M --> N[Grafana 大盘]

四 量化压缩与精度平衡

量化能让 70B 模型在单卡跑 内存 + 吞吐都受益 但选错算法会显著掉点。

# 1 量化方案对比(2024 主流)
# FP16   (基线) 显存 100% 吞吐 1x   精度 100%
# BF16   (训练) 显存 100% 吞吐 1x   精度 100%
# FP8    (H100) 显存 50%  吞吐 2x   精度 99%+
# INT8   (W8A8) 显存 50%  吞吐 1.5x 精度 98%+(SmoothQuant)
# INT4   (AWQ)  显存 25%  吞吐 2-3x 精度 96-98%(weight-only)
# GPTQ   (4bit) 显存 25%  吞吐 2x   精度 96%+(post-training)

# 2 AWQ 量化 70B 模型(单 A100 80G 可跑)
# 安装
pip install autoawq

# 量化脚本
from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer

model_path = "Qwen/Qwen2-72B-Instruct"
quant_path = "Qwen2-72B-Instruct-AWQ"
quant_config = {
    "zero_point": True,
    "q_group_size": 128,
    "w_bit": 4,
    "version": "GEMM"
}

model = AutoAWQForCausalLM.from_pretrained(
    model_path, **{"low_cpu_mem_usage": True}
)
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
model.quantize(tokenizer, quant_config=quant_config)
model.save_quantized(quant_path)

# vLLM 加载 AWQ 模型
# python -m vllm.entrypoints.openai.api_server \
#     --model Qwen2-72B-Instruct-AWQ \
#     --quantization awq \
#     --gpu-memory-utilization 0.92 \
#     --max-model-len 8192

# 3 FP8 量化(H100 专属 2x 吞吐免费午餐)
# vLLM 0.5+ 原生支持
python -m vllm.entrypoints.openai.api_server \
    --model meta-llama/Llama-3-70B-Instruct \
    --quantization fp8 \                       # 权重 + activation FP8
    --kv-cache-dtype fp8_e5m2 \                # KV cache FP8(再省 50%)
    --tensor-parallel-size 2 \                 # H100 80G * 2 即可
    --max-model-len 32768

# 4 量化精度验证(必须做!不能盲信)
# 用 lm-evaluation-harness 跑标准 benchmark
pip install lm-eval
lm_eval --model vllm \
    --model_args pretrained=Qwen2-72B-Instruct-AWQ,quantization=awq \
    --tasks mmlu,ceval,cmmlu \
    --batch_size 16 \
    --output_path results/awq.json

# 对比基线
lm_eval --model vllm \
    --model_args pretrained=Qwen/Qwen2-72B-Instruct \
    --tasks mmlu,ceval,cmmlu \
    --output_path results/fp16.json

# 5 业务侧 A/B 测试(更重要)
# 同一批用户问题 fp16 vs awq 各跑 1000 例
# 人工标注或用 GPT-4 当 judge 评 win/tie/loss
# 通常 AWQ 比 fp16 win rate 48% 49% 在可接受范围

实战经验:能用 FP8 别用 INT4 H100 + FP8 是 2024 性价比之王;AWQ 比 GPTQ 精度好且 vLLM 支持完善 是 INT4 首选;量化后必须跑 mmlu/ceval/cmmlu 三件套 看 acc 掉了多少 超过 2 点要重新调 group_size;kv-cache-dtype fp8 是被严重低估的优化 能让 max-num-seqs 翻倍;别迷信量化能"零损失" 业务 A/B 测才是真理;INT4 在数学推理任务掉得明显 RAG / 对话场景影响小;量化模型加载要预热 第一次推理慢 5x 后续才稳定。

五 Prefill/Decode 分离与请求调度

Prefill(长 prompt 处理)与 Decode(逐 token 生成)资源需求完全不同 混在一起部署 GPU 利用率永远拉不满。

# 1 PD 分离架构(vLLM 0.5+ / SGLang / DistServe)
# Prefill 阶段: 计算密集 SM 利用率 95%+ KV cache 小
# Decode 阶段: 内存带宽密集 SM 利用率 30% KV cache 大
# 混部问题: prefill 把 GPU 占满 decode 阶段没法插入 首 token 延迟暴涨

# 2 分离部署 K8s manifest(简化版)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm-prefill
spec:
  replicas: 2
  template:
    spec:
      containers:
      - name: vllm
        image: vllm/vllm-openai:v0.5.4
        args:
        - --model=Qwen/Qwen2-72B-Instruct
        - --tensor-parallel-size=4
        - --max-num-batched-tokens=32768   # prefill 大 batch
        - --max-num-seqs=64
        - --disable-log-requests
        - --enable-chunked-prefill=false   # prefill 实例不需要 chunked
        resources:
          limits:
            nvidia.com/gpu: 4
        env:
        - name: VLLM_ROLE
          value: prefill
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm-decode
spec:
  replicas: 4
  template:
    spec:
      containers:
      - name: vllm
        image: vllm/vllm-openai:v0.5.4
        args:
        - --model=Qwen/Qwen2-72B-Instruct
        - --tensor-parallel-size=2
        - --max-num-batched-tokens=2048    # decode 小 batch
        - --max-num-seqs=512               # 高并发活跃序列
        - --enable-prefix-caching=true
        resources:
          limits:
            nvidia.com/gpu: 2

# 3 KV cache 跨节点传输(关键!)
# Prefill 完成后 KV cache 必须传给 decode 实例
# 方案:
# a) RDMA + NCCL P2P(最快 但要 IB 网络)
# b) NVLink(同节点 GPU 间)
# c) CPU 中转 + Redis(慢 但通用)
# 实测: RDMA 传 70B 模型 8K KV cache 约 50ms

# 4 路由层(根据 prompt 长度路由)
# Nginx Lua 或 自研 gateway
location /v1/chat/completions {
    access_by_lua_block {
        local body = ngx.req.get_body_data()
        local data = cjson.decode(body)
        local total_tokens = 0
        for _, msg in ipairs(data.messages) do
            total_tokens = total_tokens + estimate_tokens(msg.content)
        end
        if total_tokens > 2048 then
            ngx.var.backend = "vllm-prefill-svc"
        else
            ngx.var.backend = "vllm-decode-svc"
        end
    }
    proxy_pass http://$backend;
}

PD 分离不是所有场景都必要 但在 long prompt + 高并发场景收益巨大 我们做完之后 首 token 延迟从 P99 6 秒降到 1.2 秒。下面进一步看请求级别的优先级与限流 否则一个大用户能把整个集群打挂:

# 5 请求优先级与限流
from fastapi import FastAPI, Request, HTTPException
from collections import defaultdict
import asyncio, time

app = FastAPI()

# 用户级令牌桶(防止单用户独占)
buckets = defaultdict(lambda: {"tokens": 100, "last": time.time()})
REFILL_RATE = 10  # tokens/s

async def rate_limit(user_id: str, cost: int):
    b = buckets[user_id]
    now = time.time()
    elapsed = now - b["last"]
    b["tokens"] = min(100, b["tokens"] + elapsed * REFILL_RATE)
    b["last"] = now
    if b["tokens"] < cost:
        raise HTTPException(429, "rate limit exceeded")
    b["tokens"] -= cost

# 优先级队列(VIP 用户优先调度)
class PriorityRouter:
    def __init__(self):
        self.queues = {"vip": asyncio.Queue(), "normal": asyncio.Queue()}

    async def submit(self, req, priority="normal"):
        await self.queues[priority].put(req)

    async def worker(self):
        while True:
            # VIP 队列优先
            if not self.queues["vip"].empty():
                req = await self.queues["vip"].get()
            else:
                req = await self.queues["normal"].get()
            await self.process(req)

# 长度预算控制(防止用户用 max_tokens=8192 烧 GPU)
def validate_request(req):
    if req.max_tokens > 2048:
        if req.user_tier != "premium":
            req.max_tokens = 2048   # 自动截断 + 告警
    if req.max_tokens * req.n > 8192:
        raise HTTPException(400, "total output too large")

# 慢请求熔断(防止 spec decoding 失败导致单请求卡 minutes)
async def with_timeout(coro, deadline=60):
    try:
        return await asyncio.wait_for(coro, timeout=deadline)
    except asyncio.TimeoutError:
        # 取消 vLLM 内部 request id
        await llm.abort(req_id)
        raise HTTPException(504, "inference timeout")

实战经验:PD 分离对短问答场景没必要 长 RAG / 长上下文文档总结收益最大;KV cache 传输是 PD 分离最大难点 没 IB 网络就别做了不如混部;用户级限流必上 防止单用户烧光整个集群额度;max_tokens 必须强制截断 不能信用户传的值;慢请求熔断超过 60 秒直接 abort vLLM 内部 request 否则会一直占着 KV cache;优先级队列让 VIP 用户体验稳定 但要避免饿死 normal 队列;路由层用 Nginx Lua 估算 token 数比每个请求都过 tokenizer 快 100x。

六 GPU 监控、故障演练与成本治理

LLM 推理服务最贵的资源是 GPU 没监控等于裸奔 没故障演练等于赌博 没成本治理财务会找上门。

# 1 DCGM exporter 部署(K8s daemonset)
helm install --generate-name gpu-helm-charts/dcgm-exporter \
    --set serviceMonitor.enabled=true

# 关键指标(Prometheus 采集)
# DCGM_FI_DEV_GPU_UTIL                 GPU SM 利用率
# DCGM_FI_DEV_MEM_COPY_UTIL            显存拷贝利用率
# DCGM_FI_DEV_FB_USED                  显存已用 MB
# DCGM_FI_DEV_FB_FREE                  显存空闲 MB
# DCGM_FI_DEV_POWER_USAGE              功耗 W
# DCGM_FI_DEV_GPU_TEMP                 GPU 温度
# DCGM_FI_DEV_NVLINK_BANDWIDTH_TOTAL   NVLink 带宽
# DCGM_FI_DEV_PCIE_TX_THROUGHPUT       PCIe TX 带宽
# DCGM_FI_DEV_XID_ERRORS               XID 错误码(硬件故障!)
# DCGM_FI_DEV_ROW_REMAP_FAILURE        显存 row remap 失败(显存损坏)

# 2 vLLM 业务指标
# vllm:num_requests_running
# vllm:num_requests_waiting
# vllm:gpu_cache_usage_perc
# vllm:gpu_prefix_cache_hit_rate
# vllm:time_to_first_token_seconds       (TTFT P50/P99)
# vllm:time_per_output_token_seconds     (TPOT P50/P99)
# vllm:e2e_request_latency_seconds
# vllm:request_prompt_tokens
# vllm:request_generation_tokens

# 3 关键告警规则(PrometheusRule)
groups:
- name: llm-inference-critical
  rules:
  - alert: GPUMemoryFull
    expr: DCGM_FI_DEV_FB_USED / (DCGM_FI_DEV_FB_USED + DCGM_FI_DEV_FB_FREE) > 0.95
    for: 2m
    annotations:
      summary: "GPU {{ $labels.gpu }} memory > 95% OOM 临界"

  - alert: GPUUtilLow
    expr: DCGM_FI_DEV_GPU_UTIL < 20
    for: 10m
    annotations:
      summary: "GPU {{ $labels.gpu }} 利用率 < 20% 资源浪费"

  - alert: GPUXIDError
    expr: increase(DCGM_FI_DEV_XID_ERRORS[5m]) > 0
    annotations:
      summary: "GPU {{ $labels.gpu }} XID 错误 硬件故障!需重启 node"

  - alert: VLLMQueueBacklog
    expr: vllm:num_requests_waiting > 100
    for: 3m
    annotations:
      summary: "vLLM 排队 > 100 容量不够 需扩容"

  - alert: TTFTHigh
    expr: histogram_quantile(0.99, vllm:time_to_first_token_seconds_bucket) > 3
    for: 5m
    annotations:
      summary: "首 token P99 > 3 秒 体验受损"

有了基础指标与告警 还要做实战故障演练 + 成本治理 否则只能等线上事故来教你:

# 4 故障演练清单(每月跑一次)
# a) 单卡 OOM 模拟
nvidia-smi --gpu-reset -i 0          # 不要在生产做
# 验证: K8s pod 自动重启 + alert 触发 + 流量切走

# b) NCCL 通信中断模拟
# 用 iptables 阻断节点间 NCCL 端口
iptables -A INPUT -p tcp --dport 29500 -j DROP
# 验证: 推理实例报错 + 自动 fail over

# c) 显存碎片化压测
# 持续提交不同长度请求 2 小时 看 vllm 是否能稳定调度
ab -n 10000 -c 50 -p req.json http://vllm:8000/v1/chat/completions

# d) 模型加载失败模拟
# 删除模型文件中间一个 safetensors 块
# 验证: pod startup probe 失败 不接流量

# 5 成本治理(老板每月会问)
# 单 token 成本计算
# A100-80G * 4 月租 $6000 * 4 = $24000
# 单实例吞吐 4500 token/s = 11.6B token/月(50% 利用率)
# 单百万 token 成本 = $2.07
# 对比 OpenAI gpt-4o-mini $0.15/M(便宜) vs gpt-4o $2.5/M(打平)

# 6 节流策略
# 非工作时间 K8s HPA 缩到 30% 节省 70% 成本
kubectl autoscale deployment vllm-decode --cpu-percent=80 --min=2 --max=10
# 配合 cron 在凌晨缩容
0 0 * * * kubectl scale deployment vllm-decode --replicas=2
0 8 * * * kubectl scale deployment vllm-decode --replicas=10

# 7 多模型混部(同一节点跑 7B + 13B)
# 用 NVIDIA MPS(Multi-Process Service)
export CUDA_MPS_PIPE_DIRECTORY=/tmp/mps
nvidia-cuda-mps-control -d
# 或者 MIG(A100 7 个独立切分 H100 7 个)
nvidia-smi mig -cgi 19,19,19 -C    # 3 个 1g.10gb 实例

# 8 慢请求 trace(OpenTelemetry)
# vLLM + Langfuse / Arize 集成
# 每个请求带 trace_id 关联到具体 GPU + prompt + 输出
# 慢请求一键查到是哪张卡哪个 prompt 触发的

实战经验:DCGM_FI_DEV_XID_ERRORS 是 GPU 硬件故障最可靠的信号 出现就要立刻替换卡;GPU 利用率 < 20% 持续 10 分钟告警 能逼着工程师做合并部署节省成本;故障演练每月一次 不演练等于没有 HA;成本治理一定要算单 token 成本 比 OpenAI 贵很多就说明用错了规模或者模型选错了;非工作时间缩容是最大的省钱手段 70% 流量集中在 12 小时;MIG 比 MPS 隔离更好 但显存切分粒度大 灵活性差;OpenTelemetry trace 是定位线上慢请求的杀手锏。我们做完全套监控告警 + 演练 + 成本治理后 集群可用性 99.95% 单 token 成本降到 $0.6/M 比上线初期降低了 70%。

关键概念速查

问题 关键参数 推荐值 备注
推理引擎 - vLLM 0.5+ SGLang/TRT-LLM 备选
显存利用 gpu_memory_utilization 0.90 留 10% workspace
KV cache 调度 PagedAttention 默认开 block_size=16
动态 batch Continuous Batching 默认开 3-5x 吞吐提升
系统 prompt 共享 enable_prefix_caching true RAG 场景必开
多卡并行 tensor_parallel_size =NVLink 卡数 必须 head 数因子
量化(H100) FP8 + kv_cache fp8 双开 2x 吞吐近零损失
量化(A100) AWQ INT4 group_size=128 显存 25% 精度 96%+
长 prompt enable_chunked_prefill true 不阻塞 decode
GPU 监控 DCGM exporter 必装 XID 错误关键

避坑清单

  1. gpu_memory_utilization 不要超 0.95 必 OOM 留 5-10% workspace。
  2. tensor_parallel_size 必须是 attention head 数的因子 GQA 模型尤其要算清楚。
  3. TP 必须在 NVLink 互联 GPU 上 跨 PCIe 通信慢 10x 不如单卡 offload。
  4. enable_prefix_caching 在多用户共享 system prompt 场景必开 省 50%+ 显存。
  5. chunked-prefill 长 prompt 场景必开 否则 prefill 把 decode 全堵住。
  6. FP8 量化只在 H100/H200 可用 A100 用 AWQ INT4。
  7. 量化后必须跑 mmlu/ceval 评测 精度掉超过 2 点要换 group_size 或换算法。
  8. KV cache 显存估算公式必须背 部署前算清楚 不要等 OOM 慌。
  9. DCGM XID 错误就是硬件故障 立刻把节点踢出调度 别等连环崩。
  10. 非工作时间 HPA 缩容是最大成本节流手段 70% 流量集中在 12 小时别浪费。

总结

LLM 推理服务部署不是 pip install vllm 就够 而是一个 推理引擎选型 + KV Cache 调度 + Continuous Batching + 多 GPU 并行 + 量化压缩 + PD 分离 + GPU 监控 + 成本治理 的完整工程体系。每个环节都对应着真实生产事故:KV cache 爆显存 OOM、batch size 调不好 GPU 闲置或延迟爆炸、量化掉点用户骂模型变蠢、NCCL 通信慢拖垮吞吐、GPU 硬件故障没监控连环崩、成本失控老板找上门。

本文给出的 6 个维度只是骨架 真实落地还要结合你的模型规模 GPU 型号 业务峰谷形态做细致的容量规划与混沌演练。比如 7B 模型在 A10/RTX4090 跑 跟 72B 在 A100/H100 跑完全不同套路 文本对话与多模态推理(图像/语音)显存模型也大相径庭 需要单独建模。建议每月做一次故障演练 每周看一次 GPU 利用率与成本看板 让推理服务从"能跑"进化到"稳定 高效 省钱"。

打个比方 LLM 推理服务就像一座现代化的"AI 中央厨房":vLLM 引擎是主厨流水线 KV Cache 是食材冷库(Paged 是货架管理 让食材不浪费空间)Continuous Batching 是动态出菜系统(谁先做好谁先上桌)Tensor Parallel 是多个厨师协作切配同一道菜(必须靠 NVLink 这种厨房传送带 不能隔楼传)量化是把整鸡换成预切肉块(节省冷库空间但风味略减)PD 分离是把备料区与出菜区分开(避免互相挤占灶台)DCGM 监控是厨房消防 + 食材库存告警系统(温度高了 食材少了 立刻报警)成本治理是 KPI 表(每道菜成本多少 高峰多少桌 低谷少几个厨师省工资)。任何一个环节疏忽都会让整个厨房乱套 高峰期出不来菜 客人翻桌 老板砍预算。把这套体系打磨扎实 你的 LLM 服务才能从"网红快闪店"变成"米其林三星"。

愿你的推理服务首 token 延迟稳定 GPU 利用率漂亮 KV cache 命中率高 成本曲线优雅 老板再也不会半夜打电话问"AI 又挂了?"。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

Nginx 性能调优与超大并发完全指南:从一次"直播开播 5 分钟 worker_connections 1024 撞墙全站 502"看懂为什么 apt install nginx 远远不够

2026-5-25 11:38:23

技术教程

Elasticsearch 集群运维与索引设计完全指南:从一次"1200 个索引 12000 个 shard master OOM 重启 20 分钟全站搜不出"看懂为什么 docker run elasticsearch 远远不够

2026-5-25 11:48:30

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索