Triton:07 调度、批处理与并发
这篇文章解决什么问题
上一篇已经把最小部署闭环打通了:模型能被 Triton 识别、服务能启动、一次请求可以拿到正确结果。
接下来真正进入服务化阶段时,第一个绕不开的问题不是“模型能不能跑”,而是“请求来了之后,Triton 怎么安排它们去跑”。
这件事直接决定了两个核心指标:
- 吞吐
- 延迟
而在 NVIDIA Triton 中,最核心的两个调度入口就是:
dynamic_batchinginstance_group
这篇文章的目标就是把这两个配置项背后的调度逻辑讲清楚,并建立一套更稳妥的配置思路。重点不是记住几个字段名,而是理解:
- 为什么批处理会提高吞吐。
- 为什么等待合批会抬高延迟。
- 为什么实例数不是越多越好。
Triton 不只是“来一个请求算一个请求”
如果服务端对每个请求都立刻执行,那么逻辑最直观,但硬件利用率往往并不好。尤其是模型较小、请求较碎、单次输入规模不大时,GPU 很容易处于“能跑,但没被喂满”的状态。
Triton 的调度层之所以重要,原因就在这里。它不只是把请求转发给模型,而是在到达执行前先做两类组织:
- 尝试把多个请求合并成更大的 batch。
- 决定同一个模型要不要同时开多个执行实例。
前者主要影响单次执行规模,后者主要影响并发接纳能力。两者一起决定了服务在吞吐和延迟之间的取舍方式。
先看 dynamic_batching 在做什么
dynamic_batching 的本质很简单:把一段很短时间窗口内到达的多个请求先放进队列,再尝试合并成更大的 batch 一起执行。
它解决的问题不是模型精度,也不是算子效率,而是:
如果单个请求太小,是否可以在服务侧把若干请求拼起来,让一次模型执行更值当。
可以把它想象成下面这个过程:
1 | 请求1(batch=1) --\ |
这件事对吞吐通常是有利的,因为:
- 单次执行规模变大;
- GPU 更容易被喂满;
- 每条请求分摊到的固定调度成本更低。
但代价同样直接:为了等更多请求凑成批次,部分请求必须在队列里多等一会儿。
一个最小 dynamic_batching 配置
最常见的起步配置可以写成这样:
1 | dynamic_batching { |
这两个字段分别控制不同事情。
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 | instance_group [ |
这表示:
- 在 GPU 0 上,为同一个模型启动 2 个实例。
从效果上看,这相当于让 Triton 拥有两个可并行接收任务的执行副本。这样做通常有利于提升并发请求场景下的接纳能力,尤其是在单实例不能很好填满 GPU 时。
但这件事的副作用也同样明显:多个实例会竞争同一块 GPU 的资源。
为什么实例数不是越多越好
这是 Triton 调优里非常容易被误解的一点。很多人看到 count 参数,会自然产生“副本越多,并发越高”的直觉。这个直觉只对了一半。
副本增加确实可能提升并发能力,但并不意味着性能一定更好。因为多个实例会同时带来三类影响:
- 显存占用上升;
- 计算与带宽资源被拆分;
- 请求更容易被分散到多个小 batch,而不是集中成少数大 batch。
这最后一点尤其关键。
如果实例太多,请求可能刚到队列就被不同实例分别接走,结果每个实例拿到的批次都偏小。这样一来:
- 合批收益下降;
- 单次执行规模变小;
- 吞吐可能不升反降。
所以,instance_group 的本质不是“尽可能多开”,而是“让模型实例数量与请求形态、模型规模和 GPU 资源达成平衡”。
dynamic_batching 和 instance_group 是联动的
这两个配置项不能分开看。
一个很常见的误区是:
- 一边把
max_queue_delay_microseconds调大,希望形成更大的 batch; - 一边又把实例数开很多,把请求分散到不同副本上。
这两种动作在某些情况下是互相抵消的。
更直观地说:
dynamic_batching倾向于把请求聚拢;instance_group过多时会把请求分流。
如果目标是做吞吐导向服务,通常需要让这两者协同,而不是彼此对冲。
一个更完整的配置示例
对上一篇里的 simple_mlp 模型,完整配置可以写成:
1 | name: "simple_mlp" |
这份配置并不代表最优,只代表一个合理的实验起点:
- 开启服务端合批;
- 给 Triton 一个有限的等待窗口;
- 在同一块 GPU 上开两个实例;
- 为后续压测提供一个可比较的基线。
不同目标下,配置思路为什么不同
调度配置不能脱离业务目标单独谈。至少可以分成三种典型取向。
目标一:吞吐优先
如果是离线批处理或吞吐优先场景,配置通常会更倾向于:
- 更大的
max_queue_delay_microseconds - 更偏向较大批次的
preferred_batch_size - 谨慎增加实例数,避免过度分流
这里的思路是尽量让一次执行多做一点事。
目标二:实时交互优先
如果是实时交互或低延迟优先场景,配置通常会更倾向于:
- 更小的等待上限,甚至接近 0
- 更保守的批处理策略
- 控制实例数,保证资源切分不过度
这里的思路是让请求尽快出队,而不是优先追求大 batch。
目标三:平衡模式
很多在线服务真正需要的是平衡模式,而不是极端吞吐或极端时延。这时常见策略是:
- 给出一个中等等待窗口;
- 让 Triton 有机会合出 4、8、16 这类中等批次;
- 维持有限数量的实例副本;
- 再通过压测决定是否继续偏向某一侧。
这通常也是最适合起步调参的模式。
为什么这篇还不谈 perf_analyzer
因为这一篇的任务是先把调度机制讲明白,而不是立刻进入实验指标。调度配置本身已经足够复杂,如果再把压测指标、并发扫描和结果解释全部并进来,读者会很容易失去主线。
更合理的顺序是:
- 先搞清楚调度参数分别控制什么。
- 再用压测工具验证这些参数实际造成了什么影响。
下一篇就会接着做第二件事。
常见误区
误区一:preferred_batch_size 是硬限制
它不是硬限制,而是优先目标。Triton 仍然可能在未达到这些大小时执行请求,尤其当等待时间已经到达上限时。
误区二:实例越多越好
实例增加会提高并发接纳能力,但也会带来资源竞争和请求分流问题。它从来不是单向收益。
误区三:只根据直觉改配置
调度配置很容易看起来“讲得通”,但真实效果仍然取决于:
- 请求到达模式;
- 模型规模;
- GPU 资源;
- 后端执行特征。
因此,任何配置判断最后都要落到压测数据上,而不是停留在经验印象。
结论
NVIDIA Triton 的调度层,核心不是某个字段本身,而是两件事:
dynamic_batching决定请求能否被聚成更大的执行批次。instance_group决定模型执行能力如何在设备上并行展开。
这两者共同决定了服务更偏向高吞吐,还是更偏向低时延。
因此,理解 Triton 调度的关键不是记配置名,而是先建立一条稳定判断:
- 合批倾向于提高吞吐,但会增加等待;
- 多实例倾向于提高并发接纳能力,但会带来资源竞争与请求分流;
- 两者必须联动考虑,不能分别优化。
下一篇会继续沿着这条线往前走,用 perf_analyzer 建立一套真正可落地的调参与验证闭环。
