Skip to content

部署设计原则

本页速览 推理部署项目失败很少因为引擎不够先进,更多死于设计错误。本文提炼十二条推理部署设计原则:延迟优先 vs 吞吐优先、显存预算、流式优于等结果、三层查瓶颈、量化看激活异常值、投机解码看 batch、灰度先 5%、监控 KV 命中率、故障演练、端到端基准等。

部署设计原则

推理部署项目的成败,在写第一行服务代码之前就已经决定了。

初学者常把注意力放在"用哪个引擎"上——vLLM 还是 TensorRT-LLM?AWQ 还是 GPTQ?投机解码还是不投机?但真实的部署复盘一次又一次告诉我们:引擎很少是瓶颈,设计决策才是。延迟优先还是吞吐优先没定,整个调参方向就错了;显存预算没算清,上线直接 OOM;流式 vs 非流式没选,TTFT 永远压不下;灰度策略没有,第一次上线就事故。NVIDIA 在 Deploying LLMs in Production 里反复强调:

"推理服务的稳定性,不是引擎选出来的,是设计出来的。"

本文给出 12 条可落地的推理部署设计原则,每条都回答三个问题:为什么这条原则重要、怎么做才算做到、最常见的反例长什么样。它们不要求你先学完所有引擎——恰恰相反,这些原则大多写在引擎之前。读完 从零部署一个推理服务 建立整体流程后,本文给每一步加上"纪律"。

本文的定位

本文是"流程层"文章:谈的是怎么组织一个推理部署项目,而不是怎么用某个引擎。本文的反面清单在 常见陷阱与反模式 里,调参的具体参数在 调参与性能调优 里。

一、十二条原则速览

#原则一句话反例标志
1延迟优先 vs 吞吐优先先选一个别都想要,二选一是调参的起点既要 p99 200ms 又要单卡 1000 tok/s
2显存预算先算清权重 + KV cache + 临时 = 总预算上线直接 OOM
3预处理/后处理与模型同服务减少网络跳,降低 p99tokenizer 单独跑一个服务,TTFT 翻倍
4流式优于等结果改善首字延迟,体验工程聊天接口非流式,TTFT 拉满
5模型+算子+系统三层都要查瓶颈不要只看 GPU 利用率GPU 100% 但慢,因为 memory-bound
6量化前看激活异常值分布异常值多的模型量化崩LLaMA 系量化掉精度,不知道为啥
7投机解码看 batch 大小大 batch 是反向收益大 batch 上 EAGLE 反而慢
8灰度发布先 5% 流量新配置先小流量验证全量上 AWQ,业务崩
9监控不只是 QPS还有 KV cache 命中率、温度QPS 看着正常,p99 悄悄飞涨
10故障演练常态化别等真崩了才发现没预案kill -9 一个实例,全站崩
11量化是 trade-off,不是免费午餐精度损失总在,要量它上了 INT4 业务质量悄悄掉
12端到端基准而非单算子基准用户感受到的是端到端单算子 benchmark 漂亮,端到端慢

这 12 条串成一个部署闭环

        ┌───────────────────────────────────────────┐
        │       业务 SLA 决定方向(原则 1、4)       │
        └───────────────────┬───────────────────────┘

        ┌───────────────────────────────────────────┐
        │      显存预算与配置(原则 2)              │
        └───────────────────┬───────────────────────┘

   ┌─────────────┬──────────┴───────────┬───────────────┐
   ▼             ▼                      ▼               ▼
 引擎调参     流式设计                 量化决策        投机解码决策
 (原则 5、9)  (原则 3、4)              (原则 6、11)    (原则 7)
   └─────────────┴──────────┬───────────┴───────────────┘

        ┌───────────────────────────────────────────┐
        │   灰度发布 + 监控(原则 8、9、10)          │
        └───────────────────┬───────────────────────┘

        ┌───────────────────────────────────────────┐
        │   端到端基准验证(原则 12)→ 回到调参        │
        └───────────────────────────────────────────┘

一个常见误解

