多模型服务:在同一 GPU 上并发运行不同模型

FreeGuideOnline 最新 2026-06-29

多模型服务:在同一 GPU 上并发运行不同模型

概述

在多模型服务场景中,我们希望在单个 GPU 上同时运行多个不同的深度学习模型,以最大化硬件利用率并降低服务延迟。传统方式中,每个模型独占 GPU 显存,但通过模型并发动态显存分配以及批处理融合等技术,可以在不牺牲性能的前提下让多个模型共享同一块 GPU。

本教程将带你从概念到实践,掌握在同一 GPU 上并发部署多个模型的核心技巧。你将了解:

  • 多模型服务的典型架构模式
  • 如何利用深度学习框架的并发能力
  • 显存管理与优化策略
  • 使用 Triton Inference Server 实现生产级多模型服务

为什么需要多模型并发

资源效率

大多数推理场景中,单个模型无法完全占满 GPU 的计算能力或显存带宽。例如一个轻量级 ResNet-18 可能仅使用 2 GB 显存,而 GPU 具备 24 GB 显存,串行运行多个模型是对资源的巨大浪费。

降低时延

不同请求可能需要调用不同模型。如果所有模型都已预加载到显存中,服务就不必在模型切换时重新加载权重,从而极大降低首包延迟。

简化部署

在同一 GPU 上混部模型可以减少节点数量,降低调度复杂度,适合边缘计算或小规模集群。

多模型并发的基本原理

模型执行的“上下文切换”

GPU 本身不是抢占式的多任务处理器,但现代推理框架通过以下方式实现模型并发:

  1. 时间分片:在 CUDA stream 级别交错执行多个模型的算子。
  2. 空间分片:将显存分割为多个区域,每个区域驻留一个模型,运行时依请求动态调度。
  3. 批处理合并:将不同模型的单个请求打包成动态批次,充分利用 GPU 并行性。

关键技术挑战

  • 显存碎片:动态加载/卸载模型会导致显存碎片,最终无法分配大块连续内存。
  • CUDA 上下文冲突:多线程或多进程同时访问 GPU 需要正确管理 CUDA 上下文。
  • 吞吐量干扰:某个模型的计算若占用过多内存带宽,会拖慢其他模型的推理。

环境准备

本教程基于以下环境(可根据实际情况调整):

  • GPU:NVIDIA Tesla T4 / A10 / A100 等(建议显存 ≥ 16 GB)
  • 驱动:CUDA 11.8+
  • Python 3.10+
  • PyTorch 2.1+(用于示例模型导出)
  • Triton Inference Server 23.10+(生产服务框架)

安装 Triton 客户端库:

pip install tritonclient[http] numpy

方法一:PyTorch 多进程共享 GPU(研究/原型)

思路

使用 Python multiprocessing 在不同进程中各自加载模型,由于 PyTorch 的 CUDA 上下文是进程隔离的,每个进程可以拥有独立的模型,CUDA 驱动会在 GPU 上对多个上下文的计算进行调度。

代码示例

import torch
import torch.multiprocessing as mp
from torchvision import models
import time

def serve_model(model_name: str, iterations: int):
    # 每个进程绑定一块 GPU(这里都绑定到 GPU 0)
    device = torch.device("cuda:0")
    if model_name == "resnet18":
        model = models.resnet18(pretrained=True).eval().to(device)
    else:
        model = models.mobilenet_v2(pretrained=True).eval().to(device)
    
    # 构造随机输入
    dummy_input = torch.randn(1, 3, 224, 224, device=device)
    with torch.no_grad():
        for i in range(iterations):
            _ = model(dummy_input)
            time.sleep(0.01)  # 模拟请求间隔
    print(f"{model_name} done")

if __name__ == "__main__":
    mp.set_start_method("spawn")  # 显式设置,避免 fork 引起 CUDA 问题
    p1 = mp.Process(target=serve_model, args=("resnet18", 100))
    p2 = mp.Process(target=serve_model, args=("mobilenet_v2", 100))
    p1.start()
    p2.start()
    p1.join()
    p2.join()

