这篇文章解决什么问题

上一篇已经把最小部署闭环打通了:模型能被 Triton 识别、服务能启动、一次请求可以拿到正确结果。

接下来真正进入服务化阶段时,第一个绕不开的问题不是“模型能不能跑”,而是“请求来了之后,Triton 怎么安排它们去跑”。

这件事直接决定了两个核心指标:

  • 吞吐
  • 延迟

而在 NVIDIA Triton 中,最核心的两个调度入口就是:

  • dynamic_batching
  • instance_group

这篇文章的目标就是把这两个配置项背后的调度逻辑讲清楚,并建立一套更稳妥的配置思路。重点不是记住几个字段名,而是理解:

  • 为什么批处理会提高吞吐。
  • 为什么等待合批会抬高延迟。
  • 为什么实例数不是越多越好。

Triton 不只是“来一个请求算一个请求”

如果服务端对每个请求都立刻执行,那么逻辑最直观,但硬件利用率往往并不好。尤其是模型较小、请求较碎、单次输入规模不大时,GPU 很容易处于“能跑,但没被喂满”的状态。

Triton 的调度层之所以重要,原因就在这里。它不只是把请求转发给模型,而是在到达执行前先做两类组织:

  • 尝试把多个请求合并成更大的 batch。
  • 决定同一个模型要不要同时开多个执行实例。

前者主要影响单次执行规模,后者主要影响并发接纳能力。两者一起决定了服务在吞吐和延迟之间的取舍方式。

先看 dynamic_batching 在做什么

dynamic_batching 的本质很简单:把一段很短时间窗口内到达的多个请求先放进队列,再尝试合并成更大的 batch 一起执行。

它解决的问题不是模型精度,也不是算子效率,而是:

如果单个请求太小,是否可以在服务侧把若干请求拼起来,让一次模型执行更值当。

可以把它想象成下面这个过程:

1
2
3
4
请求1(batch=1) --\
请求2(batch=1) ---\
请求3(batch=1) ----> Triton 调度队列 -> 合并成一个更大的 batch -> 模型执行
请求4(batch=1) ---/

这件事对吞吐通常是有利的,因为:

  • 单次执行规模变大;
  • GPU 更容易被喂满;
  • 每条请求分摊到的固定调度成本更低。

但代价同样直接:为了等更多请求凑成批次,部分请求必须在队列里多等一会儿。

一个最小 dynamic_batching 配置

最常见的起步配置可以写成这样:

1
2
3
4
dynamic_batching {
preferred_batch_size: [ 4, 8, 16 ]
max_queue_delay_microseconds: 5000
}

这两个字段分别控制不同事情。

preferred_batch_size

它告诉 Triton:如果能合到这些批大小,就优先以这些规模执行。

例如写成:

1
preferred_batch_size: [ 4, 8, 16 ]

意思不是“只能执行 4、8、16”,而是 Triton 会优先把这些大小视为较理想的执行规模。

max_queue_delay_microseconds

它控制等待上限。也就是说,哪怕没有等到理想批大小,只要请求在队列中的等待时间超过这个阈值,也要立刻发出去执行。

这个参数的本质是:

用可接受的等待时间,去换更大的 batch 和更高的吞吐。

也正因为如此,它是吞吐和延迟之间最直接的旋钮之一。

max_queue_delay_microseconds 为什么是关键旋钮

这一个参数,几乎直接决定了当前服务更偏向哪种目标。

值更大

意味着 Triton 愿意多等一会儿,让更多请求凑进同一批里。这通常带来:

  • 更容易形成大 batch;
  • 更高的吞吐;
  • 更差的尾延迟风险。

值更小

意味着请求更快被发出去,不愿意在队列里等待。这通常带来:

  • 更低的等待时延;
  • 更小的平均 batch;
  • 吞吐提升空间变小。

值为 0

可以理解为“几乎不等”。这时服务的行为会更接近实时优先,但合批收益也会明显减弱。

因此,这个字段不是普通配置项,而是整个服务吞吐/延迟取舍的核心控制点之一。

再看 instance_group 在做什么

如果说 dynamic_batching 解决的是“单次执行规模能否变大”,那么 instance_group 解决的是“同一个模型要开几个执行副本”。

一个最简单的例子是:

1
2
3
4
5
6
7
instance_group [
{
count: 2
kind: KIND_GPU
gpus: [ 0 ]
}
]

这表示:

  • 在 GPU 0 上,为同一个模型启动 2 个实例。

从效果上看,这相当于让 Triton 拥有两个可并行接收任务的执行副本。这样做通常有利于提升并发请求场景下的接纳能力,尤其是在单实例不能很好填满 GPU 时。

但这件事的副作用也同样明显:多个实例会竞争同一块 GPU 的资源。

为什么实例数不是越多越好

