跳到主要内容

高性能推理服务

本节定位

如果上一节讲的是:

  • 模型能不能在本地跑

这一节讲的就是更现实的一层:

模型能不能在线上稳定扛住请求。

这也是很多项目从 demo 走向生产时,第一次真正撞墙的地方。

学习目标

  • 理解吞吐、延迟、批处理、队列这些推理服务关键词
  • 理解为什么“能跑”不等于“能服务”
  • 看懂一个最小批处理推理服务思路
  • 建立对推理服务优化问题的第一层直觉

先建立一张地图

推理服务更适合按“请求怎么进来、怎么排队、怎么合批、怎么返回”来理解:

所以这节真正想解决的是:

  • 为什么单次跑通模型和服务化完全不是一回事
  • 为什么推理服务天然是排队、批次和资源平衡问题

一、为什么本地推理和推理服务是两回事?

1.1 本地推理关心的是“出不出结果”

例如:

  • 一条 prompt 能不能答
  • 一张图能不能生成

1.2 推理服务关心的是“同一时间能扛多少请求”

一旦上线,你要面对的是:

  • 多个请求同时到来
  • 流量波峰
  • 资源限制
  • 超时

所以推理服务的核心问题变成:

怎样在资源有限的情况下平衡速度和吞吐。

1.3 一个更适合新人的总类比

你可以把推理服务理解成:

  • 餐厅后厨出餐

本地推理更像:

  • 自己在家做一份饭,能不能做出来

推理服务更像:

  • 中午高峰一下来很多单
  • 厨房怎么排队、怎么拼单、怎么别让客人等太久

这个类比很适合新人,因为它会帮助你先抓住:

  • 推理服务本质上是流量组织问题

二、先分清两个最重要的词

2.1 Latency

一次请求要等多久。

2.2 Throughput

单位时间能处理多少请求。

这两个指标往往互相拉扯。

例如:

  • batch 变大,吞吐可能更高
  • 但单请求等待时间可能也会更长

所以推理服务不是“单项越高越好”,而是平衡问题。


三、为什么批处理(batching)会特别重要?

3.1 一个直觉理解

如果 8 个请求几乎同时进来,你可以:

  • 一个个单独跑

也可以:

  • 合成一个 batch 一起跑

后者通常能更高效利用硬件。

3.2 一个最小示意

requests = [12, 8, 15]
batch_size = 8

for r in requests:
num_batches = (r + batch_size - 1) // batch_size
print("needs", num_batches, "batches")

3.3 这段代码在教什么?

它在教你:

推理服务不是按“一个请求”思考,而更像按“队列和批次”思考。

3.4 一个很适合初学者先记的判断表

现象更值得先看哪一层
单请求很快,但整体扛不住吞吐和批处理
响应慢,但 GPU 不满队列和 batch 组织
请求很多时突然超时队列长度和并发控制
单次 benchmark 很好,线上还是差服务层调度而不是模型本身

这个表很适合新人,因为它会把“推理服务慢”重新拆成几个更具体的排查方向。


四、为什么队列是高性能服务的基础部件?

4.1 因为请求不会整齐地来

真实流量往往是:

  • 有波峰
  • 有波谷
  • 有突发

如果没有队列,系统很容易:

  • 突然爆掉
  • 请求直接丢失

4.2 一个最小队列示例

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)

这个例子很简单,但已经说明:

  • 请求先排队
  • 再按批次执行

这就是很多推理服务最基本的行为模式。


五、并发和批处理不是一回事

这是很多初学者最容易混的地方。

5.1 并发

多个请求在系统里同时推进。

5.2 批处理

多个请求在模型层合成一个 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)

6.2 这段代码为什么重要?

因为它已经包含了一个高性能推理服务最关键的骨架:

  • 入队
  • 合批
  • 推理
  • 回传结果

这条链才是“服务”的本体。

6.3 第一次做推理服务时,最稳的默认顺序

更稳的顺序通常是:

  1. 先让单请求稳定返回
  2. 再引入队列
  3. 再做合批
  4. 最后再调 batch 大小和资源利用率

这样会比一开始就追“最大吞吐”更容易把系统做稳。


七、为什么高性能推理服务永远在做平衡?

你通常会在这些维度之间取舍:

  • batch 大一点,吞吐更高
  • batch 小一点,响应更快
  • 常驻模型更快,但更占资源
  • 更多实例更稳,但更贵

这意味着:

推理服务的优化不是绝对值优化,而是业务约束下的平衡优化。


八、真实服务里最值得盯的指标

至少通常要看:

  • 平均延迟
  • P95 / P99 延迟
  • 队列长度
  • batch 利用率
  • 错误率
  • GPU / CPU 利用率

这些指标会告诉你:

  • 当前瓶颈是在请求侧
  • 还是在 batch 组织侧
  • 还是在模型执行侧

8.1 一个很适合初学者先记的监控表

指标最值得先回答什么问题
平均 / P95 延迟用户到底等了多久
队列长度请求是不是在堆积
batch 利用率有没有真正把硬件吃满
GPU / CPU 利用率瓶颈在模型执行还是别处

这个表很适合新人,因为它会把“监控很多指标”重新变成几个更直观的问题。


九、最常见的误区

9.1 只看单次推理 benchmark

线上服务真正重要的是整体流量下的表现。

9.2 一上来把 batch 调很大

吞吐也许变高了,但延迟可能被拖垮。

9.3 只会跑模型,不会看队列和资源利用

这会让你很难真正理解系统瓶颈在哪里。


小结

这一节最重要的不是记住某个推理服务名词,而是理解:

高性能推理服务的核心,是把模型调用从“单次执行”变成“能在真实流量里平衡吞吐、延迟和资源使用的系统”。

这也是它和“本地跑通模型”本质上完全不同的地方。

如果把它做成项目或系统设计,最值得展示什么

最值得展示的通常不是:

  • “模型能并发跑”

而是:

  1. 请求怎么排队和合批
  2. 你是怎样平衡吞吐和延迟的
  3. 线上最关键的监控指标是什么
  4. 什么时候瓶颈在队列,什么时候瓶颈在模型执行

这样别人会更容易看出:

  • 你理解的是服务化推理
  • 不只是会调用模型

练习

  1. 用自己的话解释:为什么 batching 和 concurrency 不是一回事?
  2. 想一想:如果你的产品要求很低延迟,你会倾向大 batch 还是小 batch?
  3. 设计一份最小推理服务监控指标清单。
  4. 为什么说推理服务真正难的是“平衡”,而不是单项做到极致?