"设计原则"听起来像理论说教,但它其实是省时间的方法论。跳过显存预算,你可能在 OOM 后才返工;跳过流式设计,你可能在用户投诉"卡"之后才改接口;跳过灰度发布,你可能在事故复盘后才开始写预案。原则不是束缚,是把你从"上线即事故"里捞出来的锚。

二、逐条展开

原则 1:延迟优先 vs 吞吐优先,先选一个

为什么。 这两个目标在工程实现上是直接冲突的——延迟优先要小 batch 保 p99,吞吐优先要大 batch 拉并发。同时追求两者,调参时一会儿加大 max_num_seqs、一会儿调小,永远找不到稳定配置。第一件事是把业务问清楚:用户在等结果,还是后台批处理? 这决定了 90% 的调参方向。

怎么做。 用下面这张表二选一:

业务形态优先方向max_num_seqsenable_chunked_prefill投机解码
聊天 / 代码补全延迟优先16–32开(小 batch 受益)
文档摘要 / 翻译吞吐优先64–128关(大 batch 反退化)
离线批量吞吐优先256+
RAG 问答延迟优先(用户在等)16–32

定义清楚后写进部署文档,每次调参都对照"我现在的方向是不是和最初定义一致"。详见 调参与性能调优延迟、吞吐与并发

反例。 最经典反例:聊天服务为了"成本省一点"把 max_num_seqs 调到 64,结果 p99 从 200ms 飞涨到 1200ms——用户体感"卡死了",业务投诉爆炸,最后回滚到 32。回滚后还多花了一周找其他方向调参。第一个决策错了,后面所有调参都是在错误方向上努力。第二个反例:不知道自己要哪个,先"均衡",结果两边都不好——延迟不达 SLA、吞吐也不满。均衡不等于好,均衡等于两边都不达标。

原则 2:显存预算先算清

为什么。 LLM 推理显存有三层:权重 + KV cache + 临时。不算清就开始调参,等于盲调——你以为显存够开 64 并发,实际一跑 KV pool 撑爆。这是 显存层次与带宽墙 的工程落地。

怎么做。 部署前用公式算清预算:

总显存 = 权重 + KV cache + 临时

权重:bf16 模型 = 2 × num_params bytes
       AWQ INT4 = 0.5 × num_params bytes

KV cache = 2 × num_layers × hidden_size × max_model_len × max_num_seqs × dtype_bytes

临时:≈ 1–2 GB(attention 中间矩阵、kernel 临时)

示例:Llama-2-7B bf16, max_model_len=4096, max_num_seqs=32
  权重 = 14 GB
  KV cache = 2 × 32 × 128 × 4096 × 32 × 2 bytes = 8 GB
  临时 = 1.5 GB
  总 = 23.5 GB  ← A100 40GB 放得下

预算算清后,每个参数变化都要问"是否还在预算内"。详见 调参与性能调优 的 KV cache 公式。

反例。 反例一:团队"凭感觉"调 max_num_seqs=128,上线 OOM——KV cache 占了 32 GB,权重 14 GB,临时 1.5 GB,总 47 GB 超过 A100 40GB。反例二:预算只算权重不算 KV cache,部署 70B 模型到单卡 A100 80GB,权重 140 GB 直接爆——必须用 TP=2 才放得下。反例三:预算只算"刚好够",没留临时空间——长 prompt 时 attention 中间矩阵把显存撑爆。预算要按"权重 + KV + 临时 + 20% 余量"算

原则 3:预处理/后处理与模型同服务

为什么。 推理服务不是只有 LLM——前面有 tokenizer、prompt 模板,后面有 detokenizer、内容过滤、格式化。把这些拆成独立服务,每次请求多两个网络跳,p99 延迟轻易翻倍。网络跳的代价在 LLM 场景尤其大——LLM 推理本身就几百 ms,多加 50ms 网络看起来不多,但 p99 会拉到 2× 以上(详见排队论)。

怎么做。 tokenizer、prompt 模板、detokenizer、内容过滤都放进同一个服务进程

✓ 推荐架构:

  HTTP 请求 ─→ FastAPI ─→ tokenizer ─→ vLLM ─→ detokenizer ─→ 内容过滤 ─→ HTTP 响应
                 (一个进程内)                                  (一个进程内)


