graph TB
subgraph "调度三难"
A[延迟] --- B[吞吐]
B --- C[公平性]
C --- A
end
A --> "优先处理短请求"
B --> "等满再发"
C --> "FIFO 先来先服务"
第11章 推理服务治理
导入:当推理服务从 demo 走向生产
在 demo 阶段,推理服务很简单:一个 Docker 容器跑着 vLLM,你发个请求它返回结果,一切完美。但生产环境不是这样。
生产环境的推理服务要回答这些问题:高峰期请求暴涨 10 倍怎么办?VIP 客户和免费用户的请求混在一起怎么调度?模型更新上线时如何不影响正在使用的用户?一台 GPU 同时跑多个模型时如何隔离?
这些问题没有一个能在模型层面解决——它们需要的是服务治理。本章覆盖从请求调度到自动扩缩容、从多租户隔离到模型版本管理的完整治理体系。
11.1 动态批处理与请求调度
在上一章我们介绍了 Continuous Batching。这里我们关注的是:当请求到来时,如何决定它什么时候被送入 batch?
调度的核心矛盾
推理调度本质上是在做三个权衡:
- 最低延迟:请求一来就处理 → 低吞吐(batch 可能只有 1 个请求)
- 最大吞吐:等到 batch 满再处理 → 高延迟(等批凑齐的时间)
- 公平性:先来先服务 → 长请求拖慢整体
vLLM 的调度策略
vLLM 内置了两种调度策略:
# vLLM 调度器配置
from vllm import LLM
# 策略 1: "fcfs"(默认)— 先来先服务
llm_fcfs = LLM(model="llama-3-8b", scheduler_policy="fcfs")
# 策略 2: "priority" — 按优先级调度
llm_priority = LLM(model="llama-3-8b", scheduler_policy="priority")自定义调度:基于 SLA 的请求优先级
# 基于请求优先级的调度器(概念实现)
import asyncio
from dataclasses import dataclass, field
from time import time
import heapq
@dataclass(order=True)
class PrioritizedRequest:
priority: int # 优先级(数字越小越优先)
timestamp: float # 到达时间
request_id: str = field(compare=False)
payload: dict = field(compare=False)
class PriorityScheduler:
def __init__(self, max_batch_size: int = 32):
self.max_batch_size = max_batch_size
self.queue: list[PrioritizedRequest] = []
self.lock = asyncio.Lock()
async def submit(self, request_id: str, payload: dict, tier: str = "standard"):
priority_map = {"premium": 0, "standard": 1, "batch": 2}
req = PrioritizedRequest(
priority=priority_map.get(tier, 1),
timestamp=time(),
request_id=request_id,
payload=payload,
)
async with self.lock:
heapq.heappush(self.queue, req)
async def next_batch(self) -> list[PrioritizedRequest]:
async with self.lock:
batch = []
while self.queue and len(batch) < self.max_batch_size:
batch.append(heapq.heappop(self.queue))
return batch实用经验:在生产中,不要让请求无限排队。设置一个 max_queue_delay(如 5 秒),超时直接返回 503。这比让用户等 30 秒然后 OOM 好得多——用户端可以做重试。
11.2 自动扩缩容
推理服务的流量通常有明显的波峰波谷:白天高峰,凌晨低谷。固定副本数要么浪费资源,要么高峰期不够用。自动扩缩容是必需的。
Kubernetes HPA(Horizontal Pod Autoscaler)
K8s 原生的 HPA 可以基于 CPU/GPU 利用率自动扩缩。
# HPA 配置:基于 GPU 利用率扩缩容
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: vllm-llama-70b-hpa
namespace: inference
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: vllm-llama-70b
minReplicas: 2
maxReplicas: 8
metrics:
- type: Resource
resource:
name: nvidia.com/gpu
target:
type: Utilization
averageUtilization: 70 # GPU 利用率超过 70% 扩容
- type: Pods
pods:
metric:
name: vllm_num_requests_waiting # 自定义指标:等待中的请求数
target:
type: AverageValue
averageValue: "5" # 平均等待 > 5 个请求时扩容
behavior:
scaleDown:
stabilizationWindowSeconds: 300 # 缩容冷却 5 分钟
percentage: 50 # 每次最多缩 50%自定义指标扩缩容
HPA 基于 GPU 利用率有个问题:GPU 利用率 100% 不代表服务在饱和工作(可能在做有效计算,也可能在等待)。更准确的指标是请求等待队列深度和P99 延迟。
# 自定义指标导出器(Prometheus 格式)
from prometheus_client import Gauge, generate_latest
import http.server
# 定义自定义指标
QUEUE_DEPTH = Gauge('vllm_num_requests_waiting', 'Requests waiting in queue')
ACTIVE_REQUESTS = Gauge('vllm_num_requests_running', 'Requests currently generating')
GPU_MEM_USED = Gauge('gpu_memory_used_bytes', 'GPU memory used in bytes')
class MetricsHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
if self.path == '/metrics':
# 从 vLLM 的 /metrics 端点获取数据
# 也可以从 GPU exporter 获取
self.send_response(200)
self.send_header('Content-Type', 'text/plain')
self.end_headers()
self.wfile.write(generate_latest().encode())
def log_message(self, format, *args):
pass # 静默日志
# 启动 metrics 服务器
server = http.server.HTTPServer(('0.0.0.0', 9100), MetricsHandler)
server.serve_forever()KPA(Knative Pod Autoscaler)
如果你用 Knative,KPA 能基于并发请求数(而非 CPU 利用率)自动扩缩——这比 HPA 更适合推理场景。
# Knative Service 配置
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: vllm-llama-8b
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/class: kpa.autoscaling.knative.dev
autoscaling.knative.dev/min-scale: "2" # 最少 2 副本
autoscaling.knative.dev/max-scale: "20" # 最多 20 副本
autoscaling.knative.dev/target: "10" # 每副本目标并发 10
autoscaling.knative.dev/scale-down-delay: "5m" # 缩容延迟
spec:
containers:
- image: vllm/vllm-openai:latest
resources:
limits:
nvidia.com/gpu: 1
args:
- --model
- meta-llama/Llama-3-8B-Instruct
- --max-model-len
- "4096"扩容容易缩容难:扩容是新起 Pod,几乎不丢请求。缩容是杀 Pod,正在处理的请求可能中断。务必配置 terminationGracePeriodSeconds 足够长(如 300s),让推理实例优雅完成正在处理的请求。同时从负载均衡器中摘除即将终止的实例。
11.3 延迟敏感型与吞吐敏感型场景设计
不同的业务场景对延迟和吞吐的要求完全不同:
| 场景 | 延迟要求 | 吞吐要求 | 典型模型 |
|---|---|---|---|
| 实时对话 | TTFT < 500ms | 低-中 | LLM (Chat) |
| 代码补全 | TTFT < 200ms | 低 | LLM (Coder) |
| RAG 检索增强 | TTFT < 1s | 中 | LLM + Embedding |
| 文档摘要 | < 30s | 高 | LLM |
| Embedding 生成 | < 100ms/条 | 极高 | Embedding Model |
| 批量数据标注 | 无要求 | 极高 | LLM |
延迟敏感型:实时对话
graph LR
A[用户发消息] --> B[网关]
B --> C[推理实例\nstreaming 模式]
C -->|SSE| D[逐 token 返回]
D --> E[前端渲染]
style A fill:#ff9
style D fill:#9f9
关键优化: - 流式输出:首 token 时间(TTFT)比总时间更重要 - 小模型优先:用 8B 而不是 70B,除非质量差距不可接受 - Prefix Caching:system prompt 和对话历史缓存 - 低并发:每副本只接 5-10 个并发,保证延迟
吞吐敏感型:批量处理
# 高吞吐离线处理架构
import asyncio
import aiohttp
class BatchProcessor:
"""高吞吐批量处理器"""
def __init__(self, endpoint: str, max_concurrent: int = 100):
self.endpoint = endpoint
self.semaphore = asyncio.Semaphore(max_concurrent)
async def process_batch(self, items: list[dict]):
"""并发提交所有请求"""
async with aiohttp.ClientSession() as session:
tasks = [self._process_one(session, item) for item in items]
return await asyncio.gather(*tasks)
async def _process_one(self, session, item):
async with self.semaphore:
async with session.post(
f"{self.endpoint}/v1/completions",
json={
"model": "llama-3-8b",
"prompt": item["prompt"],
"max_tokens": 256,
"temperature": 0.0, # 确定性输出
},
timeout=300,
) as resp:
result = await resp.json()
return {"id": item["id"], "output": result["choices"][0]["text"]}
# 处理 10 万条数据
processor = BatchProcessor(endpoint="http://vllm-8b:8000", max_concurrent=200)
results = await processor.process_batch(all_items)分而治之:如果一个集群同时服务实时对话和批量处理,将它们部署到不同的命名空间/节点组。批量处理的请求可以随时被暂停和恢复,但实时对话不行。用 K8s 的 ResourceQuota 和 PodPriority 确保实时任务优先获得资源。
11.4 模型版本管理与 A/B 测试
模型不是静态的——你会不断更新 prompt、微调权重、切换模型版本。如何安全地管理这些变更?
模型注册中心
graph TB
subgraph "模型生命周期"
A[训练完成] --> B[模型注册中心]
B --> C{审批}
C -->|通过| D[Staging 环境]
D --> E{A/B 测试}
E -->|通过| F[Production]
E -->|失败| G[回滚]
F --> H[流量迁移]
end
# 使用 MLflow 作为模型注册中心(简化示例)
import mlflow
# 注册新模型版本
mlflow.set_tracking_uri("http://mlflow:5000")
model_name = "llama-3-8b-instruct"
model_uri = "models:/llama-finetuned/v1"
# 注册新版本
model_version = mlflow.register_model(
model_uri=model_uri,
name=model_name,
)
# 将新版本移到 Staging
client = mlflow.tracking.MlflowClient()
client.transition_model_version_stage(
name=model_name,
version=model_version.version,
stage="Staging",
)
# Production 流量迁移(Canary)
client.transition_model_version_stage(
name=model_name,
version=model_version.version,
stage="Production",
archive_existing_versions=False, # 不立即归档旧版本
)A/B 测试与 Canary 发布
# 使用 Istio 做 Canary 发布
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: llama-model-canary
spec:
http:
- match:
- headers:
x-model-version:
exact: "v2"
route:
- destination:
host: vllm-llama-8b-v2
port:
number: 8000
- route:
- destination:
host: vllm-llama-8b-v1
port:
number: 8000
weight: 90 # 90% 流量到 v1
- destination:
host: vllm-llama-8b-v2
port:
number: 8000
weight: 10 # 10% 流量到 v2(Canary)# 应用层 A/B 路由(更灵活)
import random
def route_request(request_data: dict) -> str:
"""根据用户 ID 做一致性 A/B 路由"""
user_id = request_data.get("user_id", "")
# 基于用户 ID hash 做一致性分配
hash_val = hash(user_id) % 100
if hash_val < 10: # 10% 到新版本
return "http://vllm-llama-8b-v2:8000"
else: # 90% 到旧版本
return "http://vllm-llama-8b-v1:8000"A/B 测试的一致性:同一个用户应该始终看到同一个模型版本。不要用随机数路由——用用户 ID 的 hash。否则用户会感受到不同版本之间的不一致,体验极差。
11.5 GPU 共享与多租户隔离
GPU 很贵(H100 约 $30,000)。如果一个模型用一个 GPU,大部分时间 GPU 利用率不到 20%。GPU 共享是降低成本的关键。
三种共享模式
graph TB
subgraph "时空分片 Time-Slicing"
TS1[GPU] --> TS2[进程 A\n时间片 1]
TS1 --> TS3[进程 B\n时间片 2]
end
subgraph "MIG Multi-Instance GPU"
M1[GPU] --> M2[实例 1\n隔离的 SM + 内存]
M1 --> M3[实例 2\n隔离的 SM + 内存]
M1 --> M4[实例 3\n隔离的 SM + 内存]
end
subgraph "多模型同进程"
MM1[推理框架] --> MM2[模型 A]
MM1 --> MM3[模型 B]
MM1 --> MM4[共享 KV Cache 池]
end
NVIDIA MIG(硬件级隔离)
MIG(Multi-Instance GPU)是 H100/A100 的硬件功能,可以将一张 GPU 物理切分为多个隔离实例,每个实例有独立的 SM(流多处理器)和内存。
# 启用 MIG(需要在 GPU 节点上执行)
sudo nvidia-smi -i 0 -mig 1
# 切分为 7 个实例(每个 1g.10gb)
sudo nvidia-smi mig -cgi 19,19,19,19,19,19,19 -C
# 查看实例
sudo nvidia-smi mig -lgi
# K8s 中使用 MIG 设备
# Pod 配置申请 MIG 实例# K8s Pod 使用 MIG 实例
apiVersion: v1
kind: Pod
metadata:
name: vllm-small-model
spec:
containers:
- name: vllm
image: vllm/vllm-openai:latest
args:
- --model
- meta-llama/Llama-3-1B
resources:
limits:
nvidia.com/mig-1g.10gb: 1 # 申请 1 个 MIG 实例(10GB)时间分片(Time-Slicing)
MIG 只在 A100/H100 上支持。对于消费级 GPU(如 A10G、L4),可以用 NVIDIA 的时间分片驱动:
# K8s Device Plugin 配置时间分片
# nvidia-device-plugin 的 ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: nvidia-device-plugin-config
data:
config.yaml: |
version: v1
sharing:
timeSlicing:
resources:
- name: nvidia.com/gpu
replicas: 4 # 1 张卡共享给 4 个 Pod时间分片不是真隔离:时间分片是时分复用——多个进程轮流使用 GPU。如果一个进程突然来了一个大计算任务,其他进程的延迟会受影响。对于延迟敏感场景,不要用时间分片,用 MIG。
多租户公平性保障
当多个团队共享一个推理集群时,需要防止”吵闹的邻居”:
# K8s ResourceQuota 限制每个团队的资源
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-a-quota
namespace: team-a
spec:
hard:
requests.nvidia.com/gpu: "4" # 最多申请 4 张 GPU
requests.memory: "200Gi"
pods: "20"
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-b-quota
namespace: team-b
spec:
hard:
requests.nvidia.com/gpu: "2"
requests.memory: "100Gi"
pods: "10"# PriorityClass 确保高优先级任务优先获得 GPU
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: inference-production
value: 1000
globalDefault: false
description: "Production inference workloads"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: inference-experiment
value: 100
globalDefault: false
description: "Experimental inference workloads, can be preempted"小结
推理服务治理是从”能跑”到”能稳定服务”的关键一步。本章覆盖了五个治理维度:
- 请求调度:动态批处理在延迟和吞吐间找平衡,优先级调度保障 SLA 分层
- 自动扩缩容:HPA 基于 GPU 利用率,KPA 基于并发数,自定义指标更精准
- 场景适配:延迟敏感型用流式 + 低并发,吞吐敏感型用高并发 + 确定性输出
- 版本管理:模型注册中心 + A/B 测试 + Canary 发布,确保安全变更
- 多租户隔离:MIG 做硬件隔离,ResourceQuota + PriorityClass 做软隔离
治理的本质是在有限资源下,让不同的负载各得其所。这不是技术问题——这是一个系统工程问题。
延伸阅读
- Kubernetes HPA 文档:https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/
- NVIDIA MIG 文档:https://docs.nvidia.com/datacenter/tesla/mig-user-guide/
- “Serving LLMs at Scale: Lessons from Production” — Anyscale, 2024
- Knative Autoscaling 文档:https://knative.dev/docs/serving/autoscaling/
- “LLM Serving: A Survey of Techniques, Systems, and Challenges” — Zhang et al., 2024