graph TB
subgraph "推理框架生态"
A[用户请求] --> B{路由层}
B --> C[vLLM]
B --> D[TensorRT-LLM]
B --> E[Triton]
B --> F[SGLang]
C --> G[PagedAttention + Continuous Batching]
D --> H[Kernel Fusion + INT8/FP8]
E --> I[TensorRT / PyTorch / ONNX 后端]
F --> J[RadixAttention + 结构化解码]
G --> K[GPU]
H --> K
I --> K
J --> K
end
第9章 模型服务架构
导入:从模型权重到生产服务
你花了三周训练出一个模型。损失曲线漂亮,评估指标达标。然后你把模型权重交给工程团队,他们说:“好,我们怎么把它变成一个每秒处理上千请求的服务?”
这就是推理服务的核心问题。训练解决的是”模型能不能用”,服务解决的是”模型能不能被用起来”。
一个模型文件可能是几十 GB 的权重二进制。用户发一个请求,你需要在毫秒级加载权重到 GPU、执行前向计算、返回结果——同时还有 999 个请求在排队。这不是简单的”调用一个函数”。
本章覆盖推理服务架构的核心问题:用什么框架、什么部署模式、如何并行、如何路由。读完本章,你将理解从单个模型文件到生产级推理服务的完整路径。
9.1 推理服务框架全景
2024 年之前,推理框架的选择并不多——大部分团队用 HuggingFace Transformers 的 model.generate() 凑合着跑。但随着大模型的普及,推理框架爆发式发展。今天的主流框架各有侧重:
| 框架 | 核心优势 | 典型场景 |
|---|---|---|
| vLLM | PagedAttention,高吞吐 | 通用 LLM 服务(最流行) |
| TensorRT-LLM | 极致 GPU 利用率 | NVIDIA 硬件 + 低延迟场景 |
| Triton Inference Server | 多框架后端统一管理 | 混合模型管线 |
| SGLang | 结构化生成优化 | Agent/工具调用场景 |
vLLM:当前的事实标准
vLLM 由 UC Berkeley 团队开发,凭借 PagedAttention 技术一炮走红。它的核心创新是将 KV Cache 按”页”管理(类似操作系统的虚拟内存),避免了传统方法中大量的内存碎片。
# vLLM 基本使用
from vllm import LLM, SamplingParams
llm = LLM(
model="meta-llama/Llama-3-70B",
tensor_parallel_size=4, # 4 卡张量并行
gpu_memory_utilization=0.90, # 限制 GPU 内存使用
max_model_len=8192, # 最大序列长度
)
sampling = SamplingParams(
temperature=0.7,
top_p=0.9,
max_tokens=512,
)
outputs = llm.generate(["解释什么是 Transformer"], sampling)启动 OpenAI 兼容的 API 服务器:
# 使用 vLLM 的官方镜像启动服务
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Llama-3-8B-Instruct \
--port 8000 \
--tensor-parallel-size 2 \
--gpu-memory-utilization 0.9 \
--max-model-len 4096 \
--enable-prefix-caching选型建议:如果你不确定用什么,先用 vLLM。它的社区最活跃、文档最全、兼容性最好。只有在 vLLM 无法满足需求时(如极致延迟、特殊硬件),才考虑其他框架。
TensorRT-LLM:追求极致性能
TensorRT-LLM 是 NVIDIA 官方推出的大模型推理优化库。它在底层做了大量 kernel fusion(算子融合),减少 GPU kernel launch 开销。对于延迟敏感场景(如实时对话),TensorRT-LLM 通常能比 vLLM 快 30-50%。
代价是构建复杂:你需要先将模型编译为 TensorRT 引擎。
# Step 1: 构建 TensorRT 引擎
# 需要使用 TensorRT-LLM 的 build.py
python tensorrt_llm/examples/llama/build.py \
--model_dir meta-llama/Llama-3-8B \
--dtype float16 \
--use_gpt_attention_plugin \
--use_gemm_plugin \
--output_dir ./trt_engines/llama-3-8b/
# Step 2: 启动 Triton + TensorRT-LLM 后端
# 使用 Triton Inference Server 加载引擎TensorRT-LLM 的引擎与具体 GPU 型号绑定。在 A100 上编译的引擎不能直接在 H100 上使用。如果你的部署环境包含多种 GPU 型号,需要为每种型号分别编译。
Triton Inference Server:多后端统一管理
Triton 的定位不是”又一个推理框架”,而是一个推理服务的编排层。它支持多种后端(TensorRT、PyTorch、ONNX Runtime、Python),让你在一个服务器上同时管理多个不同框架的模型。
# Triton 模型配置示例:config.pbtxt
name: "llama-3-8b"
backend: "tensorrtllm"
max_batch_size: 256
dynamic_batching {
preferred_batch_size: [4, 8, 16, 32]
max_queue_delay_microseconds: 100000
}
instance_group [
{
count: 1
kind: KIND_GPU
}
]SGLang:为结构化生成而生
SGLang 的核心创新是 RadixAttention——用基数树复用前缀的 KV Cache。对于 Agent 场景(大量重复的 system prompt + 工具定义),SGLang 能显著降低延迟。
# SGLang 的结构化生成
import sglang as sgl
@sgl.function
def multi_step_reasoning(s, question):
s += "You are a helpful assistant."
s += f"\nQuestion: {question}"
s += "\nLet's think step by step." + sgl.gen("reasoning", max_tokens=256)
s += "\nAnswer: " + sgl.gen("answer", max_tokens=64)
# RadixAttention 自动复用 "You are a helpful assistant." 的 KV Cache
state = multi_step_reasoning.run(question="什么是模型量化?")9.2 在线服务与离线批处理架构
推理有两种根本不同的工作模式:在线服务(实时响应)和离线批处理(吞吐优先)。它们的架构设计完全不同。
在线服务:延迟为王
在线服务要求用户请求在感知时间内返回(通常 < 2 秒首 token)。核心约束是延迟。
graph LR
A[用户请求] --> B[API Gateway]
B --> C[负载均衡]
C --> D[推理实例 1]
C --> E[推理实例 2]
C --> F[推理实例 N]
D --> G[Streaming Response]
E --> G
F --> G
G --> H[用户]
关键设计点: - 流式输出:不要等整个回答生成完才返回,用 SSE/WebSocket 逐 token 返回 - 预热:服务启动时先做几次 dummy 推理,触发 CUDA kernel 编译和内存分配 - 超时控制:设置生成超时,避免个别长请求占住资源
离线批处理:吞吐优先
当你需要处理百万级文档(如 embedding 生成、数据标注),在线服务的逐条处理太慢了。离线批处理的目标是最大化 GPU 利用率。
# 离线批处理示例:用 vLLM 批量生成
from vllm import LLM, SamplingParams
llm = LLM(model="meta-llama/Llama-3-8B-Instruct", tensor_parallel_size=4)
# 一次性提交所有请求,vLLM 内部自动做 continuous batching
sampling = SamplingParams(temperature=0.0, max_tokens=256)
prompts = [f"对以下文本做摘要:{doc}" for doc in documents] # 100 万条
outputs = llm.generate(prompts, sampling) # vLLM 自动分批
# 保存结果
for output in outputs:
results.append({
"prompt": output.prompt,
"generated": output.outputs[0].text,
})架构选择经验:embedding 生成就用离线批处理,不要搭在线服务。模型对话就用在线服务。如果两者都有,分开部署——不要试图用一个集群同时服务两种负载模式,它们对资源的争夺会导致两边都做不好。
9.3 推理中的模型并行与流水线并行
当模型大到单卡放不下时(比如 70B 模型在 FP16 下需要约 140GB),你需要多卡并行。推理中的并行策略与训练类似,但约束不同——推理不需要反向传播,所以通信模式更简单。
graph TB
subgraph "张量并行 TP"
direction LR
T1["GPU 0\n(列切分)"]
T2["GPU 1\n(列切分)"]
T1 <-->|All-Reduce| T2
end
subgraph "流水线并行 PP"
direction LR
P1["GPU 0\n(Layer 1-32)"]
P2["GPU 1\n(Layer 33-64)"]
P1 -->|Send| P2
end
张量并行(Tensor Parallelism)
将每一层的权重矩阵切分到多张卡上。推理时每张卡计算部分结果,然后通过 All-Reduce 合并。
# vLLM 张量并行:4 卡
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Llama-3-70B \
--tensor-parallel-size 4张量并行的优点是延迟低(每层都在并行计算),缺点是通信开销大。对于推理,TP=2 或 TP=4 是最常见的配置。
流水线并行(Pipeline Parallelism)
将模型按层切分,GPU 0 处理第 1-32 层,GPU 1 处理第 33-64 层。推理时输入先过 GPU 0,再传给 GPU 1。
流水线并行的通信量小(只传激活值,不传完整梯度),但会引入流水线气泡——前面的 GPU 在工作时,后面的 GPU 空闲。推理场景下,可以通过 micro-batching 缓解这个问题。
实用建议:优先使用张量并行。只有当 TP=8 仍然放不下模型时(比如 405B 级别),才叠加流水线并行。TP=4 + PP=2 通常比 TP=8 通信开销更优,因为 NVLink 拓扑结构通常以 4 卡为一组。
9.4 多副本部署与负载均衡
单个推理实例的吞吐是有限的。当请求量增长,你需要部署多个副本,并在前面放一个负载均衡器。
但推理负载均衡和传统 Web 服务有一个关键区别:请求的长度差异巨大。一个 “hi” 和一个 4000 token 的文档摘要请求,占用资源完全不同。简单轮询负载均衡会导致严重的负载不均。
感知负载的均衡策略
# 基于队列长度的负载均衡(简化版)
import aiohttp
from collections import defaultdict
class InferenceLoadBalancer:
def __init__(self, backends: list[str]):
self.backends = backends
self.queue_depth = defaultdict(int) # 每个后端的待处理请求数
async def route(self, request: dict):
# 选择队列最短的后端
target = min(self.backends, key=lambda b: self.queue_depth[b])
self.queue_depth[target] += 1
try:
async with aiohttp.ClientSession() as session:
async with session.post(
f"{target}/v1/completions",
json=request
) as resp:
return await resp.json()
finally:
self.queue_depth[target] -= 1更成熟的方案是使用推理框架自带的指标(如 vLLM 的 num_requests_waiting)做基于指标的调度。
避免简单 Round-Robin:如果你的请求长度方差大(有的 10 token,有的 2000 token),Round-Robin 会导致某些副本积压大量长请求,而其他副本空闲。用 least-connections 或基于队列深度的调度。
9.5 推理网关设计
推理网关是用户和模型之间的”前台”。它不是可选的——一旦你的模型服务需要面向多个用户或多个团队,网关就是必需品。
graph TB
Client[客户端] --> GW[推理网关]
GW --> Auth{鉴权}
Auth -->|API Key| RL[限流器]
Auth -->|拒绝| Err[401 Unauthorized]
RL --> Router{模型路由}
RL -->|超限| Rate429[429 Too Many Requests]
Router --> M1[模型 A v1]
Router --> M2[模型 A v2]
Router --> M3[模型 B]
subgraph "网关职责"
Auth
RL
Router
end
鉴权
最基本的要求:不是所有人都能调用你的推理服务。
# FastAPI 推理网关示例
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import StreamingResponse
import httpx
app = FastAPI()
VALID_API_KEYS = {"team_a": "sk-aabb...", "team_b": "sk-ccdd..."}
@app.middleware("http")
async def auth_middleware(request: Request, call_next):
api_key = request.headers.get("Authorization", "").replace("Bearer ", "")
if api_key not in VALID_API_KEYS.values():
raise HTTPException(status_code=401, detail="Invalid API key")
return await call_next(request)
# 模型路由
MODEL_ENDPOINTS = {
"llama-3-70b": "http://vllm-70b:8000",
"llama-3-8b": "http://vllm-8b:8000",
"qwen-72b": "http://sglang-qwen:8000",
}
@app.post("/v1/chat/completions")
async def proxy(request: Request):
body = await request.json()
model_name = body.get("model", "llama-3-8b")
endpoint = MODEL_ENDPOINTS.get(model_name)
if not endpoint:
raise HTTPException(status_code=404, detail=f"Model {model_name} not found")
async def stream():
async with httpx.AsyncClient() as client:
async with client.stream(
"POST", f"{endpoint}/v1/chat/completions",
json=body, timeout=120
) as resp:
async for chunk in resp.aiter_bytes():
yield chunk
return StreamingResponse(stream(), media_type="text/event-stream")限流
限流不仅是保护后端,更是成本控制的手段。常见的限流维度:
| 维度 | 示例 | 说明 |
|---|---|---|
| RPM(每分钟请求数) | 60 RPM | 防止单用户刷接口 |
| TPM(每分钟 Token 数) | 100K TPM | 按实际计算量限制 |
| 并发数 | 5 并发 | 防止长请求堆积 |
# Token-based 限流(令牌桶)
import time
from collections import defaultdict
class TokenBucketLimiter:
def __init__(self, capacity: int, refill_rate: float):
self.capacity = capacity # 桶容量
self.refill_rate = refill_rate # 每秒补充速率
self.buckets = defaultdict(lambda: {"tokens": capacity, "last": time.time()})
def allow(self, key: str, cost: int = 1) -> bool:
bucket = self.buckets[key]
now = time.time()
elapsed = now - bucket["last"]
bucket["tokens"] = min(self.capacity, bucket["tokens"] + elapsed * self.refill_rate)
bucket["last"] = now
if bucket["tokens"] >= cost:
bucket["tokens"] -= cost
return True
return False
# 配置:每用户每分钟 10000 tokens
limiter = TokenBucketLimiter(capacity=10000, refill_rate=10000 / 60)路由
路由层决定请求发给哪个模型。常见策略:
- 精确路由:请求指定
model: llama-3-70b,直接路由到对应实例 - 回退路由:70B 不可用时,自动回退到 8B
- 灰度路由:10% 流量打到新版本模型
网关设计的关键原则:网关层应该是无状态的。所有状态(API Key、限流计数器)存在 Redis 中。这样网关可以水平扩展,且单点故障不影响服务。
小结
模型服务架构是推理系统的骨架。本章覆盖了四个层次:
- 框架层:vLLM 适合大多数场景,TensorRT-LLM 适合极致延迟,Triton 适合多模型管理,SGLang 适合结构化生成
- 部署模式:在线服务追求延迟,离线批处理追求吞吐,两者架构完全不同
- 并行策略:张量并行优先,流水线并行补充,根据模型大小和硬件拓扑选择组合
- 基础设施:多副本 + 负载均衡保证可用性,推理网关统一鉴权、限流和路由
下一章将深入推理优化技术——如何在不损失精度的前提下,让推理更快、更省。
延伸阅读
- vLLM 官方文档:https://docs.vllm.ai — 最权威的 vLLM 使用指南
- TensorRT-LLM GitHub:https://github.com/NVIDIA/TensorRT-LLM — NVIDIA 官方实现
- SGLang 论文:“SGLang: Efficient Execution of Structured Language Model Programs” — RadixAttention 原理
- Triton Inference Server 文档:https://docs.nvidia.com/deeplearning/triton-inference-server/
- Chip Huyen, AI Engineering Chapter 7-8 — 推理服务设计的系统性讨论