✗ 反模式:

  HTTP 请求 ─→ tokenizer 服务 ─→ HTTP ─→ vLLM ─→ HTTP ─→ detokenizer 服务 ─→ HTTP ─→ 内容过滤服务 ─→ HTTP
   (5 个网络跳,p99 翻倍)

唯一例外是 embedder / reranker 这种独立模型——它们必须独立服务(不同模型权重),但仍然建议放在同主机减少网络开销。详见 模型服务化与编排

反例。 反例一:微服务教条化,把 tokenizer 拆成独立服务"为了可复用"——结果 p99 翻倍,可复用性没带来收益。反例二:内容过滤放在客户端,但客户端实现不一致,导致生产过滤行为漂移。反例三:把后处理(如 JSON 解析)放在异步队列"不阻塞响应"——用户看到响应里没有结构化字段,又得轮询拿结果,等于多了一次往返。

原则 4:流式优于等结果

为什么。 用户的"卡顿感"主要由首字延迟(TTFT)决定,不是端到端延迟。同样 5 秒生成完成,流式让用户在 200ms 看到第一个字、感受是"在思考",非流式让用户在 5 秒后才看到完整结果、感受是"卡死了"。这是体验工程的核心——用户感知的时间 ≠ 实际生成时间

怎么做。 在线服务(聊天、补全)默认流式,用 SSE(Server-Sent Events)协议:

GET /v1/chat/completions
Content-Type: text/event-stream

data: {"choices":[{"delta":{"content":"Paged"}}]}
data: {"choices":[{"delta":{"content":"Attention"}}]}
data: {"choices":[{"delta":{"content":"是"}}]}
data: {"choices":[{"delta":{"content":"vLLM"}}]}
data: [DONE]

不用 WebSocket(双向、复杂),不用 polling(每秒轮询等于多 N 次请求)。详见 作品集项目 的"流式对话 API"项目。

反例。 反例一:聊天接口设计成"等模型生成完一次性返回"——TTFT 5 秒,用户跑了。反例二:用 polling 模拟流式(前端每 200ms 发一次请求拿增量)——每次请求都是完整 HTTP 往返,QPS 几十倍放大,服务被打挂。反例三:用 WebSocket 做流式——双向通道用不上,复杂度反而上来。SSE 是流式输出的标准答案

原则 5:模型 + 算子 + 系统三层都要查瓶颈

为什么。 推理服务的性能瓶颈可能出现在三层:模型层(FLOPs 太多)、算子层(kernel 实现差)、系统层(调度、批处理、网络)。只看 GPU 利用率一个指标,看不出在哪层——GPU 100% 可能是 compute-bound(模型层),也可能是 memory-bound(算子层)。详见 Roofline 模型与算力分析GPU 体系结构与优化

怎么做。调参与性能调优 的四步诊断:

Step 1: nvidia-smi 看 GPU 利用率、显存利用率、温度
Step 2: vLLM logs 看 KV cache 占用、吞吐、swapped
Step 3: Nsight Systems 看算子时间分布
Step 4: 定位瓶颈层:
       - 模型层 → 上量化、蒸馏、剪枝
       - 算子层 → 上 FlashAttention、kernel fusion
       - 系统层 → 调 max_num_seqs、加 PagedAttention

每层的应对手段完全不同——盲调等于在错层使劲。

反例。 反例一:GPU 利用率 100% 但慢,团队以为是算力不够,去加 GPU——结果是 memory-bound,加 GPU 不解决访存瓶颈。反例二:单算子 benchmark 漂亮(attention 5ms),但端到端慢——瓶颈在调度层(kernel launch 太频繁),加算子优化不解决。反例三:只看吞吐不看延迟,p99 悄悄飞涨没发现——监控不完整。详见原则 9。

原则 6:量化前看激活异常值分布

为什么。 量化的本质是把连续值压到离散桶——激活值分布正常(钟形)的模型量化无损,激活值有大量异常值(如 LLaMA 系的"巨大激活")的模型量化崩盘。不看你激活分布就上量化,等于赌博。这是 模型量化基础 的核心 trade-off,也是 权重量化与混合精度 的根本动因——为什么 weight-only 量化在 LLM 场景比 weight+activation 量化更稳?因为激活异常值远比权重异常值严重。

