8.2.3 高性能推理服务
如果上一节讲的是:
- 模型能不能在本地跑
这一节讲的就是更现实的一层:
模型能不能在线上稳定扛住请求。
这也是很多项目从 demo 走向生产时,第一次真正撞墙的地方。
学习目标
- 理解吞吐、延迟、批处理、队列这些推理服务关键词
- 理解为什么“能跑”不等于“能服务”
- 看懂一个最小批处理推理服务思路
- 建立对推理服务优化问题的第一层直觉
先建立一张地图
推理服务更适合按“请求怎么进来、怎么排队、怎么合批、怎么返回”来理解:
所以这节真正想解决的是:
- 为什么单次跑通模型和服务化完全不是一回事
- 为什么推理服务天然是排队、批次和资源平衡问题
一、为什么本地推理和推理服务是两回事?
本地推理关心的是“出不出结果”
例如:
- 一条 prompt 能不能答
- 一张图能不能生成
推理服务关心的是“同一时间能扛多少请求”
一旦上线,你要面对的是:
- 多个请求同时到来
- 流量波峰
- 资源限制
- 超时
所以推理服务的核心问题变成:
怎样在资源有限的情况下平衡速度和吞吐。
一个更适合新人的总类比
你可以把推理服务理解成:
- 餐厅后厨出餐
本地推理更像:
- 自己在家做一份饭,能不能做出来
推理服务更像:
- 中午高峰一下来很多单
- 厨房怎么排队、怎么拼单、怎么别让客人等太久
这个类比很适合新人,因为它会帮助你先抓住:
- 推理服务本质上是流量组织问题
二、先分清两个最重要的词
Latency
一次请求要等多久。
Throughput
单位时间能处理多少请求。
这两个指标往往互相拉扯。
例如:
- batch 变大,吞吐可能更高
- 但单请求等待时间可能也会更长
所以推理服务不是“单项越高越好”,而是平衡问题。
三、为什么批处理(batching)会特别重要?
一个直觉理解
如果 8 个请求几乎同时进来,你可以:
- 一个个单独跑
也可以:
- 合成一个 batch 一起跑
后者通常能更高效利用硬件。
一个最小示意
requests = [12, 8, 15]
batch_size = 8
for r in requests:
num_batches = (r + batch_size - 1) // batch_size
print("needs", num_batches, "batches")
预期输出:
needs 2 batches
needs 1 batches
needs 2 batches
这里的 requests 是刻意简化的工作量列表。重点是向上取整:一旦工作量超过 batch size,服务层就要把它拆成多次模型执行。
这段代码在教什么?
它在教你:
推理服务不是按“一个请求”思考,而更像按“队列和批次”思考。
一个很适合初学者先记的判断表
| 现象 | 更值得先看哪一层 |
|---|---|
| 单请求很快,但整体扛不住 | 吞吐和批处理 |
| 响应慢,但 GPU 不满 | 队列和 batch 组织 |
| 请求很多时突然超时 | 队列长度和并发控制 |
| 单次 benchmark 很好,线上还是差 | 服务层调度而不是模型本身 |
这个表很适合新人,因为它会把“推理服务慢”重新拆成几个更具体的排查方向。