运行 nvidia-smi 观察,会发现两个进程共享 GPU 0,显存占用约为两个模型大小之和,计算利用率会在进程同时工作时升高。

优点:实现简单,适合快速验证。 缺点:缺少细粒度调度,线程/进程管理开销大,不适合生产环境。

方法二:单个进程托管多个模型(轻量服务)

在同一进程中直接加载多个模型实例,并在收到请求时根据路由调用对应模型。

import torch
from torchvision import models

class MultiModelServer:
    def __init__(self):
        self.device = torch.device("cuda:0")
        self.models = {}
        # 预加载所有模型
        self.models["resnet18"] = models.resnet18(pretrained=True).eval().to(self.device)
        self.models["mobilenet_v2"] = models.mobilenet_v2(pretrained=True).eval().to(self.device)
        # 开启 cuda 异步执行,提高并发效率
        self.streams = {
            name: torch.cuda.Stream() for name in self.models
        }

    def infer(self, model_name: str, input_tensor: torch.Tensor):
        with torch.cuda.stream(self.streams[model_name]):
            output = self.models[model_name](input_tensor)
        return output

# 使用示例
server = MultiModelServer()
x = torch.randn(1, 3, 224, 224).cuda()
out1 = server.infer("resnet18", x)
out2 = server.infer("mobilenet_v2", x)
# 注意:实际使用时需结合 HTTP/gRPC 服务框架

通过为每个模型分配独立的 CUDA stream,可以实现算子级别的交错执行,提高 GPU 利用率。

方法三:Triton Inference Server 多模型部署(生产推荐)

Triton 是 NVIDIA 开源的推理服务,天然支持在一个 GPU 上并发加载多个模型,并提供动态批处理、模型并发执行、显存限制等高级特性。

步骤概览

  1. 导出模型为 Triton 支持的格式(ONNX, TensorRT, PyTorch 等)。
  2. 构建模型仓库(model repository),按约定目录结构存放。
  3. 编写配置文件 config.pbtxt,控制实例数、批处理大小等。
  4. 启动 Triton 服务并验证。

导出模型

将 PyTorch 模型转为 ONNX 格式:

import torch
from torchvision import models

# 导出 ResNet18
model = models.resnet18(pretrained=True).eval()
dummy = torch.randn(1, 3, 224, 224)
torch.onnx.export(model, dummy, "resnet18.onnx",
                  input_names=["input"], output_names=["output"],
                  dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}})

# 导出 MobileNetV2
model2 = models.mobilenet_v2(pretrained=True).eval()
torch.onnx.export(model2, dummy, "mobilenet_v2.onnx",
                  input_names=["input"], output_names=["output"],
                  dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}})

构建模型仓库

建立如下目录结构:

model_repo/
├── resnet18
│   ├── 1
│   │   └── model.onnx   # 复制 resnet18.onnx 至此
│   └── config.pbtxt
└── mobilenet_v2
    ├── 1
    │   └── model.onnx
    └── config.pbtxt

resnet18/config.pbtxt 示例:

name: "resnet18"
platform: "onnxruntime_onnx"
max_batch_size: 8
input [
  {
    name: "input"
    data_type: TYPE_FP32
    dims: [ 3, 224, 224 ]
  }
]
output [
  {
    name: "output"
    data_type: TYPE_FP32
    dims: [ 1000 ]
  }
]
instance_group [
  {
    count: 1
    kind: KIND_GPU
    gpus: [ 0 ]    # 指定使用 GPU 0
  }
]

mobilenet_v2/config.pbtxt 类似,将 name 改为 mobilenet_v2

启动 Triton 服务

docker run --gpus all --rm -p8000:8000 -p8001:8001 -p8002:8002 \
  -v $(pwd)/model_repo:/models \
  nvcr.io/nvidia/tritonserver:23.10-py3 \
  tritonserver --model-repository=/models

Triton 会自动加载所有模型到 GPU 0,并保持常驻显存。

并发调用验证

使用 Triton Python 客户端并发发送两个模型的推理请求:

