3、多流推理架构设计:为什么需要多流、多流推理的适用场景、吞吐量与延迟的权衡、多流架构的三种模式
各位同学,咱们今天聊点实在的。多流推理,说白了就是让GPU同时干好几件事。你可能会问:一个模型推理一次不就完了吗?为什么要搞这么复杂?
我刚开始接触TensorRT时,也觉得单流跑得挺顺。直到有一次,线上业务量突然暴涨,单流的延迟直接飙到不可接受。嗯,那时候我才意识到——单流架构的天花板,其实很低。
为什么需要多流
先说说根本原因。GPU是个并行计算怪兽,它有几千个CUDA核心。你想想看,单流推理时,这些核心真的全用上了吗?
答案是否定的。单流推理时,GPU内部存在大量空闲周期。比如:
- 计算单元空闲:小模型推理时,计算量不够填满所有SM
- 内存带宽瓶颈:数据搬运时,计算单元在干等
- 预处理/后处理串行:CPU和GPU之间来回切换,浪费不少时间
多流架构就是为了填满这些空闲。我个人的习惯是,只要GPU利用率低于70%,我就会考虑上多流。
核心观点:多流不是让单个请求跑得更快,而是让单位时间内处理的总请求数更多。
多流推理的适用场景
不是所有场景都适合多流。我踩过坑,所以给你总结一下:
| 场景 | 是否适合多流 | 原因 |
|---|---|---|
| 高并发在线服务(如推荐系统) | ✅ 非常适合 | 请求量大,可以并行处理 |
| 视频流分析(多路视频) | ✅ 非常适合 | 每路视频独立推理,天然并行 |
| 单请求低延迟(如自动驾驶) | ❌ 不适合 | 多流会增加单请求延迟 |
| 超大模型推理(如LLM) | ⚠️ 谨慎使用 | 显存占用大,多流容易OOM |
| 批量离线处理 | ✅ 适合 | 不在乎延迟,只求吞吐 |
我曾经在一个视频分析项目里,硬是把单流改成了四流。结果呢?吞吐量翻了3倍,但单帧延迟从20ms涨到了35ms。客户说可以接受,因为实时性要求没那么高。你看,这就是场景决定一切。
吞吐量与延迟的权衡
这是多流架构里最绕不开的话题。说白了,就是鱼和熊掌不可兼得。
吞吐量:单位时间内处理的任务数。多流越多,吞吐越高,但有个上限。
延迟:单个任务从进来到出去的时间。多流越多,延迟越高,因为要排队。
为什么会这样?我给你画个简单的图:
单流: [请求1] -> [请求2] -> [请求3] -> ...
延迟低,但吞吐也低
多流: [请求1] ---->
[请求2] ----> GPU并行处理
[请求3] ---->
吞吐高,但每个请求都要等别的请求一起处理
我一般用这个公式来估算最优流数:
最优流数 ≈ GPU计算能力 / 单个请求的计算量
举个例子:你的GPU有80个SM,每个请求需要2个SM跑1ms。那理论上,你可以同时跑40个流。但实际中,我建议从4个流开始测试,逐步往上加,直到延迟达到你的容忍上限。
我的经验:先定延迟上限,再调流数。比如业务要求延迟不超过50ms,那就从1个流开始,逐步增加,直到延迟接近50ms。这时候的流数就是最优解。
多流架构的三种模式
好了,到了最核心的部分。多流架构不是只有一种玩法。我把它归纳为三种模式:
模式一:独立流模式
每个流完全独立,互不干扰。每个流有自己的CUDA stream、自己的输入输出缓冲区、自己的推理上下文。
// 伪代码示例
cudaStream_t streams[4];
for (int i = 0; i < 4; i++) {
cudaStreamCreate(&streams[i]);
}
// 每个请求分配到不同的流
void infer(Request req, int stream_id) {
// 预处理
preprocess(req.input, stream_id);
// 推理
engine->enqueueV2(buffers[stream_id], streams[stream_id], nullptr);
// 后处理
postprocess(req.output, stream_id);
}
优点:实现简单,隔离性好,一个流出错不影响其他流。
缺点:显存开销大,每个流都要独立分配缓冲区。
我建议在显存充足、请求之间完全独立时用这种模式。
模式二:共享上下文模式
多个流共享同一个推理引擎和ExecutionContext。说白了,就是模型只加载一份,但用多个stream来提交任务。
// 共享一个ExecutionContext
IExecutionContext* ctx = engine->createExecutionContext();
// 多个流共用这个ctx
void infer(Request req, cudaStream_t stream) {
// 注意:需要确保ctx的输入输出缓冲区是线程安全的
ctx->enqueueV2(buffers, stream, nullptr);
}
优点:显存省很多,适合大模型。
缺点:需要自己处理线程安全问题。TensorRT的ExecutionContext不是线程安全的,你得加锁或用线程局部存储。
注意:我曾经在这里翻过车。共享上下文时,两个流同时调用enqueueV2,结果数据错乱了。后来我加了个互斥锁,但性能又降了。最后改用每个线程一个ExecutionContext,才解决问题。
模式三:批量流模式
这是我最喜欢的一种模式。把多个小请求合并成一个batch,然后用一个流推理。说白了,就是「攒一波再打」。
// 批量流模式
std::vector<Request> batch;
void batch_infer() {
// 攒够4个请求或超时
if (batch.size() >= 4 || timeout) {
// 合并成batch
float* batched_input = concat(batch);
// 一次推理
engine->enqueueV2(batched_buffers, stream, nullptr);
// 拆开结果
split_results(batch);
batch.clear();
}
}
优点:吞吐量最高,GPU利用率最满。
缺点:延迟不稳定,要等batch攒够。而且batch大小不好调。
我一般这样调batch大小:先设成1,看延迟。然后逐步增大,直到延迟翻倍。那个临界点就是最优batch size。
三种模式对比
| 模式 | 吞吐量 | 延迟 | 显存占用 | 实现难度 |
|---|---|---|---|---|
| 独立流模式 | 中 | 低 | 高 | 低 |
| 共享上下文模式 | 中高 | 中 | 低 | 中 |
| 批量流模式 | 高 | 高 | 中 | 高 |
你问我选哪个?我的建议是:
- 显存够、请求独立 → 独立流模式
- 显存紧张、模型大 → 共享上下文模式
- 追求极致吞吐、能接受延迟波动 → 批量流模式
嗯,其实没有银弹。我每个项目都会把三种模式都跑一遍,看数据说话。毕竟,实践出真知嘛。
最后说一句:多流架构不是万能的。如果你的模型已经占满了GPU,再加流只会增加调度开销。先看看GPU利用率,再决定要不要上多流。