怎么做。 量化前跑一遍激活分布检查

python
# 用 transformers 跑几十个样本,每层激活的统计
from transformers import AutoModel
model = AutoModel.from_pretrained("meta-llama/Llama-2-7b-chat-hf")

# 跑样本时 hook 每层激活
activations = {}
def hook(name):
    def fn(module, input, output):
        activations[name] = output.detach()
    return fn

for name, module in model.named_modules():
    if "mlp.gate" in name or "attention" in name:
        module.register_forward_hook(hook(name))

# 跑样本后看每层激活的 max/mean 比值
for name, act in activations.items():
    ratio = act.max() / act.mean()
    print(f"{name}: max/mean = {ratio:.1f}")  # > 100 就是异常值多

ratio > 100 的层,量化要谨慎——考虑 weight-only(保留激活原精度),或用 AWQ/GPTQ 这类激活感知的量化方法。详见 模型量化基础权重量化与混合精度

反例。 反例一:团队上 SmoothQuant(activation 量化)跑 LLaMA-2,MMLU 掉 3 分——因为 LLaMA 激活异常值多,activation 量化把异常值压没了。反例二:用 INT8 weight + INT8 activation 跑 LLaMA,性能反而比 bf16 慢——因为反量化开销大于访存节省(详见 常见陷阱与反模式 的"INT8 权重 + FP16 激活算反更慢")。反例三:不跑业务评测集就上线 AWQ,业务质量悄悄掉——MMLU 几乎不变但业务 prompt 分布上掉了 5 分。

原则 7:投机解码看 batch 大小

为什么。 投机解码的收益严重依赖 batch 大小——小 batch 时大模型算力闲置,小模型猜对了等于白嫖;大 batch 时大模型已被喂满,小模型加进来徒增显存与计算。这是 投机解码与 Medusa/EAGLE 的核心权衡,也是最常见的反模式之一。

怎么做。 投机解码的"黄金区间"在 batch ≤ 16——超过 32 是盈亏平衡点,超过 64 是反向收益。生产部署时:

场景batch 大小投机解码理由
在线聊天1–16小 batch 受益最大
在线批处理32–64大 batch 反退化
离线批量256+完全反向收益

如果你的业务同时有"小 batch 在线"和"大 batch 离线"两条流量,用路由把流量分流——投机解码实例专门接小 batch 流量,普通实例接大 batch 流量。详见 作品集项目 的"多模型路由"项目。

反例。 反例一:大 batch 离线场景开 EAGLE,吞吐反退化 30%——大模型已被算力喂满,draft 模型白白占显存。反例二:在线聊天没开投机解码,TTFT 50ms 没法再降——小 batch 场景是投机解码的甜区,没上等于白浪费性能。反例三:不知道 batch 依赖,把 max_num_seqs 设成 64 同时开 EAGLE,调试一周找不到原因——根本是设计冲突。

原则 8:灰度发布先 5% 流量

为什么。 新配置(新量化、新引擎、新参数)总有意想不到的副作用——精度漂移、长尾延迟、显存泄露、客户端兼容性。全量上线 = 全量暴露问题。灰度发布把问题控制在 5% 流量内,影响小、回滚快。这是 模型服务化与编排 的标准实践,也是 Triton 推理服务 多版本能力的典型用法。

怎么做。 标准灰度发布流程:

Day 0: 新配置部署到 1 个实例(占总实例 5%)
       - 流量按权重 5:95 分流到新:旧
       - 24 小时观察 QPS / p99 / 错误率 / 业务指标
Day 1: 无异常 → 扩到 20%
       - 再观察 24 小时
Day 2: 无异常 → 扩到 50%
       - 此时如有问题,影响面大但仍可控
Day 3: 无异常 → 100% 全量
       - 旧配置下线

每一步都要有自动回滚机制:p99 超 SLA 1.5× 自动回滚、错误率 > 1% 自动回滚、业务指标掉 5% 自动回滚。详见 模型服务化与编排 的"灰度与回滚"。