请求不是直接冲进模型,而是先排队、再合批、再执行。batch 能提高吞吐,但也会增加等待,所以服务化调优永远是在 latency 和 throughput 之间取平衡。
四、为什么队列是高性能服务的基础部件?
因为请求不会整齐地来
真实流量往往是:
- 有波峰
- 有波谷
- 有突发
如果没有队列,系统很容易:
- 突然爆掉
- 请求直接丢失
一个最小队列示例
from collections import deque
queue = deque(["req1", "req2", "req3", "req4", "req5"])
batch_size = 2
while queue:
batch = []
for _ in range(min(batch_size, len(queue))):
batch.append(queue.popleft())
print("run batch:", batch)
预期输出:
run batch: ['req1', 'req2']
run batch: ['req3', 'req4']
run batch: ['req5']
这个例子很简单,但已经说明:
- 请求先排队
- 再按批次执行
这就是很多推理服务最基本的行为模式。
五、并发和批处理不是一回事
这是很多初学者最容易混的地方。
并发
多个请求在系统里同时推进。
批处理
多个请求在模型层合成一个 batch 一起算。
所以可以记成:
- 并发是调度层问题
- 批处理是模型执行层问题
这两个常常一起出现,但并不等价。
六、一个最小推理服务主循环
from collections import deque
queue = deque(["q1", "q2", "q3", "q4"])
batch_size = 2
def run_model(batch):
return [f"answer_for_{item}" for item in batch]
while queue:
batch = []
for _ in range(min(batch_size, len(queue))):
batch.append(queue.popleft())
results = run_model(batch)
for item, result in zip(batch, results):
print(item, "->", result)
预期输出:
q1 -> answer_for_q1
q2 -> answer_for_q2
q3 -> answer_for_q3
q4 -> answer_for_q4
这段代码为什么重要?
因为它已经包含了一个高性能推理服务最关键的骨架:
- 入队
- 合批
- 推理
- 回传结果
这条链才是“服务”的本体。
第一次做推理服务时,最稳的默认顺序
更稳的顺序通常是:
- 先让单请求稳定返回
- 再引入队列
- 再做合批
- 最后再调 batch 大小和资源利用率
这样会比一开始就追“最大吞吐”更容易把系统做稳。
七、为什么高性能推理服务永远在做平衡?
你通常会在这些维度之间取舍:
- batch 大一点,吞吐更高
- batch 小一点,响应更快
- 常驻模型更快,但更占资源
- 更多实例更稳,但更贵
这意味着:
推理服务的优化不是绝对值优化,而是业务约束下的平衡优化。
八、真实服务里最值得盯的指标
至少通常要看:
- 平均延迟
- P95 / P99 延迟
- 队列长度
- batch 利用率
- 错误率
- GPU / CPU 利用率
这些指标会告诉你:
- 当前瓶颈是在请求侧
- 还是在 batch 组织侧
- 还是在模型执行侧
一个很适合初学者先记的监控表
| 指标 | 最值得先回答什么问题 |
|---|---|
| 平均 / P95 延迟 | 用户到底等了多久 |
| 队列长度 | 请求是不是在堆积 |
| batch 利用率 | 有没有真正把硬件吃满 |
| GPU / CPU 利用率 | 瓶颈在模型执行还是别处 |
这个表很适合新人,因为它会把“监控很多指标”重新变成几个更直观的问题。
九、最常见的误区
只看单次推理 benchmark
线上服务真正重要的是整体流量下的表现。
一上来把 batch 调很大
吞吐也许变高了,但延迟可能被拖垮。
只会跑模型,不会看队列和资源利用
这会让你很难真正理解系统瓶颈在哪里。
小结
这一节最重要的不是记住某个推理服务名词,而是理解:
高性能推理服务的核心,是把模型调用从“单次执行”变成“能在真实流量里平衡吞吐、延迟和资源使用的系统”。
这也是它和“本地跑通模型”本质上完全不同的地方。
如果把它做成项目或系统设计,最值得展示什么
最值得展示的通常不是:
- “模型能并发跑”
而是:
- 请求怎么排队和合批
- 你是怎样平衡吞吐和延迟的
- 线上最关键的监控指标是什么
- 什么时候瓶颈在队列,什么时候瓶颈在模型执行
这样别人会更容易看出:
- 你理解的是服务化推理
- 不只是会调用模型
练习
- 用自己的话解释:为什么 batching 和 concurrency 不是一回事?
- 想一想:如果你的产品要求很低延迟,你会倾向大 batch 还是小 batch?
- 设计一份最小推理服务监控指标清单。
- 为什么说推理服务真正难的是“平衡”,而不是单项做到极致?