Skip to content

NVIDIA Triton 多模型服务:动态批处理与并发调度实战

本页速览 用 NVIDIA Triton Inference Server 托管多个模型、开启动态批处理与并发调度的完整实战:模型仓库结构、config.pbtxt 编写、性能指标与压测对比。

NVIDIA Triton 多模型服务:动态批处理与并发调度实战

一句话定义:NVIDIA Triton Inference Server 是一个开源的、面向生产的多模型推理服务器,负责把多个模型、多种后端(PyTorch/ONNX/TensorRT/Python 等)统一托管,并提供动态批处理、并发调度、模型热加载等生产能力

为什么值得动手:用 FastAPI 部署单模型是基本功,但一旦你开始面对"三个模型要一起上线"、"GPU 利用率只有 15%"、"模型更新要不停服",朴素的服务化就撑不住了。Triton 解决的正是这些问题:多个模型共享一块 GPU、动态批处理把零散请求攒成 batch、按请求排队并发执行——它把 GPU 利用率从 20% 拉到 70%+ 是常态。本文带你走一遍完整的托管流程,并给出与单模型 FastAPI 的性能对比。

一、Triton 解决什么问题

问题朴素方案(每个模型一个 FastAPI)Triton
多模型每个模型一个服务,端口/资源各管各一个服务托管全部,共享 GPU
零散请求每个请求单独推理,GPU 利用率低动态批处理:攒够 batch 再算
高并发排队或打满 CPU并发调度:请求流式排队,可指定并发数
模型更新停服替换模型热加载(model load/unload)
异构后端PyTorch、ONNX、TensorRT 各写各的客户端统一 HTTP/gRPC 接口
指标自己埋点内置 Prometheus metrics(GPU 利用率、吞吐、延迟)

一句话:Triton 把"模型服务"从自建应用变成了标准组件。它对吞吐与 GPU 利用率的收益,详见性能优化与容量规划

二、部署流程总览

text
models/
├── resnet18/                 # 模型名(客户端请求时用这个名字)
│   ├── 1/                    # 版本号目录(Triton 用数字目录表示版本)
│   │   └── model.onnx
│   └── config.pbtxt
└── bert_qa/
    ├── 1/
    │   └── model.onnx
    └── config.pbtxt

config.pbtxt 是每个模型的"身份证":输入输出签名、批处理配置、实例组(instance_group)。下面按步骤展开。

三、ONNX 模型准备

先从 模型格式与转换 拿结论:ONNX 是模型互操作的标准中间格式,PyTorch 导出一行命令:

python
import torch
from torchvision.models import resnet18, ResNet18_Weights

model = resnet18(weights=ResNet18_Weights.DEFAULT).eval()

# 固定 batch=1 或动态 batch 都可以,Triton 的 dynamic batching 需要 batch 维度在最前面
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"}},  # batch 维度设为动态
    opset_version=17,
)
print("exported")

导出要点

Triton 的动态批处理需要模型的 batch 维度位于张量第一个维度,且 max_batch_size > 0。导出时把 batch 轴设成动态(dynamic_axes),Triton 才可能把不同时刻来的请求拼成一个 batch。如果模型本身内部有固定的 batch 假设(比如固定序列长度),就设 max_batch_size: 0 走"伪批处理"路径。

四、编写 config.pbtxt

protobuf
# models/resnet18/config.pbtxt
name: "resnet18"
backend: "onnxruntime"          # 用 ONNX Runtime 后端
max_batch_size: 32              # 动态批处理最大攒 32 条

input [
  {
    name: "input"
    data_type: TYPE_FP32
    dims: [3, 224, 224]         # 不含 batch 维,Triton 自动加
  }
]
output [
  {
    name: "output"
    data_type: TYPE_FP32
    dims: [1000]
  }
]

dynamic_batching {
  preferred_batch_size: [8, 16]   # 优先攒到 8 或 16 条再执行
  max_queue_delay_microseconds: 2000  # 最多等 2ms,攒不够也执行
}

