这篇文章解决什么问题

前面的 0005 解决的是 OpenAI Triton 这一侧的问题,也就是怎样理解和编写 GPU kernel。接下来系列切到另一条主线:NVIDIA Triton Inference Server

这条线关注的不是单个算子,而是怎样把一个已有模型组织成可调用、可检查、可继续调优的在线服务。

这一篇只解决最基础也最关键的一个问题:

如何把一个 ONNX 模型放进 NVIDIA Triton,成功启动服务,并完成一次最小推理闭环。

这里有意只做最小闭环,不讨论动态批处理、实例副本或压测工具。原因很简单:如果连最基本的模型加载、配置文件和请求调用都没有先打通,后续所有调优话题都没有稳定起点。

先明确 Triton Server 在做什么

用一句话概括,NVIDIA Triton Inference Server 解决的是:

怎样把模型组织成标准化的推理服务,而不是把推理逻辑散落在自写脚本或接口代码里。

它和“自己写一个 FastAPI 包住模型”的差别,不是能不能提供 HTTP 接口,而是是否具备一套围绕推理服务的标准结构,包括:

  • 模型目录与版本管理;
  • 后端选择;
  • 模型元信息配置;
  • 服务健康检查;
  • 后续调度与压测入口。

从工程角度看,它的价值不在于替代所有业务层,而在于先把模型服务这一层标准化。

最小闭环长什么样

本文的最小闭环包含四个环节:

  1. 准备一个 ONNX 模型。
  2. 按 Triton 约定组织 model repository
  3. 写出最小可用的 config.pbtxt
  4. 启动服务并发送一次推理请求。

只要这四步成立,说明以下几个关键点都已经打通:

  • Triton Server 能启动。
  • 模型能被识别。
  • 配置文件与模型输入输出一致。
  • 客户端能拿到有效结果。

第一步:按 Triton 规范组织模型目录

Triton 并不是“把一个模型文件放到某个目录里就能跑”。它要求模型以固定结构出现在一个 model repository 中。

最常见的最小结构如下:

1
2
3
4
5
models/
simple_mlp/
config.pbtxt
1/
model.onnx

这里每一层都有明确语义:

  • models/ 是整个 model repository 根目录。
  • simple_mlp/ 是模型名。
  • config.pbtxt 是 Triton 读取模型配置的入口文件。
  • 1/ 是模型版本目录,必须是数字。
  • model.onnx 是模型文件本体。

如果目录结构不符合这个约定,Triton 即使启动成功,也不会按预期加载模型。

为什么版本目录必须是数字

这不是随意设计出来的命名风格,而是 Triton 管理模型版本的基本机制。数字目录允许同一个模型在同一仓库下并存多个版本,例如:

1
2
3
4
5
models/
simple_mlp/
config.pbtxt
1/model.onnx
2/model.onnx

这为后续灰度、对比和回滚提供了基础结构。即使当前只部署一个最小模型,也应该从一开始就遵守这个约定。

第二步:准备一个最小 ONNX 模型

为了让后续示例统一,沿用环境篇中导出的两层 MLP:

1
2
输入:  [batch, 128]
输出: [batch, 10]

如果还没有导出模型,可以使用下面这段脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import os
import torch
import torch.nn as nn


