Skip to content

第17章 API 服务器与生产部署

"The last mile is often the hardest."

本章要点

  • 理解 OpenAI 兼容 API 的实现:endpoint 映射与参数转换
  • 掌握流式输出(SSE)的实现机制
  • 深入生产部署的关键配置:tensor-parallel-sizegpu-memory-utilization
  • 理解 vllm serve 命令的启动流程
  • 认识性能调优的实践经验

17.1 OpenAI 兼容层

源码vllm/entrypoints/openai/api_server.py(FastAPI 应用),vllm/entrypoints/openai/serving_chat.py(Chat Completions 处理)。

vLLM 的 API 服务器基于 FastAPI 构建(api_server.py:23),通过 uvicorn 运行。核心架构是一个 FastAPI app + EngineClient(连接推理引擎的客户端):

提供与 OpenAI API 规格兼容的端点:

端点功能
POST /v1/chat/completions多轮对话
POST /v1/completions文本补全
POST /v1/embeddings文本嵌入
GET /v1/models可用模型列表
GET /health健康检查

兼容的目的是让现有使用 OpenAI SDK 的应用可以零代码迁移到 vLLM——只需要修改 base_url

python
from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:8000/v1",  # 指向 vLLM
    api_key="not-needed",
)

response = client.chat.completions.create(
    model="meta-llama/Llama-2-7b-chat-hf",
    messages=[{"role": "user", "content": "Hello!"}],
    stream=True,
)

17.2 流式输出

流式输出通过 Server-Sent Events (SSE) 实现。每生成一个 Token,服务器立即推送一个事件:

data: {"id":"chatcmpl-xxx","choices":[{"delta":{"content":"Hello"}}]}

data: {"id":"chatcmpl-xxx","choices":[{"delta":{"content":" world"}}]}

data: {"id":"chatcmpl-xxx","choices":[{"finish_reason":"stop"}]}

data: [DONE]

在 V1 的多进程架构下,流式输出的路径是:

  1. Worker 生成 Token → 共享内存 → EngineCore
  2. EngineCore → ZMQ → API Server
  3. API Server 去分词 → SSE → 客户端

每个环节都是非阻塞的,确保 Token 生成后能尽快到达客户端。

流式输出的延迟分析

一个 Token 从生成到客户端接收,经历的延迟包括:

环节典型延迟备注
GPU 前向传播20-50 ms取决于模型大小和批次大小
Worker → EngineCore(共享内存)< 0.1 ms零拷贝
EngineCore → API Server(ZMQ)0.1-0.5 msIPC 模式
去分词0.01-0.1 msRust tokenizer 很快
SSE 序列化 + 网络传输0.5-5 ms取决于网络条件

总计:约 20-55 ms/Token。绝大部分时间花在 GPU 计算上,通信和处理开销只占 1-2%。这说明 V1 的多进程架构很好地隐藏了非 GPU 开销。

非流式输出

非流式模式(stream=false)下,服务器等待所有 Token 生成完毕后一次性返回完整结果。这种模式的总延迟 = 首 Token 延迟(TTFT)+ 解码延迟 × Token 数,但用户体验上感觉更"卡"(长时间等待后突然出现全部文本)。

在实际应用中,绝大多数面向用户的场景都应该使用流式模式——它让用户在第一个 Token 就看到响应,极大提升了感知速度。非流式模式适合 API 调用、批处理等后台任务。

17.3 vllm serve 启动流程

源码vllm/entrypoints/openai/api_server.py

vllm serve 命令是生产部署的主要入口。FastAPI 应用的生命周期由 lifespan 函数管理(api_server.py:108):

python
# vllm/entrypoints/openai/api_server.py:108-136
async def lifespan(app: FastAPI):
    try:
        if app.state.log_stats:
            # 启动后台统计日志任务(每 10 秒)
            async def _force_log():
                while True:
                    await asyncio.sleep(10.)
                    await engine_client.do_log_stats()
            task = asyncio.create_task(_force_log())

        # 关键优化:标记启动堆为静态,被 GC 忽略
        # 减少老年代回收的暂停时间
        gc.collect()
        gc.freeze()
        yield  # FastAPI 运行期间
    finally:
        del app.state  # 确保引擎引用被 GC

注意 gc.freeze() 这个精妙的优化——vLLM 在启动完成后将当前堆冻结为"静态",Python GC 不再扫描它。这避免了 GC 暂停影响推理延迟,对于需要毫秒级一致性的推理服务至关重要。

从命令行到服务就绪,经历以下步骤:

整个启动过程可能耗时 30 秒到 5 分钟,取决于模型大小、是否需要下载权重、是否启用 CUDA 图编译。在 Kubernetes 或容器编排环境中,应该设置足够的 startupProbe 超时。

