返回 Expert 笔记
Expert Day 163

推理基础设施 — vLLM/SGLang/TGI 与 PagedAttention

### 1.1 推理引擎全景图

2026-10-11
Phase 3 - 生产基础设施与评估 (Day 163-176)
LLMOpsvLLMSGLangPagedAttentionInferenceContinuousBatching

日期: 2026-10-11 方向: AI系统工程 / LLMOps / Inference Infra 阶段: Phase 3 - 生产基础设施与评估 (Day 163-176) 标签: #LLMOps #vLLM #SGLang #PagedAttention #Inference #ContinuousBatching


今日目标

类型内容
学习vLLM/SGLang/TGI 三大推理引擎对比;PagedAttention 内核原理;continuous batching;KV cache 内存模型;speculative decoding
实操用 vLLM 部署 Llama 4 70B 与 DeepSeek-V3,跑 OpenAI 兼容 API;与 Transformers naive inference 做 benchmark
产出docs/ai-infra/vllm_setup.md:vLLM 启动脚本、Docker compose、基准测试报告

为什么是 Day 163:Phase 3 前 42 天(Day 121-162)覆盖了 LLM/RAG/Agent 的"应用层",但生产环境真正吃成本和延迟的是"推理层"。从今天开始的两周(Day 163-176)是 LLMOps 工程冲刺:把 demo 变成可上线的金融级 AI 系统。


一、核心概念

1.1 推理引擎全景图

应用层(LangChain / LlamaIndex / 自研 Agent)
       │ OpenAI 兼容 API(/v1/chat/completions)
       ▼
┌──────────────────────────────────────────────┐
│  Serving 层(这一层是今天的主题)              │
│  ┌──────────┬──────────┬──────────┬───────┐  │
│  │  vLLM    │  SGLang  │   TGI    │ TRT-LLM│  │
│  └──────────┴──────────┴──────────┴───────┘  │
└──────────────────────────────────────────────┘
       │ tensor / KV cache 在 GPU 上分页管理
       ▼
GPU 层(A100 / H100 / H200 / B200)
引擎出身核心创新强项弱点
vLLMUC BerkeleyPagedAttention、continuous batching通用、社区最大、模型支持广量化生态略弱于 TRT-LLM
SGLangUC Berkeley/LMSYSRadixAttention(前缀缓存)多轮对话、agent prompt 复用场景 throughput 翻倍较新,生产案例少
TGIHugging Face流式优先、生产打磨HF 生态无缝,部署简单性能落后 vLLM
TensorRT-LLMNVIDIAkernel 级优化 + 编译H100/B200 上 throughput 最高闭源 kernel、模型支持周期长
Llama.cpp社区CPU/Apple Silicon边缘/本地服务器场景 throughput 低

金融场景选型经验:私有化部署金融模型(合规、不出域),首选 vLLM(开源 + 社区 + 中等成本);前缀稳定的 RAG 场景考虑 SGLang;H100 集群优先级高的核心服务用 TRT-LLM。

1.2 PagedAttention 原理

问题:朴素的 attention 实现给每个 sequence 预留 max_length 的 KV cache 空间,导致 60-80% 的显存碎片浪费。