反例。 反例一:全量上 AWQ INT4,业务质量崩——MMLU 没变但业务 prompt 分布上掉了 10 分。回滚花了一小时,期间业务受损。反例二:新版本只测延迟不测业务指标,延迟看着没问题但业务输出格式变了(INT4 把 logits 量化偏移),客户端解析失败。反例三:没准备回滚预案,发现问题后手忙脚乱——回滚速度比问题本身影响更大。

原则 9:监控不只是 QPS

为什么。 QPS 是"流量",不是"健康"——QPS 正常但 p99 悄悄飞涨、KV cache 命中率掉、GPU 温度限频——这些都是"看起来在跑、其实快崩了"的信号。生产监控必须多维度,每个维度都有自己的告警阈值。

怎么做。 推理服务的最小监控集:

指标含义告警阈值
QPS流量突变 > 50% 告警
p50 / p99 延迟体感p99 > SLA 1.5× 告警
GPU 利用率算力使用< 50% 持续 5 分钟告警
GPU 显存利用率显存使用> 95% 告警
GPU 温度散热> 85°C 告警
KV cache 命中率prefix 复用< 50% 告警
swapped 请求数显存换出> 0 告警
错误率异常> 1% 告警
业务指标端到端跌 5% 告警

加粗的三项是 LLM 推理特有的指标,经常被忽略。详见 调参与性能调优 的"vLLM logs 看微观"。

反例。 反例一:只监控 QPS 与错误率,p99 悄悄从 200ms 飞涨到 800ms 没发现——用户投诉了才知道。反例二:监控 GPU 利用率不看温度,温度限频性能掉 30% 没告警——直到用户说"为什么慢了"。反例三:不看 KV cache 命中率,多轮对话场景 prefix 缓存失效没发现,TTFT 高了一倍。

原则 10:故障演练常态化

为什么。 故障不是"如果",是"什么时候"。生产环境一定会遇到:实例崩溃、GPU 故障、网络分区、磁盘满、依赖服务超时。不演练,故障来时就手忙脚乱——你说"我们有冗余",结果发现自动 failover 没配置;你说"我们有监控",结果发现告警阈值不合理。Netflix 的 Chaos Monkey 思路在推理服务同样适用:主动制造故障,验证系统韧性

怎么做。 至少演练四种故障:

故障类型演练方法验证点
单实例崩溃kubectl delete pod 随机杀一个流量是否自动转移,p99 是否仍达标
GPU 故障nvidia-smi -r 重置 GPU实例是否自动重启,是否触发告警
网络分区iptables 模拟网络丢包客户端是否重试,是否产生雪崩
上游超时模拟 LLM 响应慢 5 秒是否触发降级(路由到小模型或返回缓存)

每月演练一次,每次写复盘报告。详见 模型服务化与编排 的"故障演练"。

反例。 反例一:没演练,GPU 故障时实例自动重启但 KV cache 全丢,正在飞的所有请求失败,客户端雪崩重试。反例二:演练时才发现自动 failover 配错了——杀 pod 后流量没转移,而是 503 给用户。反例三:上游超时没降级机制,单次 LLM 超时把整个请求链路卡住。

原则 11:量化是 trade-off,不是免费午餐

为什么。 量化是推理优化最有效的手段之一,但它总是有代价——精度损失、调参复杂度、引擎兼容性。把量化当银弹的团队都翻过车:上了 INT4 业务质量悄悄掉、MMLU 没变但 JSON 输出格式坏了、不同引擎的量化结果不一致。详见 模型量化基础 的"量化的 trade-off"。

怎么做。 量化决策要回答四个问题:

Q1: 业务对精度损失容忍度?
   高(聊天、摘要) → AWQ INT4 可考虑
   低(数学、代码) → 保 bf16,最多 INT8

Q2: 模型激活异常值分布?
   正常 → 可上 weight+activation 量化
   异常值多 → 只上 weight-only 量化

Q3: 量化前后业务评测集跑了吗?
   跑了,< 1 分下降 → 上线
   没跑 → 不准上线

Q4: 量化的代价算清了吗?
   收益:吞吐 1.5–2×,显存 1/4
   代价:精度损失、引擎兼容性、调试复杂度