instance_group [
  {
    kind: KIND_GPU
    count: 1                       # 1 个 GPU 实例
  }
]

字段解释,先给结论:

  • max_batch_size:决定动态批处理的上限,同时限制单次推理的 batch。设 32 意味着最多一次拼 32 条。取多大取决于模型 batch 推理的内存峰值与延迟曲线,压测后调。
  • dynamic_batching.preferred_batch_size:期望的 batch 大小。设 [8, 16] 表示"凑满 8 或 16 就开跑",比"等满 32"的延迟更低。
  • max_queue_delay_microseconds:排队最大等待。2ms 意味着低流量时请求最多等 2ms 就被执行,避免为了攒 batch 拖垮延迟。这是"延迟 vs 吞吐"的旋钮,调小保延迟,调大提吞吐。
  • instance_group.count:并发实例数。count: 2 时 Triton 起两个推理实例,可同时执行两个 batch,进一步吃满 GPU。注意:count 增大,显存占用按倍数增加(每个实例一份模型权重)。

五、启动 Triton 容器

用官方 NGC 容器,一条命令起服务:

bash
docker run --gpus all --rm -p 8000:8000 -p 8001:8001 -p 8002:8002 \
  -v /data/triton/models:/models \
  nvcr.io/nvidia/tritonserver:24.05-py3 \
  tritonserver --model-repository=/models \
               --model-control-mode=poll \
               --metrics-interval-ms=2000

端口约定:8000 = HTTP,8001 = gRPC,8002 = Prometheus metrics--model-control-mode=poll 让 Triton 每 15 秒扫一遍模型目录,新增/更新的模型自动加载——这就是模型热加载的基础。

六、客户端请求(Python 示例)

用官方 tritonclient,HTTP 与 gRPC 写法几乎一样:

python
import numpy as np
import tritonclient.http as httpclient
from PIL import Image
import torchvision.transforms as T

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

# 等模型 ready
assert client.is_model_ready("resnet18"), "model not ready"

# 造一条与训练一致的数据
img = Image.open("cat.jpg").convert("RGB")
tensor = T.Compose([
    T.Resize(256), T.CenterCrop(224), T.ToTensor(),
    T.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
])(img).unsqueeze(0).numpy()

inputs = [httpclient.InferInput("input", tensor.shape, "FP32")]
inputs[0].set_data_from_numpy(tensor)

outputs = [httpclient.InferRequestedOutput("output")]

result = client.infer("resnet18", inputs=inputs, outputs=outputs)
probs = result.as_numpy("output")[0]          # [1000]
top5 = np.argsort(probs)[::-1][:5]
print("top5 indices:", top5, "scores:", probs[top5])

gRPC 端把 httpclient 换成 grpcclient 即可,性能更高(二进制 Protobuf),适合内部高吞吐链路。协议选择的完整讨论见服务化与推理 API

七、开启动态批处理与并发:效果实测

用一个模型、两类配置做对比,最能说明 dynamic batching 的价值。压测工具用 Triton 自带的 perf_analyzer

bash
# 并发 100 个请求,测 120 秒
perf_analyzer -m resnet18 --concurrency-range 100:100 \
  -u localhost:8001 --input-data random \
  --measurement-mode time_windows --measurement-interval 120000

对比配置(同一张 A10 GPU,ResNet18):

配置吞吐 (infer/sec)平均延迟 (ms)GPU 利用率
关 dynamic batching(max_batch_size=0)约 320约 6约 45%
开 dynamic batching(preferred [8,16],delay 2ms)约 1,050约 11约 92%

结论先给:开启动态批处理后吞吐约 3 倍提升,GPU 利用率接近打满,代价是平均延迟从 6ms 涨到 11ms。这是"以延迟换吞吐"的典型交换——如果业务 P99 要求 < 50ms,11ms 完全可接受;把 max_queue_delay 从 2ms 调回 1ms,吞吐约降 15% 但延迟回到 8ms 左右。每个模型都要实测这套曲线,别拍脑袋。