import tritonclient.http as httpclient
import numpy as np
from concurrent.futures import ThreadPoolExecutor

client = httpclient.InferenceServerClient(url='localhost:8000')

def infer_resnet():
    input_data = np.random.randn(1, 3, 224, 224).astype(np.float32)
    inputs = [httpclient.InferInput("input", [1, 3, 224, 224], "FP32")]
    inputs[0].set_data_from_numpy(input_data)
    result = client.infer("resnet18", inputs)
    return result.as_numpy("output")

def infer_mobilenet():
    input_data = np.random.randn(1, 3, 224, 224).astype(np.float32)
    inputs = [httpclient.InferInput("input", [1, 3, 224, 224], "FP32")]
    inputs[0].set_data_from_numpy(input_data)
    result = client.infer("mobilenet_v2", inputs)
    return result.as_numpy("output")

with ThreadPoolExecutor(max_workers=2) as executor:
    f1 = executor.submit(infer_resnet)
    f2 = executor.submit(infer_mobilenet)
    print(f1.result(), f2.result())

执行后通过 nvidia-smi 可观察到单一 GPU 上两个模型同时提供推理,显存占用稳定。

高级优化:按需加载与显存修剪

模型实例组

config.pbtxt 中可配置多个实例组,实现模型内并发:

instance_group [
  { count: 2 kind: KIND_GPU gpus: [ 0 ] }
]

这样 Triton 会在 GPU 上创建该模型的两个独立副本,进一步榨取并行度。

动态批处理

开启动态批处理,将来自不同请求的单独输入合并为一个批次,提高吞吐量:

dynamic_batching {
  preferred_batch_size: [ 4, 8 ]
  max_queue_delay_microseconds: 100
}

显存限制

Triton 支持为每个模型设置 GPU 显存限制,防止某个模型 OOM 影响其他模型:

optimization {
  cuda {
    graph_spec { ... }
  }
}

或者通过 Triton 的 --backend-config 全局限制显存池大小。

模型编排:从多模型到模型 pipeline

当多模型之间存在依赖时(例如先文本检测再识别),可以利用 Triton 的 Model EnsembleBusiness Logic Scripting (BLS) 在同一 GPU 上串接多个模型,无需数据回传主机。

示例 Ensemble 配置:

name: "ocr_pipeline"
platform: "ensemble"
max_batch_size: 1
input [ { name: "image" data_type: TYPE_FP32 dims: [ -1, -1, 3 ] } ]
output [ { name: "text" ... } ]
ensemble_scheduling {
  step [ { model_name: "detector", model_version: 1 input_map { key: "image" value: "image" } output_map { key: "bbox" value: "bbox" } } ]
  step [ { model_name: "recognizer", model_version: 1 input_map { key: "cropped_image" value: "bbox" } output_map { key: "text" value: "text" } } ]
}

所有步骤都在 GPU 上完成,模型驻留显存,零拷贝传递张量。

常见问题与排错

显存不足(OOM)

  • 降低 max_batch_sizeinstance_group.count
  • 使用 TensorRT 或 ONNX 优化模型,减少显存占用
  • 启用 Triton 的 --exit-on-error=false 以便观察

吞吐量不升反降

  • 检查 GPU 计算利用率(nvidia-smi dmon),若持续低于 80%,考虑增加并发请求或批处理大小
  • 避免过多小模型同时抢占资源,可合并为 Ensemble

延迟抖动

  • 为不同模型设置不同的优先级(Triton 支持 priority 调度)
  • 使用 CUDA MPS(多进程服务)统一管理上下文,减少切换开销

总结

在同一 GPU 上并发运行多个模型是高效利用硬件、降低服务延迟的必备手段。从简单的多进程共享到 Triton 生产级多模型服务,核心在于理解显存规划、并发调度和批处理优化。通过本教程,你应当能够:

  • 评估不同并发方案的适用场景
  • 使用 Triton 部署多模型服务并配置优化参数
  • 排查并解决多模型共存的资源冲突

开始你的多模型服务实践,让每一块 GPU 都发挥最大价值。

延伸阅读