解决方案(vLLM 论文 SOSP'23 借鉴 OS 虚拟内存):

传统 KV cache(连续分配):
[seq1: ████████████████░░░░░░░░░░░░] ← 浪费空间
[seq2: ████░░░░░░░░░░░░░░░░░░░░░░░░]
[seq3: ████████░░░░░░░░░░░░░░░░░░░░]

PagedAttention(分页):
物理 block pool: [B1][B2][B3][B4][B5][B6][B7][B8]
seq1 → page table: [B1, B3, B5]   ← 只用 3 个 block
seq2 → page table: [B2]            ← 只用 1 个
seq3 → page table: [B4, B6]        ← 只用 2 个
                  [B7, B8] free,可立刻分配给新请求
  • block_size:默认 16 token / block
  • copy-on-write:beam search / parallel sampling 时多个 sequence 共享前缀 block,分叉时才 copy
  • prefix caching:v0.4+ 支持,重复 system prompt 直接命中

1.3 Continuous batching(动态批处理)

对比 static batching

static batching(旧):
  step1: [s1, s2, s3, s4]  ← 一起开始
  step2: [s1, s2, s3, s4]
  step3: [s1, s2, s3, s4]  ← 必须等最长的完成
  step4: [_, _, s3, _]     ← s3 还没完,s1/s2/s4 GPU 空转

continuous batching(vLLM):
  step1: [s1, s2, s3, s4]
  step2: [s1, s5, s3, s6]  ← s2/s4 完成后立刻插入新请求 s5/s6
  step3: [s7, s5, s3, s6]
                ↑ GPU 利用率从 30% 提到 80%+

Throughput 增益:vLLM 在 LLaMA-13B + A100 上比 HF Transformers 提升 23x(论文数据),实际生产 10-20x 是常态。

1.4 KV cache 显存计算

公式:KV_cache_per_token = 2 × num_layers × num_heads × head_dim × dtype_bytes

例:Llama 4 70B(80 layers, 64 heads, head_dim=128, fp16=2B):

  • 每 token KV cache = 2 × 80 × 64 × 128 × 2 = 2.6 MB
  • 8K context = 21 GB
  • 32K context = 84 GB(单卡 H100 80G 装不下,需 TP/PP)

二、生产架构图

                    ┌─────────────────────────────────┐
                    │       Client(金融业务系统)      │
                    └─────────────┬───────────────────┘
                                  │ HTTPS
                                  ▼
                    ┌─────────────────────────────────┐
                    │   API Gateway(Kong / Envoy)    │
                    │   - 鉴权(JWT)  - 限流  - 审计    │
                    └─────────────┬───────────────────┘
                                  │
                ┌─────────────────┼─────────────────┐
                ▼                 ▼                 ▼
        ┌──────────────┐  ┌──────────────┐  ┌──────────────┐
        │ Guardrails   │  │ Prompt Cache │  │   Router     │
        │ (input scan) │  │  (Redis)     │  │ (model pick) │
        └──────┬───────┘  └──────┬───────┘  └──────┬───────┘
               └─────────────────┼─────────────────┘
                                 ▼
                    ┌─────────────────────────────────┐
                    │   vLLM Serving Cluster          │
                    │   ┌──────┐ ┌──────┐ ┌──────┐    │
                    │   │ H100 │ │ H100 │ │ H100 │    │
                    │   │ TP=2 │ │ TP=2 │ │ TP=2 │    │
                    │   └──────┘ └──────┘ └──────┘    │
                    │   Llama 4 70B / DeepSeek V3     │
                    └─────────────┬───────────────────┘
                                  │
                ┌─────────────────┼─────────────────┐
                ▼                 ▼                 ▼
        ┌──────────────┐  ┌──────────────┐  ┌──────────────┐
        │  Eval Hook   │  │ Observability│  │  Analytics   │
        │ (LLM judge)  │  │  (Langfuse)  │  │ (ClickHouse) │
        └──────────────┘  └──────────────┘  └──────────────┘

三、代码实现

3.1 vLLM 启动(Docker + CLI)

# install
pip install vllm==0.6.3 torch==2.4.0

# Docker(生产推荐)
docker run --runtime nvidia --gpus all \
  -v ~/.cache/huggingface:/root/.cache/huggingface \
  -p 8000:8000 \
  --ipc=host \
  --name vllm-llama4 \
  vllm/vllm-openai:v0.6.3 \
  --model meta-llama/Llama-4-70B-Instruct \
  --tensor-parallel-size 2 \
  --max-model-len 32768 \
  --gpu-memory-utilization 0.92 \
  --enable-prefix-caching \
  --max-num-seqs 256 \
  --dtype bfloat16 \
  --quantization fp8 \
  --api-key $VLLM_API_KEY

关键参数说明:

参数默认推荐(生产)含义
--tensor-parallel-size12-8跨 GPU 切分单个模型
--max-model-len模型自带按业务设 32K防 OOM
--gpu-memory-utilization0.90.92-0.95越高 KV cache 越多,throughput 越高
--enable-prefix-cachingFalseTrueRAG/agent 场景必开
--max-num-seqs256256-512并发上限
--quantizationNonefp8 / awq / gptqA100 用 awq,H100 用 fp8
--enable-chunked-prefillTrue (v0.6+)Trueprefill 切块,TTFT 更稳定

3.2 OpenAI 兼容 API 调用

"""vllm_client.py — 调用本地 vLLM,对比 Anthropic API"""
import asyncio
import time
from openai import AsyncOpenAI

VLLM_BASE = "http://localhost:8000/v1"
VLLM_KEY = "local-key"
MODEL = "meta-llama/Llama-4-70B-Instruct"

client = AsyncOpenAI(base_url=VLLM_BASE, api_key=VLLM_KEY)


async def chat(prompt: str, stream: bool = False):
    t0 = time.time()
    if stream:
        ttft = None
        full = ""
        async with client.chat.completions.stream(
            model=MODEL,
            messages=[{"role": "user", "content": prompt}],
            max_tokens=512,
            temperature=0.2,
        ) as s:
            async for event in s:
                if event.type == "content.delta":
                    if ttft is None:
                        ttft = time.time() - t0
                    full += event.delta
        total = time.time() - t0
        n_tok = len(full.split())
        return {"ttft": ttft, "total": total, "tokens": n_tok, "tps": n_tok / total}
    else:
        r = await client.chat.completions.create(
            model=MODEL,
            messages=[{"role": "user", "content": prompt}],
            max_tokens=512,
        )
        return {"total": time.time() - t0, "text": r.choices[0].message.content}


async def benchmark(n_concurrent: int = 32, n_total: int = 256):
    """Concurrent throughput benchmark."""
    prompts = [f"Explain financial concept #{i} in 200 words." for i in range(n_total)]
    sem = asyncio.Semaphore(n_concurrent)

    async def one(p):
        async with sem:
            return await chat(p, stream=True)

    t0 = time.time()
    results = await asyncio.gather(*[one(p) for p in prompts])
    total = time.time() - t0
    total_tokens = sum(r["tokens"] for r in results)
    avg_ttft = sum(r["ttft"] for r in results) / len(results)

    print(f"=== vLLM Benchmark ===")
    print(f"Total requests : {n_total}")
    print(f"Concurrency    : {n_concurrent}")
    print(f"Wall time      : {total:.2f}s")
    print(f"Total tokens   : {total_tokens}")
    print(f"Throughput     : {total_tokens / total:.1f} tok/s")
    print(f"Avg TTFT       : {avg_ttft * 1000:.0f} ms")
    print(f"QPS            : {n_total / total:.2f}")


if __name__ == "__main__":
    asyncio.run(benchmark(n_concurrent=32, n_total=256))

3.3 Naive baseline(对比)

"""naive_baseline.py — Hugging Face Transformers 朴素推理(不开任何优化)"""
import time
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

m = "meta-llama/Llama-3.2-3B-Instruct"  # 用小模型也行,比例类似
tok = AutoTokenizer.from_pretrained(m)
model = AutoModelForCausalLM.from_pretrained(m, torch_dtype=torch.bfloat16, device_map="auto")

prompts = [f"Explain financial concept #{i} in 200 words." for i in range(32)]

t0 = time.time()
for p in prompts:  # 串行!
    inputs = tok(p, return_tensors="pt").to(model.device)
    out = model.generate(**inputs, max_new_tokens=256, do_sample=False)
    _ = tok.decode(out[0], skip_special_tokens=True)
total = time.time() - t0
print(f"Naive: {32 / total:.2f} req/s, {total:.1f}s wall")

四、Cost & Performance 实测数据

4.1 vLLM vs Naive(A100 80G, Llama 3.2 3B, 256 req, max_tokens=256)

配置Wall timeThroughput (tok/s)TTFT (ms)GPU util
HF Transformers(串行)412 s19885028%
HF Transformers(batch=8)89 s920110065%
vLLM(continuous batch, prefix off)22 s3,72032088%
vLLM(continuous batch, prefix on14 s5,84018092%
加速比(vs naive)29.5x

4.2 自托管 vs API 单 token 成本(金融 PoC 测算,2026 价格)

选项输入 ($/MTok)输出 ($/MTok)备注
Anthropic claude-opus-4-7$15$75顶级,但贵
Anthropic claude-sonnet-4-6$3$15主力
Anthropic claude-haiku-4-5$0.80$4路由小任务
vLLM Llama 4 70B 自托管(H100 x 2,月租 $4800)≈ $0.35≈ $0.35QPS > 5 才划算
vLLM DeepSeek-V3 自托管(H100 x 4)≈ $0.20≈ $0.20671B MoE,37B activated

关键决策:金融机构内部知识 RAG,日 token 量 > 200M,自托管 break-even;面客高质量任务用 Anthropic claude-opus-4-7/sonnet-4-6。


五、金融领域应用

  1. 私有化部署合规要求:监管要求"数据不出域",国内金融机构必须自托管开源模型,vLLM 是事实标准
  2. 多租户隔离:私行客户、零售客户用不同模型实例,KV cache 不混
  3. 审计模式:vLLM 启动加 --max-log-len 4096 完整记录每次请求 prompt+output,对接审计系统
  4. 熔断:在 API gateway 层做 token-bucket,防止单个业务把 GPU 打爆影响交易系统
  5. 模型版本固定:金融生产禁止自动升级模型,model_id 必须 pin commit hash,eval pipeline 通过才能 promote

六、生产经验与陷阱

  1. max-model-len 不要等于模型 max:Llama 支持 128K,但你设 128K 会让 KV cache 预留过大、并发数骤降。按业务实际 P99 设(如 32K)
  2. gpu-memory-utilization 不是越高越好:> 0.95 时 CUDA 临时分配会 OOM。生产稳态 0.92
  3. prefix caching 的"陷阱命中":如果你的 system prompt 里嵌入了"今天日期"这种动态内容,prefix 永远不命中。把动态字段挪到 user message 末尾
  4. chunked prefill 与 TTFT--enable-chunked-prefill 让长 prompt 不阻塞短 prompt,但会让长 prompt 的 TTFT 略增(10-20%),交易类低延迟服务慎开
  5. Tensor parallel 的网络瓶颈:TP=8 跨节点时 NVLink/InfiniBand 必须就位,否则不如 TP=2 + 多副本
  6. 量化 fp8 vs awq 的精度差:金融数值任务用 fp8(H100),AWQ 在涉及 reasoning 的任务可能掉 1-3 个点,上线前必须 eval

七、关键速查

工具命令
启动 vLLMpython -m vllm.entrypoints.openai.api_server --model X --tensor-parallel-size 2
查看 KV cache 使用curl http://localhost:8000/metrics | grep kv_cache
健康检查curl http://localhost:8000/health
模型列表curl http://localhost:8000/v1/models
强制 cache 清空重启 vLLM(v0.6 暂无 hot 清理)
监控指标(vLLM /metrics)含义
vllm:num_requests_running当前并发
vllm:num_requests_waiting排队(>0 说明 saturate)
vllm:gpu_cache_usage_percKV cache 占用率(>0.95 接近 OOM)
vllm:time_to_first_token_secondsTTFT 直方图

八、面试题

  1. 解释 PagedAttention 解决了什么问题?为什么传统 attention 浪费显存?

    • 传统给每个 seq 预留 max_len 连续显存,60-80% 浪费。PagedAttention 借鉴 OS 虚拟内存,分 16 token block,按需分配,配 page table 索引,碎片率降到 < 4%
  2. vLLM continuous batching 与 static batching 的核心差异?

    • static 等 batch 全部完成才进下一批;continuous 在每 step 把已完成的 seq 替换为新请求,GPU 利用率 80%+
  3. 金融场景何时选 vLLM 自托管,何时用 Anthropic API?

    • 自托管:日 token > 200M、合规要求数据不出域、QPS > 5 持续;API:低 QPS / 突发流量、要 claude-opus 级 reasoning、新业务 PoC
  4. TTFT 高(>1s)怎么排查?

    • num_requests_waiting、prefill 是否被 chunked、prompt 长度分布、prefix cache hit rate、GPU mem 是否吃满导致 evict
  5. TP=4 还是 PP=4 怎么选?

    • 单卡装不下选 TP(latency 友好,all-reduce 通信开销中);多卡 throughput 优先选 PP(latency 高,吞吐高);70B 单机 H100 x 4 选 TP=4 即可

明日预告

Day 164:Cost 优化 — Anthropic Prompt Caching 与 Batch API 深入 cache_control: {"type": "ephemeral"} 的 5min/1hr TTL,复杂 RAG/Agent 场景下如何把成本砍 90%;Batch API 50% 折扣的 async 流程;从 token 经济学看模型选型梯度。