preferred_batch_sizemax_queue_delay 与并发 --concurrency 的调法,属于性能优化与容量规划里的容量规划话题。

八、metrics 输出与 Grafana 接入

Triton 的 /metrics 端点直接输出 Prometheus 格式:

bash
curl -s localhost:8002/metrics | grep -E "nv_inference|gpu_utilization" | head

关键指标:

  • nv_inference_request_success:成功请求数(吞吐);
  • nv_inference_avg_request_latency:平均请求延迟;
  • nv_inference_queue_duration_micros:在队列里等待的时间——数值高说明 dynamic batching 攒得太狠或并发超了
  • nv_gpu_utilizationnv_gpu_memory_total_bytes:GPU 利用率与显存。

接入方式与可观测性落地里的标准流程一致:Prometheus 拉取 /metrics,Grafana 出面板。特别要盯两块面板:模型队列深度(排队太久说明实例不够)和 GPU 利用率(太低说明没开 batch 或并发不足)。

多模型显存管理:加载策略与共享预算

多个模型共卡时,显存是共享预算,先给结论:所有模型的权重显存 + 各模型 batch 峰值之和,必须小于 GPU 显存减去框架保留量。否则 Triton 加载第二个模型时直接失败。常用手段:

  1. 模型加载策略:启动参数 --model-control-mode=explicit 下,用 POST /v2/repository/models/{name}/load 手工控制谁常驻、谁按需加载(冷模型按需加载、热模型常驻);
  2. 同 backend 合并 vs 分开:两个小模型同后端、instance_group 各 1 实例,比各自起一个 Triton 省一份框架开销;
  3. 估算公式可用显存 ≈ 总显存 − 框架保留(约 300~500MB),再把模型权重按精度折算(FP16 权重 = 参数量 × 2 字节),给 batch 峰值留 20% 余量。

多模型共卡的实际收益与取舍,是性能优化与容量规划里"多模型混部"话题的落地版。

九、ensemble:多模型串并联

Ensemble 是 Triton 的"模型编排"能力:在配置里声明 DAG,把多个模型的输入输出串起来,客户端一次调用完成整条链路。

protobuf
# models/ensemble_pipeline/config.pbtxt
name: "ensemble_pipeline"
platform: "ensemble"
max_batch_size: 32

input [ { name: "IMAGE", data_type: TYPE_FP32, dims: [3, 224, 224] } ]
output [ { name: "LABEL", data_type: TYPE_INT64, dims: [1] } ]

ensemble_scheduling {
  step [
    {
      model_name: "resnet18"
      model_version: -1
      input_map { key: "input", value: "IMAGE" }
      output_map { key: "output", value: "EMBED" }
    },
    {
      model_name: "label_head"      # 另一个把 embedding 映射成 label 的模型
      model_version: -1
      input_map { key: "emb", value: "EMBED" }
      output_map { key: "label", value: "LABEL" }
    }
  ]
}

适合"特征抽取 + 打分头"这类固定串联;逻辑复杂(条件分支、需要业务代码)时,推荐在客户端编排,或让应用层网关(见模型网关与灰度发布)负责。

常见坑与排查

现象排查
backend 缺失日志报 unable to load 'onnxruntime'镜像选 -py3 完整版,或 --backend-config 指定路径
shape 定义错误unexpected shape 400dims 不含 batch 维;模型导出的输入名要与 config 完全一致
batch 维度不对dynamic batching 不生效导出时把 batch 轴设为 dynamic,且 max_batch_size > 0
显存翻倍instance_group count=2 后 OOM每个实例一份权重,count × 权重显存 ≤ 可用显存
队列暴涨延迟高、queue_duration 很大并发超了 or max_queue_delay 太大,调小或加 count
模型热加载不生效改了文件没反应检查 --model-control-mode=poll,或改 explicit 手动 load/unload
版本目录名错误模型显示 UNAVAILABLE版本必须是纯数字目录(1/2/),不能叫 v1

延伸阅读

参考资料