class SimpleMLP(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(128, 64)
self.relu = nn.ReLU()
self.fc2 = nn.Linear(64, 10)

def forward(self, x):
return self.fc2(self.relu(self.fc1(x)))


os.makedirs("models/simple_mlp/1", exist_ok=True)

model = SimpleMLP().eval()
dummy = torch.randn(1, 128)

torch.onnx.export(
model,
dummy,
"models/simple_mlp/1/model.onnx",
input_names=["input"],
output_names=["output"],
dynamic_axes={
"input": {0: "batch_size"},
"output": {0: "batch_size"},
},
opset_version=13,
)

执行完成后,模型文件应位于:

1
models/simple_mlp/1/model.onnx

第三步:写出最小可用的 config.pbtxt

模型目录存在,不代表 Triton 就知道这个模型该怎样解释。真正把模型暴露成服务的是配置文件。

最小版本的 config.pbtxt 可以写成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
name: "simple_mlp"
backend: "onnxruntime"
max_batch_size: 32

input [
{
name: "input"
data_type: TYPE_FP32
dims: [ 128 ]
}
]

output [
{
name: "output"
data_type: TYPE_FP32
dims: [ 10 ]
}
]

这段配置里最容易出错的地方有三个。

name

必须与模型目录名一致。这里是 simple_mlp,因此目录也必须叫 simple_mlp

backend

这里写成 onnxruntime,表示该模型由 ONNX Runtime backend 执行,而不是 TensorRT、PyTorch 或 Python backend。

dims

dims 只描述单条样本的形状,不包含 batch 维。batch 维由 Triton 结合 max_batch_size 统一解释。

因此:

  • 输入真实形状是 [batch, 128]
  • 输出真实形状是 [batch, 10]

如果这里把 batch 维也写进 dims,配置就会和 Triton 的 batch 语义冲突。

第四步:启动 Triton Server

NVIDIA Triton Inference Server 最常见的运行方式是容器。假设当前目录下已经有 models/ 目录,可以这样启动:

1
2
3
4
5
6
7
docker run --gpus all --rm \
-p 8000:8000 \
-p 8001:8001 \
-p 8002:8002 \
-v $(pwd)/models:/models \
nvcr.io/nvidia/tritonserver:23.12-py3 \
tritonserver --model-repository=/models

这条命令里几个关键部分分别对应:

  • --gpus all:把 GPU 暴露给容器。
  • -p 8000:8000:HTTP 推理接口。
  • -p 8001:8001:gRPC 推理接口。
  • -p 8002:8002:metrics 接口。
  • -v $(pwd)/models:/models:把宿主机上的模型目录挂载到容器里。
  • --model-repository=/models:告诉 Triton model repository 的根路径。

这里需要注意,本文默认命令在远端 Linux GPU 服务器上执行,不是 Windows 本地机。Windows 本地只适合作为 SSH 客户端和编辑环境。

如何判断服务是否真正启动成功

只看容器是不是在运行还不够。更可靠的判断至少包括两层。

第一层:看日志

启动成功后,日志里通常会出现三类服务启动信息:

  • HTTP Service
  • GRPC Inference Service
  • Metrics Service

如果服务进程一启动就退出,优先检查:

  • model repository 是否挂载正确;
  • config.pbtxt 是否有语法错误;
  • 模型文件是否与 backend 匹配。

第二层:做健康检查

即使容器还活着,也不代表服务已经 ready。最简单的 ready 检查是:

1
curl -s http://localhost:8000/v2/health/ready

如果返回空 JSON,说明服务层已经就绪。

如果要再进一步看模型元信息,可以请求:

1
curl -s http://localhost:8000/v2/models/simple_mlp

这样可以确认目标模型是否已被 Triton 识别。

第五步:发送一次最小推理请求

服务 ready 之后,还需要再确认一次:模型不只是“被看见了”,而是真的可以完成推理。

先安装 HTTP 客户端:

1
pip install tritonclient[http]==2.40.0

然后使用下面这段最小客户端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import numpy as np
import tritonclient.http as httpclient


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

assert client.is_server_ready()
assert client.is_model_ready("simple_mlp")

batch_input = np.random.randn(4, 128).astype(np.float32)

infer_input = httpclient.InferInput("input", batch_input.shape, "FP32")
infer_input.set_data_from_numpy(batch_input)

infer_output = httpclient.InferRequestedOutput("output")

response = client.infer(
model_name="simple_mlp",
inputs=[infer_input],
outputs=[infer_output],
)

result = response.as_numpy("output")
assert result.shape == (4, 10)

print("inference passed")
print("input shape:", batch_input.shape)
print("output shape:", result.shape)

如果输出形状是 (4, 10),说明最小闭环已经成立。

这个闭环的意义在于,它同时验证了:

  • 服务端接口可访问;
  • 模型名称可解析;
  • 配置文件里的输入输出定义是对的;
  • ONNX backend 能完成实际推理。

这里为什么不直接讲动态批处理

很多教程会在第一次部署时就把 dynamic_batchinginstance_group、并发参数一起塞进来。这样做信息量很大,但通常不利于定位问题。

如果当前目标只是先打通最小闭环,那么最稳妥的顺序应该是:

  1. 先保证模型能被正确加载。
  2. 再保证一次请求能顺利返回。
  3. 最后再引入调度与性能变量。

这样做的好处是,任何一个阶段出问题时,排查范围都比较清晰。

Triton 和自写接口的边界

这一节也顺带澄清一个常见问题:用了 Triton Server,并不代表业务层接口全部不需要了。

Triton 更像是标准化推理服务层,解决的是:

  • 模型加载;
  • 模型版本;
  • backend 执行;
  • 后续调度和压测。

而业务系统仍然可能需要:

  • 身份认证;
  • 业务协议封装;
  • 请求编排;
  • 结果缓存;
  • 上层路由逻辑。

因此,把 Triton 理解为“模型服务层”,会比理解成“完整业务 API 替代品”更准确。

常见误区

误区一:把 config.pbtxt 当成可选文件

在很多最小教程里,读者容易把注意力全部放在 model.onnx 上,忽略 config.pbtxt。但对 Triton 来说,配置文件不是附属说明,而是模型服务定义的一部分。

误区二:把 batch 维写进 dims

这是最常见的配置错误之一。只要 max_batch_size 大于 0,dims 就描述单样本形状,而不是完整请求张量形状。

误区三:服务 ready 就等于模型可用

ready 只能说明服务层已经就绪,不能替代真实推理请求。只有客户端成功拿到正确形状的返回结果,才能说明模型加载与配置路径都正常。

结论

NVIDIA Triton Inference Server 的基础部署,关键不在于把容器拉起来,而在于建立一个稳定、可继续扩展的最小服务闭环:

  • 模型按 model repository 规范组织;
  • config.pbtxt 正确描述输入输出与 backend;
  • 服务成功启动并 ready;
  • 一次最小推理请求可以拿到正确结果。

只要这一步打通,后续关于动态批处理、实例并发、压测和调优的讨论才有共同起点。

下一篇会继续沿着这条主线往前走,专门讨论 dynamic batchinginstance group 以及吞吐和时延之间的权衡关系。

系列导航