外观
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.pbtxtconfig.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_size、max_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_utilization、nv_gpu_memory_total_bytes:GPU 利用率与显存。
接入方式与可观测性落地里的标准流程一致:Prometheus 拉取 /metrics,Grafana 出面板。特别要盯两块面板:模型队列深度(排队太久说明实例不够)和 GPU 利用率(太低说明没开 batch 或并发不足)。
多模型显存管理:加载策略与共享预算
多个模型共卡时,显存是共享预算,先给结论:所有模型的权重显存 + 各模型 batch 峰值之和,必须小于 GPU 显存减去框架保留量。否则 Triton 加载第二个模型时直接失败。常用手段:
- 模型加载策略:启动参数
--model-control-mode=explicit下,用POST /v2/repository/models/{name}/load手工控制谁常驻、谁按需加载(冷模型按需加载、热模型常驻); - 同 backend 合并 vs 分开:两个小模型同后端、
instance_group各 1 实例,比各自起一个 Triton 省一份框架开销; - 估算公式:
可用显存 ≈ 总显存 − 框架保留(约 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 400 | dims 不含 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 |
延伸阅读
- FastAPI + Docker 在线服务 —— 单模型朴素部署的对照实验组,Triton 的收益从对比中来
- 性能优化与容量规划 —— 动态批处理、并发与延迟-吞吐曲线的理论框架
- 模型格式与转换 —— ONNX 导出细节、opset 与算子兼容
- 可观测性落地 —— Prometheus + Grafana 接入推理指标的完整流程
- 模型网关与灰度发布 —— 多个 Triton 服务前的流量入口与路由
- TensorRT 与边缘部署 —— 同一条 ONNX 模型在 Triton 里换 TensorRT 后端的进一步加速
参考资料
- NVIDIA Triton Inference Server 官方文档:https://docs.nvidia.com/deeplearning/triton-inference-server/user-guide/docs/index.html
- Triton 官方容器(NGC):https://catalog.ngc.nvidia.com/orgs/nvidia/containers/tritonserver
- Triton client 与 perf_analyzer:https://github.com/triton-inference-server/client
- ONNX Runtime 官方文档:https://onnxruntime.ai/docs/