多模型服务:在同一 GPU 上并发运行不同模型
多模型服务:在同一 GPU 上并发运行不同模型
概述
在多模型服务场景中,我们希望在单个 GPU 上同时运行多个不同的深度学习模型,以最大化硬件利用率并降低服务延迟。传统方式中,每个模型独占 GPU 显存,但通过模型并发、动态显存分配以及批处理融合等技术,可以在不牺牲性能的前提下让多个模型共享同一块 GPU。
本教程将带你从概念到实践,掌握在同一 GPU 上并发部署多个模型的核心技巧。你将了解:
- 多模型服务的典型架构模式
- 如何利用深度学习框架的并发能力
- 显存管理与优化策略
- 使用 Triton Inference Server 实现生产级多模型服务
为什么需要多模型并发
资源效率
大多数推理场景中,单个模型无法完全占满 GPU 的计算能力或显存带宽。例如一个轻量级 ResNet-18 可能仅使用 2 GB 显存,而 GPU 具备 24 GB 显存,串行运行多个模型是对资源的巨大浪费。
降低时延
不同请求可能需要调用不同模型。如果所有模型都已预加载到显存中,服务就不必在模型切换时重新加载权重,从而极大降低首包延迟。
简化部署
在同一 GPU 上混部模型可以减少节点数量,降低调度复杂度,适合边缘计算或小规模集群。
多模型并发的基本原理
模型执行的“上下文切换”
GPU 本身不是抢占式的多任务处理器,但现代推理框架通过以下方式实现模型并发:
- 时间分片:在 CUDA stream 级别交错执行多个模型的算子。
- 空间分片:将显存分割为多个区域,每个区域驻留一个模型,运行时依请求动态调度。
- 批处理合并:将不同模型的单个请求打包成动态批次,充分利用 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 上并发加载多个模型,并提供动态批处理、模型并发执行、显存限制等高级特性。
步骤概览
- 导出模型为 Triton 支持的格式(ONNX, TensorRT, PyTorch 等)。
- 构建模型仓库(model repository),按约定目录结构存放。
- 编写配置文件
config.pbtxt,控制实例数、批处理大小等。 - 启动 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 Ensemble 或 Business 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_size或instance_group.count - 使用 TensorRT 或 ONNX 优化模型,减少显存占用
- 启用 Triton 的
--exit-on-error=false以便观察
吞吐量不升反降
- 检查 GPU 计算利用率(
nvidia-smi dmon),若持续低于 80%,考虑增加并发请求或批处理大小 - 避免过多小模型同时抢占资源,可合并为 Ensemble
延迟抖动
- 为不同模型设置不同的优先级(Triton 支持 priority 调度)
- 使用 CUDA MPS(多进程服务)统一管理上下文,减少切换开销
总结
在同一 GPU 上并发运行多个模型是高效利用硬件、降低服务延迟的必备手段。从简单的多进程共享到 Triton 生产级多模型服务,核心在于理解显存规划、并发调度和批处理优化。通过本教程,你应当能够:
- 评估不同并发方案的适用场景
- 使用 Triton 部署多模型服务并配置优化参数
- 排查并解决多模型共存的资源冲突
开始你的多模型服务实践,让每一块 GPU 都发挥最大价值。