模型能回答问题还远远不够,真正的生产系统必须回答另一组问题:能撑多久、能顶多大峰值、出错时能不能收得住
真正决定线上体验的,往往不是模型回答得多聪明
做到这一步,模型已经能推理。
引擎已经选好。
量化、GGUF、LoRA、蒸馏这些路径也都有了位置。
但如果把系统放到线上,真正先暴露出来的问题通常不是“模型知识不够”。
而是另外一组更工程化的问题。
为什么高峰时排队突然变长。
为什么同一台卡白天稳定、晚上却频繁 OOM。
为什么压测里 TPS 很高,用户却抱怨首字太慢。
为什么上线新模型后错误率没涨,满意度却掉了。
这些都说明一件事。
推理服务一旦进入生产,核心难题就不再只是模型本身。
限流不是保守,而是保护系统边界
推理服务最常见的事故之一,就是把所有请求都照单全收。
对普通 HTTP 服务来说,这可能只是慢一些。
对大模型服务来说,后果往往更尖锐。
因为每个请求都不是均质成本。
长 prompt、长输出、多轮历史、工具调用,都会把资源消耗拉开。
因此推理服务必须明确自己的容量边界。
限流rate limitingRate limiting constrains how many requests or tokens a client can consume in a time window so the service stays within safe resource boundaries.限流的目标不是让业务难用。
而是避免少数大请求或突发流量把整台引擎拖死。
限流应该按什么维度做
只按 QPS 限流通常不够。
因为一条 50 token 的问题和一条 5000 token 的问题,对系统的占用不是一个量级。
更务实的做法通常是把多个维度结合起来。
- 每秒请求数。
- 每分钟输入 token 总量。
- 每分钟输出 token 总量。
- 单请求最大上下文。
- 单请求最大生成长度。
这样做的本质,是把“请求数量”转换成更接近真实成本的预算模型。
一个最小的应用层限流示例
如果你的服务是 FastAPI,可以很快在网关前先挡一层。
from fastapi import FastAPI
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address, default_limits=["60/minute"])
app = FastAPI()
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
@app.post("/v1/chat/completions")
@limiter.limit("10/minute")
async def chat_completions():
return {"ok": True}
真正上线时,限流通常还会放到 API Gateway、Nginx 或 Service Mesh 层。
但先在应用里把边界意识建立起来,是最低成本的一步。
动态批处理不是越激进越好
前面讲过,continuous batching 或动态批处理能提高吞吐。
这并不意味着可以无限堆请求。
动态批处理动态批处理动态批处理会在请求持续到达时不断合并可同时执行的工作,以提升 GPU 利用率,但批处理窗口和 token 上限必须被严格控制。如果批处理窗口过大,TTFT 会迅速恶化。
如果总 token 上限设得过猛,显存波动会变得危险。
如果混入太多超长请求,短请求也会被一起拖慢。
因此动态批处理真正要管理的不是“开或不开”。
而是三条边界。
- 允许等多久再合批。
- 一批最多容纳多少请求。
- 一批最多容纳多少 token。
这三条线决定的是吞吐和延迟的平衡点。
为什么监控不能只看 QPS
这里常说的 QPS,也就是 queries per second,只能告诉你每秒处理了多少请求,却无法描述请求形状和 token 成本差异。
QPS 对推理服务来说只是非常粗的一层指标。
你真正需要同时盯住的,至少还包括:
- TPS,即每秒输出 token 数。
- TTFT,即首 token 延迟。
- TPOT,即每输出一个 token 的平均时间。
- 输入 token 与输出 token 分布。
- 排队时间和被拒绝请求数。
如果没有这些指标,很多性能问题根本不会被看见。
因为系统并不是突然彻底挂掉。
它更常见的恶化方式,是首字越来越慢,短请求被长请求拖住,或者某些租户在高峰期持续被饿死。
TTFT 和 TPOT 为什么要分开看
TTFTtime to first tokenTTFT measures how long the user waits before the first output token arrives, capturing queueing delay and prefill cost.TTFT 受排队、prefill、调度策略影响很大。
如果 RAG prompt 很长,TTFT 很可能先出问题。
TPOTtime per output tokenTPOT measures the average latency for each generated token after the first token, reflecting decode efficiency and scheduling overhead.TPOT 则更像 decode 效率指标。
如果采样、KV Cache 访问或调度切换有问题,TPOT 会明显变差。
把这两个指标拆开看,才能知道瓶颈是在“首字前”还是“出字中”。
这对调优方向几乎是决定性的。
一个简单的 Prometheus 指标思路
很多团队一开始不知道从哪里埋点。
其实可以先从最少但最有用的一组开始。
from prometheus_client import Counter, Histogram
REQUESTS = Counter("llm_requests_total", "Total requests", ["model", "status"])
TOKENS_IN = Counter("llm_prompt_tokens_total", "Prompt tokens", ["model"])
TOKENS_OUT = Counter("llm_completion_tokens_total", "Completion tokens", ["model"])
TTFT = Histogram("llm_ttft_seconds", "Time to first token", ["model"])
TPOT = Histogram("llm_tpot_seconds", "Time per output token", ["model"])
再配合请求日志,把这些指标和租户、模型版本、错误类型关联起来,很多问题就会突然变得可定位。
OOM 不是偶发异常,而是必须被设计进恢复路径
大模型服务和普通 Web 服务有一个很不一样的地方。
资源耗尽通常不会只是返回慢一点。
它可能直接把 worker 打死。
或者让引擎进入不稳定状态。
因此 VRAM OOM 不应被看成“偶尔碰到的 bug”。
而应当被视为系统必须设计的恢复场景。
OOM 恢复的最低要求是什么
最少要做到这几件事。
- 能识别是显存不足而不是普通业务异常。
- 能拒绝后续高风险请求,而不是继续雪崩。
- 能触发 worker 重启或引擎重建。
- 能把失败请求记录到可回放队列。
- 能在恢复后快速接回健康检查。
如果这些没有预先设计,OOM 一来就会变成整条服务链的连锁故障。
一个务实的 OOM 处理思路
很多线上服务会在推理进程外再包一层 supervisor。
例如 systemd、Kubernetes、Ray Serve 或自研 worker 管理器。
应用层一旦识别到 CUDA OOM 或引擎分配失败,就立刻:
- 拒绝新请求或把新请求转走。
- 标记当前 worker 为
unhealthy。 - 触发重启。
- 等待模型重新 warmup。
- 健康恢复后再重新接流量。
这条链路听起来笨。
但比“让已经坏掉的进程继续硬撑”可靠得多。
为什么推理服务要接受‘降级’这个词
灰度和 A/B 对模型服务尤其重要
传统服务发布关注功能正确性。
模型服务还要多看一层行为变化。
新模型上线后,即使 200 状态码完全正常,回答风格、工具调用习惯、长度分布和敏感问题处理都可能发生变化。
因此灰度和 A/B 在这里不是锦上添花。
而是必须品。
金丝雀发布canary releaseA canary release sends a small fraction of production traffic to a new model version first, letting engineers observe latency, error rates, and behavioral changes before full rollout.先让极小比例流量进入新模型。
观察 TTFT、TPOT、错误率、输出长度和人工抽样质量。
确认没有系统性退化,再逐步放量。
这是最稳的上线方式。
A/B 不该只比用户主观喜好
模型服务做 A/B,很多团队第一反应是看用户点赞率。
这当然重要。
但远远不够。
你还应当同时比较:
- 同类请求下的 TTFT 与 TPOT。
- 每次回答的平均输出 token。
- 工具调用成功率。
- 引用命中率或业务完成率。
- 被限流、被拒绝、超时的占比。
因为一个“看起来更聪明”的模型版本,可能同时更慢、更贵、更长。
如果这些成本没有被一起纳入比较,A/B 结论很容易失真。
多模型与多版本共存时,要把路由权做细
推理服务工程化很快就会碰到一个现实。
不只一个模型在线。
同一个模型也不只一个版本在线。
有基础版。
有高质量版。
有低成本版。
还有灰度中的候选版。
这时候路由策略就不能只按 URL 来分。
你可能需要按租户、请求大小、SLA 等级、是否工作时间、是否命中缓存来做选择。
一旦把路由权做细,很多“同一套服务怎么兼顾成本和质量”的矛盾就会缓和很多。
真正成熟的推理服务,是把资源预算也产品化
工程化的终点,不是把模型藏在一个 API 后面。
而是让整套资源边界可描述、可观测、可协商。
一个成熟服务应当能回答:
- 一个租户每分钟最多能用多少 token。
- 某个模型版本在 95 分位延迟下能撑多少并发。
- 高峰时系统先保护哪类流量。
- 某个回答如果过长,是否要被截断或切换模型。
这类问题看上去像平台治理。
实际上它们就是推理能力能否长期稳定成为产品能力的前提。
系列结语
如果回看整个系列,会发现推理优化并没有某种神秘的新原理。
它建立在一条非常清楚的链条之上:自回归生成决定了 decode 的串行约束,KV Cache 决定了显存与延迟的核心交换,量化、GGUF、LoRA、蒸馏这些方法分别在压缩权重、改变训练成本或改写部署形态,引擎层再把缓存管理、批处理和编译优化补齐,最后服务层把限流、监控、恢复和灰度收拢成一套可持续运行的系统。
真正决定成败的,也往往不是某个术语听起来多先进,而是你有没有把“模型怎样被表示、怎样被执行、怎样被服务、怎样在异常时收住”这整条链一起想清楚。推理优化因此不只是部署末端的一点性能调参,它和 RAG 一样决定模型能否接上真实知识,与 Agent 一样决定能力能否稳定暴露为系统接口。对今天的大模型应用来说,推理优化已经是一根独立的工程支柱,而不是一份上线前临时补的运维清单。
本篇要点
- 推理服务工程化的核心,是明确容量边界,而不是无限制接收请求。
- 限流应同时考虑请求数、输入输出 token 和单请求上限,动态批处理也必须守住等待窗口与 token 预算。
- QPS 远远不够,TTFT、TPOT、TPS、队列长度和拒绝率才是定位问题的关键指标。
- VRAM OOM 必须被设计进恢复路径,包括拒绝新流量、重启 worker、回放请求和健康恢复。
- 灰度与 A/B 不只是看回答好不好,还要一起比较延迟、成本、输出长度和业务完成率。
下一篇
这是本系列最后一篇。若顺着这条线继续深入,下一组真正值得系统展开的话题,通常会是推理系统如何与 RAG、Agent、工具调用和多租户平台治理进一步耦合。
参考资料
- vLLM Serve CLI
- Hugging Face TGI Launcher Arguments
- SGLang OpenAI API Completions
- Prometheus Client Python
版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。
(采用 CC BY-NC-SA 4.0 许可协议进行授权)
本文标题:推理服务工程化
本文链接:https://www.sshipanoo.com/blog/ai/inference-opt/08-推理服务工程化/
本文最后一次更新为 天前,文章中的某些内容可能已过时!