第 19 章 生产级模型服务¶
开场问题:半夜流量突增三倍,系统应该慢一点还是拒绝一部分请求?
19.1 网关、路由与接纳控制¶
网关做鉴权、配额、协议转换。路由器选副本,判断依据不只是负载,还包括前缀缓存命中(把同一会话送回原副本,收益常常大于负载均衡本身)和模型/适配器亲和。
接纳控制是最容易被忽略却最重要的一环。队列无限长不是"更宽容",只是把失败变成超时——用户等了 30 秒才拿到错误,比 1 秒内被拒绝更糟,而且这期间的算力全浪费了。系统应该根据当前队列长度和 KV 余量估算新请求的预期完成时间,超出 SLO 就直接拒绝或降级。
19.2 过载时的降级顺序¶
流量超出容量时要有预设的降级链,而不是临场决策:
- 关掉投机解码(第 14 章:高并发下它本来就没收益)
- 收紧 token 预算,让调度偏向吞吐
- 限制单请求的最大输出长度
- 按优先级限流,保低优先级之外的流量
- 拒绝新请求,保住已接纳的
每一级都要可观测、可手动触发、可自动回退。降级悄悄发生而没人知道,会变成更难查的问题。
19.3 弹性扩缩容和它的滞后¶
推理服务的扩容不是秒级的:拉起容器、加载几十上百 GB 权重、编译或捕获 CUDA Graph、warmup,全流程可能几分钟。这意味着按当前指标触发扩容,等新实例就绪时高峰可能已经过去了。
对策包括:按预测而非当前值扩容、保留一定比例的热备实例、把权重放在本地 SSD 或用共享内存加速加载、以及缩容要比扩容保守得多(缩错了要几分钟才能补回来)。
触发指标也不该用 GPU 利用率——Decode 阶段带宽打满时算力利用率仍然很低。应该用队列长度、TTFT 的 p95、KV 占用率这些直接反映服务质量的量。
19.4 故障与重试¶
流式生成中途失败是个特殊难题:已经吐出去 200 个 token 了,剩下的怎么办?重新生成会得到不同内容(采样有随机性),用户会看到前后不连贯。
系统要按阶段区分处理:还在排队 → 直接换个实例重试,无副作用;Prefill 阶段失败 → 重试,用户只感觉慢了点;Decode 中途失败 → 要么带着已生成内容和固定种子在别处续上(需要 KV 迁移或重算),要么明确告诉用户中断。默认的无脑重试在这里是错的。
TP 组内单卡故障会让整组不可用,这是纵向切分的代价;多副本部署下故障只影响一个副本,这是它的优势之一。
19.5 可观测性:至少这些指标¶
按模型和租户维度拆分记录:QPS、输入/输出长度分布、TTFT、ITL、端到端延迟(都要 p50/p95/p99)、队列时间、运行中请求数、batch 大小、KV 占用率、前缀缓存命中率、抢占次数、投机解码的接受长度、拒绝和超时率、OOM 和 GPU/NCCL 错误。
关键在于能把一次慢请求归因到具体阶段:trace 要贯穿网关、调度器、worker,并在每个阶段打时间戳。没有这个,p99 上升时只能靠猜。
19.6 灰度、回滚与版本¶
可以改变输出的东西比想象的多:权重、量化方案、引擎版本、采样默认值、对话模板、tokenizer。它们必须作为一个整体制品打版本,回滚时一起回滚。只回滚权重而留着新模板,是典型的事故放大方式。
上线流程应该有 shadow traffic(复制真实流量到新版本但不返回结果,对比输出和延迟)和 canary(小比例真实流量),且两者都要看质量指标而不只是性能指标。
19.7 多租户¶
不同租户的优先级、配额、数据隔离要求不同。三个要点:前缀缓存不能跨越安全边界(第 11.5 节);配额要按实际资源消耗计,而不是按请求数(一个 100K 上下文的请求抵得上几百个短请求);要防止单个大租户长期占满,通常用按租户的并发上限加上公平调度。
核心问答¶
Q:为什么 GPU 利用率不适合做扩容指标? Decode 是带宽受限的,带宽打满时算力利用率可能只有 20%。应该用队列、TTFT、KV 占用这些贴近服务质量的指标。
Q:过载时为什么要主动拒绝? 无限接收会让所有请求都超时,goodput 归零。拒绝一部分是为了保住剩下的能正常完成。
Q:新量化模型上线只测准确率够吗? 不够。要测各业务切片、长上下文、结构化输出稳定性,还要测延迟和显存变化——量化可能改变最优 batch 配置。
Q:为什么模板和 tokenizer 要和权重一起版本化? 它们直接改变模型看到的输入和输出解析,单独变更等于换了个模型,还会让前缀缓存失效甚至串号。