详见 模型量化基础权重量化与混合精度

反例。 反例一:上 INT4 不跑业务评测,MMLU 几乎不变但业务 prompt 上掉 5 分。反例二:上 activation 量化不查激活分布,LLaMA 异常值被压掉,输出格式变了。反例三:以为量化加速一定大,但小模型 + 大 batch 时反量化开销大于访存节省——性能反而退化。

原则 12:端到端基准而非单算子基准

为什么。 单算子 benchmark(如"attention 5ms")能反映引擎实现质量,不能反映用户体验。用户感受到的是端到端:HTTP 请求 → tokenizer → prefill → decode → detokenizer → 内容过滤 → HTTP 响应。其中 attention 只是一段,瓶颈可能完全在别处。用单算子 benchmark 做决策,等于用显微镜看全景

怎么做。 部署决策必须基于端到端基准

python
# 端到端延迟(HTTP 请求到响应完整返回)
import time, requests
t0 = time.perf_counter()
resp = requests.post("http://llm-service/v1/chat/completions",
                    json={"messages": [...]})
t1 = time.perf_counter()
print(f"端到端: {(t1-t0)*1000:.0f} ms")

报告里同时报"端到端延迟"与"引擎内部延迟"(vLLM 自己测的),前者是用户体感、后者是引擎能力。详见 推理基准测试实践 的"测什么"。

反例。 反例一:团队优化 attention 算子从 5ms 降到 3ms,但端到端延迟没变——瓶颈在 HTTP 与 tokenizer。反例二:单算子 benchmark 漂亮,上线后发现调度差,端到端慢一倍。反例三:换引擎时只看引擎内部 benchmark,没测端到端——新引擎内部快但 HTTP 层慢,反而退化。

三、原则之间的张力

12 条原则并非独立——它们之间有张力,需要在具体场景权衡:

张力 1:原则 1(延迟 vs 吞吐)vs 原则 8(灰度发布)

延迟优先的配置(小 batch)在灰度期间可能因为"流量小"看不出问题,全量上后才暴露。应对:灰度时主动灌超流量(5% 实例跑 20% 流量),人为压测验证。

张力 2:原则 4(流式)vs 原则 9(监控)

流式输出的延迟分布与非流式完全不同——不能套用同一套监控阈值。应对:流式服务的 p99 要用 chunk-level 延迟,不是请求-响应延迟。详见 推理基准测试实践 的"流式测试方法"。

张力 3:原则 6(看激活异常值)vs 原则 11(量化是 trade-off)

激活异常值多的模型上 weight-only 量化,但 weight-only 在某些引擎上加速不如 weight+activation 多。应对:先看业务对精度容忍度,再决定量化方案——精度敏感场景哪怕慢一点也保 weight-only。

张力 4:原则 7(投机解码看 batch)vs 原则 1(延迟 vs 吞吐)

延迟优先(小 batch)正好是投机解码的甜区,但吞吐优先(大 batch)就关投机解码。应对:这两者其实不冲突——延迟优先选小 batch + 投机解码,吞吐优先选大 batch + 关投机解码,是同一决策的两面

四、把原则变成团队规范

12 条原则要落到团队规范里才有约束力。建议的规范结构:

markdown
# 团队部署规范

## 1. 部署前 checklist
- [ ] 业务 SLA 已定义(原则 1)
- [ ] 显存预算已算(原则 2)
- [ ] 流式 / 非流式已定(原则 4)
- [ ] 量化方案已评估(原则 6、11)
- [ ] 投机解码适用性已评估(原则 7)

## 2. 部署中规范
- [ ] 预处理/后处理与模型同服务(原则 3)
- [ ] 三层瓶颈诊断已走(原则 5)
- [ ] 端到端基准已测(原则 12)

## 3. 上线规范
- [ ] 灰度发布流程(原则 8)
- [ ] 监控告警阈值(原则 9)
- [ ] 故障演练计划(原则 10)

## 4. 运维规范
- [ ] 每月故障演练
- [ ] 每季度 SLA 复盘
- [ ] 每次事故根因分析

详见 模型服务化与编排 的"团队规范模板"。

五、延伸阅读

参考资料