17.4 生产部署配置

vllm serve 命令的常用参数及其影响:

bash
vllm serve meta-llama/Llama-2-70b-chat-hf \
    --tensor-parallel-size 4 \           # 4 卡张量并行
    --gpu-memory-utilization 0.9 \       # 使用 90% GPU 显存
    --max-model-len 4096 \               # 最大序列长度
    --max-num-batched-tokens 4096 \      # 每步最大 Token 数
    --max-num-seqs 256 \                 # 最大并发请求数
    --quantization fp8 \                 # FP8 量化
    --enable-prefix-caching \            # 前缀缓存(V1 默认启用)
    --port 8000

关键参数指南

tensor-parallel-size——设为模型所需的最少 GPU 数。70B FP16 需要至少 2 张 A100-80GB(140 GB / 80 GB ≈ 2)。FP8 量化后 1 张可能够用,但为了留足 KV Cache 空间,2 张更保险。

gpu-memory-utilization——默认 0.9。增大到 0.95 可以多容纳一些并发请求,但留给 CUDA 内核的临时空间更少,有 OOM 风险。

max-num-batched-tokens——影响分块预填充的粒度和每步延迟。在线服务建议 2048-4096;离线批处理可以设得更大(8192+)。

max-num-seqs——最大并发数。设太大会导致 KV Cache 分散在太多请求上,每个请求的可用块变少,增加抢占概率。

17.5 性能调优经验

场景一:在线对话服务

  • 优先降低延迟:适中的 max-num-batched-tokens(2048)
  • 启用前缀缓存(系统提示复用)
  • 考虑投机解码(提高单请求速度)

场景二:离线批量推理

  • 优先提高吞吐:增大 max-num-batched-tokens(8192+)和 max-num-seqs(512+)
  • 使用 LLM 类的离线接口(不启动 HTTP 服务)
  • 量化到 FP8 或 INT4 以增加并发

场景三:多租户 LoRA 服务

  • 启用 --enable-lora
  • 限制 --max-loras(同时活跃的 LoRA 数量)
  • 确保 LoRA 权重文件在本地或低延迟存储上

17.6 监控与可观测性

生产环境中,你需要监控 vLLM 的运行状态。vLLM 通过多种方式暴露指标:

健康检查

GET /health 返回引擎是否正常运行。Kubernetes 的 livenessProbereadinessProbe 应该指向这个端点。

Prometheus 指标

vLLM 内置了 Prometheus 指标导出。关键指标包括:

  • vllm:num_requests_running——当前正在运行的请求数。持续接近 max-num-seqs 说明系统已饱和
  • vllm:num_requests_waiting——等待队列长度。持续增长说明吞吐不足
  • vllm:gpu_cache_usage_perc——KV Cache 使用率。接近 100% 会触发抢占
  • vllm:avg_generation_throughput_toks_per_s——平均吞吐量(Token/秒)
  • vllm:avg_prompt_throughput_toks_per_s——预填充吞吐量

日志

vLLM 使用 Python 标准日志库,通过 VLLM_LOGGING_LEVEL 环境变量控制级别。生产环境建议设为 WARNING,调试时设为 INFODEBUG

--disable-log-requests 可以禁用每个请求的日志输出——在高 QPS 场景下,请求日志本身可能成为性能瓶颈。

分布式追踪

vLLM 支持 OpenTelemetry 集成(vllm/tracing/),可以将请求的延迟分解追踪(调度时间、GPU 时间、去分词时间等)导出到 Jaeger 或 Zipkin。

17.7 常见故障排查

现象可能原因解决方案
OOM (Out of Memory)gpu-memory-utilization 太高降低到 0.85,或减少 max-num-seqs
首 Token 延迟高长 Prompt + 未启用分块预填充设置合理的 max-num-batched-tokens
吞吐低于预期批次太小(并发不足)增加 max-num-seqs 或增加客户端并发
Worker 进程崩溃CUDA 错误或显存泄漏检查 docker logs,升级 CUDA 驱动
LoRA 切换慢LoRA 权重在远程存储预加载或缓存到本地 SSD

17.8 本章小结

  • OpenAI 兼容——零代码迁移现有应用
  • 流式输出——SSE 实现,多进程非阻塞流水线
  • 部署参数——TP size、显存利用率、批次大小、并发数的平衡
  • 调优经验——在线服务侧重延迟,离线推理侧重吞吐

源码导航

  • OpenAI API Server:vllm/entrypoints/openai/api_server.py
  • CLI 入口:vllm/entrypoints/cli/
  • 离线推理:vllm/entrypoints/llm.py

基于 VitePress 构建