这是 Triton 调优里非常容易被误解的一点。很多人看到 count 参数,会自然产生“副本越多,并发越高”的直觉。这个直觉只对了一半。

副本增加确实可能提升并发能力,但并不意味着性能一定更好。因为多个实例会同时带来三类影响:

  • 显存占用上升;
  • 计算与带宽资源被拆分;
  • 请求更容易被分散到多个小 batch,而不是集中成少数大 batch。

这最后一点尤其关键。

如果实例太多,请求可能刚到队列就被不同实例分别接走,结果每个实例拿到的批次都偏小。这样一来:

  • 合批收益下降;
  • 单次执行规模变小;
  • 吞吐可能不升反降。

所以,instance_group 的本质不是“尽可能多开”,而是“让模型实例数量与请求形态、模型规模和 GPU 资源达成平衡”。

dynamic_batchinginstance_group 是联动的

这两个配置项不能分开看。

一个很常见的误区是:

  • 一边把 max_queue_delay_microseconds 调大,希望形成更大的 batch;
  • 一边又把实例数开很多,把请求分散到不同副本上。

这两种动作在某些情况下是互相抵消的。

更直观地说:

  • dynamic_batching 倾向于把请求聚拢;
  • instance_group 过多时会把请求分流。

如果目标是做吞吐导向服务,通常需要让这两者协同,而不是彼此对冲。

一个更完整的配置示例

对上一篇里的 simple_mlp 模型,完整配置可以写成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 ] }
]

dynamic_batching {
preferred_batch_size: [ 4, 8, 16 ]
max_queue_delay_microseconds: 5000
}

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

这份配置并不代表最优,只代表一个合理的实验起点:

  • 开启服务端合批;
  • 给 Triton 一个有限的等待窗口;
  • 在同一块 GPU 上开两个实例;
  • 为后续压测提供一个可比较的基线。

不同目标下,配置思路为什么不同

调度配置不能脱离业务目标单独谈。至少可以分成三种典型取向。

目标一:吞吐优先

如果是离线批处理或吞吐优先场景,配置通常会更倾向于:

  • 更大的 max_queue_delay_microseconds
  • 更偏向较大批次的 preferred_batch_size
  • 谨慎增加实例数,避免过度分流

这里的思路是尽量让一次执行多做一点事。

目标二:实时交互优先

如果是实时交互或低延迟优先场景,配置通常会更倾向于:

  • 更小的等待上限,甚至接近 0
  • 更保守的批处理策略
  • 控制实例数,保证资源切分不过度

这里的思路是让请求尽快出队,而不是优先追求大 batch。

目标三:平衡模式

很多在线服务真正需要的是平衡模式,而不是极端吞吐或极端时延。这时常见策略是:

  • 给出一个中等等待窗口;
  • 让 Triton 有机会合出 4、8、16 这类中等批次;
  • 维持有限数量的实例副本;
  • 再通过压测决定是否继续偏向某一侧。

这通常也是最适合起步调参的模式。

为什么这篇还不谈 perf_analyzer

因为这一篇的任务是先把调度机制讲明白,而不是立刻进入实验指标。调度配置本身已经足够复杂,如果再把压测指标、并发扫描和结果解释全部并进来,读者会很容易失去主线。

更合理的顺序是:

  1. 先搞清楚调度参数分别控制什么。
  2. 再用压测工具验证这些参数实际造成了什么影响。

下一篇就会接着做第二件事。

常见误区

误区一:preferred_batch_size 是硬限制

它不是硬限制,而是优先目标。Triton 仍然可能在未达到这些大小时执行请求,尤其当等待时间已经到达上限时。

误区二:实例越多越好

实例增加会提高并发接纳能力,但也会带来资源竞争和请求分流问题。它从来不是单向收益。

误区三:只根据直觉改配置

调度配置很容易看起来“讲得通”,但真实效果仍然取决于:

  • 请求到达模式;
  • 模型规模;
  • GPU 资源;
  • 后端执行特征。

因此,任何配置判断最后都要落到压测数据上,而不是停留在经验印象。

结论

NVIDIA Triton 的调度层,核心不是某个字段本身,而是两件事:

  • dynamic_batching 决定请求能否被聚成更大的执行批次。
  • instance_group 决定模型执行能力如何在设备上并行展开。

这两者共同决定了服务更偏向高吞吐,还是更偏向低时延。

因此,理解 Triton 调度的关键不是记配置名,而是先建立一条稳定判断:

  • 合批倾向于提高吞吐,但会增加等待;
  • 多实例倾向于提高并发接纳能力,但会带来资源竞争与请求分流;
  • 两者必须联动考虑,不能分别优化。

下一篇会继续沿着这条线往前走,用 perf_analyzer 建立一套真正可落地的调参与验证闭环。

系列导航