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利用率,再决定要不要上多流。