深夜十一点,一家共享厨房的灯还亮着。三位厨师同时冲进来——主厨要煎牛排、甜点师要烤蛋糕、汤锅师傅要炖老火汤。三个人抢着同一排灶台:牛排还没下锅,烤箱计时器被改;汤刚烧开,火眼被另一个锅占走。一瞬间,厨房里满是报警声和吵架声。把场景搬到AI世界,这三位厨师就是三个AI模型,灶台就是GPU的计算核心,食材柜则是显存。共享厨房里没人指挥,结果就是一道菜都出不来。 ONNX Runtime 正是那位姗姗来迟的“厨师长”。他先给所有人固定编制(线程池管理),再把灶台改造成可以同时炖炒的多灶头(流并行),接着给每个人分配专属冷藏抽屉(内存配额控制),最后制定一套公平出菜顺序(调度策略)。一番整顿后,原本拥挤混乱的后厨变得井然有序——即使来了几十张订单、各式外卖平台同时接入,厨房照常运转。这就是“多模型并发调度”的核心故事:让多个模型共享一块GPU,却感觉每条推理请求都像在独享资源。

岗位关联

多模型并发调度是AI推理服务从“能跑”走向“扛得住”的关键一课。掌握它,你才能在多租户、高并发的生产环境中,让每一个模型都稳定、高效地输出结果。

目录
  1. 可视化演示
  2. 并发问题起源
  3. 线程池管理
  4. 流并行机制
  5. 内存配额控制
  6. 调度策略核心
  7. 吞吐量提升
  8. 多租户场景
  9. 监控与调优
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

演示多个模型共享GPU资源时通过线程池调度与内存配额分配,实现并发执行不阻塞,提升整体吞吐量。

本节配有交互动效(进入视口后自动加载,约 3KB)。

多模型并发调度 模型M1 模型M2 模型M3 线程池+内存配额并发

并发问题起源

共享厨房开业第一天就乱成一锅粥。主厨要煎牛排,甜点师要烤蛋糕,汤锅师傅要炖老火汤,三个人同时按下了灶台开关。火候互相干扰,烤箱计时器被抢改了好几次,最后夹生的夹生、过火的过火。把这场面搬到 AI 服务中,几位厨师就是多个模型,灶台和烤箱就是 GPU 的计算核心,食材柜则是显存。

当一个服务同时承载多个模型的推理请求时,GPU 上的资源是共享的。每个模型都需要计算单元,都需要显存,也都希望“即到即算”。如果没有任何协调,多个请求就会在计算核心和显存上互相碰撞:一个模型正在执行的矩阵乘法占满了 GPU,另一个模型只能干等;显存被某个模型一次性申请殆尽,其他模型连模型参数都加载不进去。结果就是执行互相阻塞,请求排队越堆越长,吞吐量直线下降。

这个现象的本质,和计算机体系结构中的阿姆达尔定律如出一辙:一段程序的加速上限,由其中不可并行的串行部分决定。对于多模型并发来说,模型之间的资源争抢、锁等待、显存分配冲突,就是那个无法消除的“串行比例”——GPU 核再多,也填不平彼此抢资源的坑。

所幸,ONNX Runtime 这位“厨师长”正是在这个乱局中登场。他不需要增加灶台,而是要让每一位厨师都高效、不打架地使用现有资源——守住串行损耗,并发才有意义。

S(N)=1(1−P)+PN\displaystyle S(N) = \frac{1}{(1-P) + \dfrac{P}{N}}
公式说明:S(N) 表示使用 N 份计算资源后获得的加速比;P 是程序中可并行部分的比例,1-P 是必须串行的部分。多模型场景中,1-P 正对应因争夺 GPU 而产生的等待时间。
  1. 加速比定义S(N) 衡量“投入更多计算资源后,性能提升了多少倍”,这是评估并发改造收益的核心指标。
    S(N)S(N)
  2. 串行部分占比(1-P) 是程序中不可并行的部分,在多模型抢占 GPU 时,对应锁竞争、显存排队等无法消除的开销。
    1−P1-P
  3. 并行部分分摊P/N 表示把可并行的工作平均分给 N 份资源后,每份资源需要承担的耗时。
    PN\dfrac{P}{N}
  4. 整体加速公式原耗时 1 除以新耗时(串行部分加上分摊后的并行部分),得到实际加速比。
    1(1−P)+PN\dfrac{1}{(1-P) + \dfrac{P}{N}}

线程池管理

厨师长上任后的第一项改革是固定编制。他不许再“谁想上手谁上手”,而是规定后厨只保留四名厨师:两名炒菜师傅、一名甜品师、一名汤锅师傅。无论订单多少,只能由这四个人轮流接活,绝不临时从外面拉人。这样一来,厨房里来回穿梭的人变少了,碰撞也少了。

ONNX Runtime 的线程池正是这套“固定编制”的技术实现。线程池在初始化时创建一组工作线程,它们从任务队列中领取模型推理产生的算子任务并执行。推理请求到来时,只需把任务送进队列,由池中的空闲线程接手,而无需临时创建线程。这既省去了频繁创建、销毁线程的开销,也避免了线程数量无节制增长导致系统资源被耗尽。

线程池的规模需要精心设定。线程数太少,任务排队等待;线程数太多,CPU 在多个线程间切换本身就会消耗大量时间。每个线程切换都需要保存和恢复寄存器状态、更新调度队列,这些“切换开销”在任务密集时会被急剧放大,反而拖慢整体推理速度。

厨师长把“临时工”模式改成固定团队后,后厨终于不再人挤人。但人的问题解决了,灶台的分配和食材的存放还有一堆难题等着他。

Ctotal=Texec+Nswitch×Tswitch\displaystyle C_{total} = T_{exec} + N_{switch} \times T_{switch}
公式说明:一个推理任务的总耗时,等于实际执行时间加上线程切换带来的额外时间。切换次数越多,无谓开销越大,因此线程池规模要与 CPU 核数匹配。
  1. 任务总耗时C_total 表示一个推理任务从进入线程池到完成所花费的总时间。
    CtotalC_{total}
  2. 实际执行时间T_exec 是线程真正在 CPU 上执行算子的时间,这是产生价值的核心部分。
    TexecT_{exec}
  3. 切换次数与开销N_switch 是线程切换次数,T_switch 是单次切换的成本,两者相乘得到切换总开销。
    Nswitch×TswitchN_{switch} \times T_{switch}
  4. 总耗时构成执行时间与切换开销相加,说明线程数越过临界点后,切换成本会吃掉并行收益。
    Texec+Nswitch×TswitchT_{exec} + N_{switch} \times T_{switch}

流并行机制

固定了编制,厨师长又发现新问题:四名厨师不撞人了,但灶台依然不够用。他索性把每个灶台改造成双灶头——炖汤的同时还能炒菜,烤蛋糕的同时还能炖肉。只要两道菜不在同一个火眼上,就能同时推进。这个看似不起眼的改造,让后厨效率直接翻倍。

在 GPU 上,CUDA 流就是“双灶头”。流是 GPU 上执行一系列相互独立操作的通道,不同流之间的算子可以并行执行。ONNX Runtime 把不同模型的计算任务分发到不同的流上:A 模型跑矩阵乘法时,B 模型的卷积可以同时在另一个流里运行,两者互不干扰,真正实现了“并行而非排队”。

流并行的另一个好处是异步化。一个模型往流里提交计算任务后,CPU 可以立刻返回,去调度下一个模型的任务;GPU 则自行协调多个流中操作的重叠执行。当然,如果两个流之间存在数据依赖,调度器必须插入同步点,确保前一个任务完成后,后一个才能开始——这就好比“炖汤的锅必须等汤料备齐了才能下锅”。

多灶并行让共享厨房的出品效率大幅提升:炖汤的、炒菜的、烤蛋糕的互不干扰。但灶台平了,新的矛盾又冒出来了——食材柜开始不够用。

Tparallel=max⁡(t1,t2,…,tn),Tsequential=∑i=1nti\displaystyle T_{parallel} = \max(t_1, t_2, \ldots, t_n), \quad T_{sequential} = \sum_{i=1}^{n} t_i
公式说明:n 个独立任务串行执行的总耗时是时间之和;放到多条流上并行后,总耗时只取决于耗时最长的那个任务。并行带来的收益一目了然。
  1. 串行总耗时T_sequential 是所有任务逐个执行的时间总和,洗衣机一个接一个地转,最后一锅要等前面所有锅都完成。
    Tsequential=∑i=1ntiT_{sequential} = \sum_{i=1}^{n} t_i
  2. 并行总耗时T_parallel 是这些任务同时开始后,整体完成所花的时间,只由最慢的那个任务决定。
    Tparallel=max⁡(t1,t2,…,tn)T_{parallel} = \max(t_1, t_2, \ldots, t_n)
  3. 对比意义只要任务数大于 1,max 一定小于 sum,流的并行价值就在这一小一大之间体现。
    max⁡(t1,t2,…,tn)<∑i=1nti\max(t_1, t_2, \ldots, t_n) < \sum_{i=1}^{n} t_i

内存配额控制

灶台的冲突解决了,厨师们又开始抢食材柜。一名厨师囤了一整箱稀缺食材,甜点师要用时却找不到。厨师长的解决方式非常直接:每个人分配一个固定的冷藏抽屉,里面只能放规定数量的食材;谁要超量,就先把自己的旧存货清出去。

GPU 显存的管理遵循同样的道理。显存是模型的“食材柜”,如果分配不当,某个模型可能霸占绝大部分显存,其他模型连参数都加载不进去。ONNX Runtime 通过内存配额控制,为每个模型设定显存使用上限,并由内存分配器严格把关:一旦申请总量达到配额,就拒绝新的申请,必须等释放后才能继续。

在实现层面,ONNX Runtime 引入了 Arena(竞技场)分配器。Arena 一次性向 GPU 申请一块较大的显存,然后在这块连续区域内为算子分配小块空间;算子释放后,空间能被后续任务复用,而不是反复向驱动申请和归还。这种机制显著减少了显存碎片化,配合配额上限,让显存这块“共享食材柜”得到高效利用。

制冷抽屉各安其位后,食材浪费和争抢消失了。厨师长擦了擦汗,把目光投向最复杂的问题——面对雪片般飞来的订单,到底应该让谁先做?

∑i=1nQi≤MGPU\displaystyle \sum_{i=1}^{n} Q_i \le M_{GPU}
公式说明:n 个模型的显存配额 Q_i 之和不得超过 GPU 总显存 M_GPU。配额是硬边界,超过即拒绝,从根上防止单一模型耗尽整块显存。
  1. 单个模型配额Q_i 表示第 i 个模型被允许使用的最大显存,这是调度系统为它划定的“冷藏抽屉”大小。
    QiQ_i
  2. 配额求和把所有模型的配额加起来,得到的是设计时刻的显存规划总量。
    ∑i=1nQi\sum_{i=1}^{n} Q_i
  3. 总显存约束配额总和必须小于等于 GPU 总显存,保证每个模型都有空间、互不侵占。
    ∑i=1nQi≤MGPU\sum_{i=1}^{n} Q_i \le M_{GPU}

调度策略核心

高峰时段一到,点菜单子雪片般飞来。厨师长没有按先来后到傻排队,因为他心里清楚:十人桌的家宴和一杯下午茶的外卖,着急程度完全不同。他给每类订单设定了一个权重,再按权重分配出菜顺序——大订单拿大份额,小订单也有自己的出菜窗口。

ONNX Runtime 的调度器同样是这个逻辑。它维护着多个请求队列,推理请求先入队,再由调度策略决定何时提交给 GPU 执行。不同模型可以配置不同的优先级:核心在线服务延迟敏感,权重就调高;离线批处理任务能容忍等待,权重就调低。调度器按权重动态分配 GPU 资源,防止某个模型的突发流量把其他模型“饿死”。

当某个请求已经在 GPU 上执行时,调度器还能在模型之间灵活切换:一个模型的流还有空闲,就顺手把排队中另一个模型的任务提交上去,充分利用 GPU 的空闲周期。这种多模型之间的负载均衡,本质上是让资源在时间维度和空间维度上都流动起来,而不是僵硬地“一队排到底”。

有了这套加权公平调度,共享厨房的每一家档口都不挨饿了。但厨师长的目标远不止“不挨饿”,他想让整个厨房在同样的人手和灶台下,出更多的菜。

Si=Wi∑jWj×Rtotal\displaystyle S_i = \frac{W_i}{\sum_{j} W_j} \times R_{total}
公式说明:模型 i 分到的资源份额 S_i,等于它的权重 W_i 占全部模型权重之和的比例,再乘以总资源 R_total。权重越高,份额越大。
  1. 权重份额W_i 是模型 i 的优先级权重,表示它在调度器心中的“重要程度”。
    WiW_i
  2. 权重归一化W_i 除以所有模型权重之和,得到一个 0 到 1 之间的比例,代表这个模型在总权重中的占比。
    Wi∑jWj\dfrac{W_i}{\sum_{j} W_j}
  3. 资源分配用这个占比乘以总资源 R_total,得到模型 i 实际获得的 GPU 资源份额。
    Wi∑jWj×Rtotal\dfrac{W_i}{\sum_{j} W_j} \times R_{total}

吞吐量提升

调度理顺之后,厨师长开始琢磨怎么让厨房整体“出菜量”更大。他发现,与其一单一单地做,不如把同一道菜的多份订单合并成一锅:一份披萨要烤十五分钟,十份披萨也是一样的十五分钟。同时出炉,效率翻了好几倍。这一招,在推理服务里就叫批处理。

批处理(batching)把多个推理请求聚合为一个批次,GPU 一次前向推理同时计算多份输入。矩阵乘法这类算子在大批量的数据形状下,能更充分地利用 GPU 的计算单元,吞吐量随之大幅上升。再加上多模型并发,多个模型各自的批次可以同时在多流上推进,整块 GPU 的算力被摊得又薄又满。

吞吐量和延迟之间存在天然张力:批次越大,单次处理耗时就越长,队列中请求的等待时间也会拉长。优秀的调度器会动态调节批次大小——GPU 空闲时加大批次“乘胜追击”,延迟告警时果断缩小批次保响应。吞吐量与延迟的平衡,是调度器最见功力的地方。

共享厨房的菜从一桌一桌做,变成几桌一起做,高峰期营业额翻了一倍,顾客的等待时间却没怎么变长。可这时候,更大的挑战来了——这座厨房要变成美食广场的共享后厨。

TP=B×CL\displaystyle TP = \frac{B \times C}{L}
公式说明:吞吐量 TP 等于批大小 B 乘以并发模型数(或并发请求数)C,再除以单次推理的平均延迟 L。分子越大、分母越小,系统单位时间处理请求的能力越强。
  1. 批大小B 是每次推理合并的请求数,一批 10 个请求和一批 1 个请求,计算耗时几乎相同。
    BB
  2. 并发数量C 是同时执行推理的模型或请求数,对应多模型并发调度的直接收益。
    CC
  3. 平均延迟L 是单个批次从提交到完成的耗时,它是吞吐量的分母,延迟越低,吞吐越高。
    LL
  4. 吞吐量计算分子相乘再除以延迟,得到单位时间内完成的推理请求数,这是服务容量的核心指标。
    B×CL\dfrac{B \times C}{L}

多租户场景

共享厨房的故事还有后半段。后来,这座厨房搬进了一家美食广场:川菜档口、日料档口、轻食档口各自经营,却共用同一个后厨。每个档口都要求,隔壁家再怎么爆单,也不能把我的灶火和冰柜占光。这就是多租户场景的原始诉求。

在多租户 AI 推理平台中,多家客户(租户)共享同一批 GPU 服务器,每家都部署自己的模型、拥有自己的请求流量。调度器必须实现真正的租户隔离:显存配额各自独立,线程池中的任务互不串扰,一个租户的流量高峰不能拖垮另一个租户的服务质量。这里的“隔离”不是物理隔离,而是靠调度策略画出的虚拟边界。

更重要的是服务质量保证。每个租户签约时都有明确的资源承诺,比如“高峰期 P99 延迟不超过 100ms”。并发调度器通过权重分配、配额上限和优先级排队来兑现承诺,同时预留弹性空间,让空闲资源能在租户之间动态调剂——哪家档口暂时没人点单,它的灶火可以暂时借给隔壁,但高峰期必须归还。

美食广场开得红红火火,每个档口都觉得“这厨房好像是我家开的”。但这套魔术并非一劳永逸,厨师长还需要一双时刻盯着看板的眼睛。

Ui=usediquotai≤1\displaystyle U_i = \frac{used_i}{quota_i} \le 1
公式说明:租户 i 的瞬时资源使用量 used_i 与配额 quota_i 的比值称为占用率 U_i。只要占用率不超过 1,租户的资源边界就是安全的,调度器的隔离承诺就有效。
  1. 使用量used_i 表示租户 i 当前实际使用的显存或计算资源,是动态变化的瞬时值。
    usediused_i
  2. 资源配额quota_i 是租户 i 签约获得的资源上限,代表调度器承诺给它的最大份额。
    quotaiquota_i
  3. 占用率约束使用量与配额的比值不超过 1,是租户隔离的数学表达——一旦超过,就必须触发调度干预。
    usediquotai≤1\dfrac{used_i}{quota_i} \le 1

监控与调优

维持魔术的前提,是厨师长随时掌握后厨的真实状态:今天平均出菜要多久?哪台灶一直空转?哪个档口的食材经常见底?只有数据在手,他才知道明天要不要多分配一个灶台、把哪个抽屉的配额调大一点。

ONNX Runtime 的推理服务同样离不开监控。生产环境通常持续采集 GPU 利用率、推理延迟分布、请求吞吐量、显存占用和队列长度等指标。这些数据直接反映了调度策略是否健康:如果 GPU 利用率很低,说明流没有铺满;如果延迟飙高,可能是线程池太小或者批次过大;如果频繁 OOM,多半是配额设置不当。

调优是一个循环迭代的过程:先观察监控数据,定位瓶颈,再调整线程数、批次大小、显存配额和模型权重,然后评估效果,继续下一轮优化。所有调优动作都围绕一个核心指标展开——GPU 是否真的在“忙碌并高效地忙碌着”。过低的利用率浪费算力,过高的利用率则往往伴随排队抖动,理想的区间是留出少量余量,既能满载又不至于积压。

厨师长的看板从一张白纸变成实时作战地图,共享厨房的每一次运转都在数据里留下痕迹。而看板的最终目标只有一句话:让每一位厨师都在忙碌,让每一道菜都不迟到。

UGPU=TbusyTtotal\displaystyle U_{GPU} = \frac{T_{busy}}{T_{total}}
公式说明:GPU 利用率等于采样周期内 GPU 实际忙碌的时间 T_busy 除以总时间 T_total。它反映 GPU 在多大程度上被有效利用,是调度调优的“体温计”。
  1. 忙碌时间T_busy 是在一段采样时间内,GPU 的计算单元真正在执行算子而没有空转的时间总和。
    TbusyT_{busy}
  2. 总采样时间T_total 是监控统计的完整时间窗口,例如 1 秒或 1 分钟。
    TtotalT_{total}
  3. 利用率核算忙碌时间除以总时间,得到 GPU 利用率;接近 1 说明资源吃紧,偏低则说明还有调度优化的空间。
    TbusyTtotal\dfrac{T_{busy}}{T_{total}}

AI 应用场景

多租户 GPU 推理服务平台

阶段:模型部署与推理服务化

解决的问题:多家客户模型共享同一批 GPU 时,资源争抢导致服务稳定性难以保证。传统做法是给每个客户单独分配 GPU,成本高昂且利用率低。ONNX Runtime 的多模型并发调度,让多个租户的模型在同一块 GPU 上各自隔离、按权重公平调度,既压低了硬件成本,又保障了各自的延迟承诺。

具体案例:某 SaaS 公司为一个图像识别平台同时托管 20 个客户模型,但 GPU 只有 4 块。通过 ONNX Runtime 线程池、内存配额和权重调度,每个客户单独设定显存上限和优先级,高峰时段 P99 延迟稳定在 80ms 以内,GPU 平均利用率从 35% 提升到 78%。

边缘端多模型实时推理

阶段:边缘部署与实时推理

解决的问题:无人配送车需要同时运行物体检测、红绿灯识别和路径规划三个模型,车载 GPU 算力有限。若三个模型串行执行,总延迟会超过决策安全窗口;ONNX Runtime 通过流并行让三个模型在不同 CUDA 流上重叠执行,互不阻塞。

具体案例:某无人配送机器人原型中,检测与规划模型原本串行耗时 70ms;接入 ONNX Runtime 多流并行调度后,压缩至 30ms 以内,为避障决策留出了充裕的缓冲时间。

推荐系统多级模型推理链路

阶段:在线推理服务

解决的问题:推荐链路通常包含召回、粗排、精排三个模型,串行执行会显著拉高请求总延迟。通过 ONNX Runtime 并发调度,把三个模型的执行窗口打平重叠,在 GPU 空闲时段并行推进,是降低链路延迟的有效手段。

具体案例:某电商推荐团队将三个模型放入同一个 ONNX Runtime 服务进程,利用多模型并发调度让精排和召回并行,链路总延迟下降 40%,峰值吞吐提升 60%,大促期间无需临时扩容。

Python 演示

python68 行
import threadingimport timeimport randomfrom concurrent.futures import ThreadPoolExecutor  class GpuSimulator:    """模拟 GPU 的显存与流配额控制"""     def __init__(self, total_mem=512, max_streams=2):        self.total_mem = total_mem        self.max_streams = max_streams        self._mem_used = 0        self._stream_used = 0        self._mem_lock = threading.Lock()        self._stream_lock = threading.Lock()     def run_task(self, mem, duration):        """申请流配额后执行计算任务"""        while not self._acquire_stream():            time.sleep(0.01)        try:            with self._mem_lock:                if self._mem_used + mem > self.total_mem:                    raise MemoryError("GPU 显存配额不足")                self._mem_used += mem            time.sleep(duration)            with self._mem_lock:                self._mem_used -= mem        finally:            self._release_stream()     def _acquire_stream(self):        with self._stream_lock:            if self._stream_used >= self.max_streams:                return False            self._stream_used += 1            return True     def _release_stream(self):        with self._stream_lock:            self._stream_used -= 1  def run_model(model_id, mem, latency, gpu):    gpu.run_task(mem, latency)    print(f"Model-{model_id} inference done, mem_used={gpu._mem_used}")  def main():    gpu = GpuSimulator(total_mem=512, max_streams=2)    with ThreadPoolExecutor(max_workers=4) as pool:        requests = [            (0, 200, 0.6),            (1, 220, 0.8),            (2, 150, 0.4),            (0, 200, 0.5),            (1, 220, 0.7),            (2, 150, 0.3),        ]        futures = [pool.submit(run_model, *req, gpu) for req in requests]        for fut in futures:            fut.result()    print("All inference requests completed.")  if __name__ == "__main__":    main()
目的:用 Python 模拟 ONNX Runtime 多模型并发调度的两个核心机制——流配额限制并发执行数、显存配额防止单一模型占满 GPU,并通过线程池提交多模型的推理请求。
  1. class GpuSimulator:定义 GPU 模拟器,持有限流和显存的锁与计数器。
  2. while not self._acquire_stream():不断尝试获取流配额,拿不到就睡眠等待,模拟 GPU 流并行上限。
  3. if self._mem_used + mem > self.total_mem:显存配额检查,超限直接抛 MemoryError,模拟配额控制的硬边界。
  4. time.sleep(duration)在获取资源后模拟推理计算耗时,释放前保持占用状态。
  5. with ThreadPoolExecutor(max_workers=4) as pool:创建线程池,控制同时运行的推理请求数量。
  6. futures = [pool.submit(run_model, *req, gpu) for req in requests]把多个模型的请求提交到共享线程池,实现多模型并发调度。
  7. for fut in futures:逐个等待任务完成,确保主线程在所有推理结束后汇总输出。

常见误区

误区听说线程池能提升并发,就把线程数设成几百。反正并发数越高吞吐越高,多设几个线程总没坏处。

正解线程池的本质是“复用”而非“扩容”。当线程数超过 CPU 核数后,多出来的线程并不能并行执行,反而会带来大量上下文切换开销,拖慢推理速度。ONNX Runtime 默认线程数接近 CPU 核数,正是基于这个平衡点。

为何常见:人们常把“并发”简单理解为“线程多”,忽略了操作系统调度线程所付出的真实成本。

误区GPU 显存一紧张,就换更大的显卡或加购 GPU 服务器。但换完之后,OOM 问题依旧频繁出现。

正解显存问题往往是分配不均和碎片化导致的,而不是总量不够。Arena 分配器的块复用和配额控制能从源头减少碎片、约束单一模型的显存占用,先做配额约束,再谈扩容才有意义。

为何常见:看到“内存不足”第一反应是扩容,很少有人分析分配器的使用模式和碎片率。

误区流是并行通道,那就开 50 条流,把 50 个模型任务全塞进去一起跑,肯定更快。

正解GPU 的流处理器(SM)数量是有限的,流过多只会在大量流之间增加调度开销,而不会创造额外的计算资源。流的数量应该与 GPU 的并行能力匹配,而非贪多。

为何常见:把“流”当成无限并发通道,忽略了每个流背后都要占用 GPU 调度器的跟踪和排队资源。

误区既然只有一个 GPU,多模型就只能一个个串行排队。模型一多,性能必崩,干脆让它们排队等吧。

正解不同模型的计算在流上可以重叠,尤其是计算密集型和访存密集型的算子在 GPU 内部有交错执行的空间。配合多流并行、线程池和调度策略,多个模型可以在同一块 GPU 上同时运行且互不阻塞。

为何常见:只看到“共享等于排队”,不了解 GPU 内部执行单元可以同时处理来自多个流的算子。

误区ONNX Runtime 是自动优化引擎,模型丢进去它就会自己调好一切,不需要人工设置线程池、配额、权重这些参数。

正解ONNX Runtime 提供自动优化能力,但多租户场景下的资源配额、优先级和隔离边界,必须由部署者根据业务 SLA 手动配置。不同业务模型适合的线程数和批次大小差异巨大,自动调度的默认值只能算起点。

为何常见:把“开箱即用”误认为“全自动最优”,忽略了部署调优本身就是工程核心工作的一部分。

面试题

Q1. 多模型共享 GPU 时,最需要解决的问题是什么?

单选题 · easy

  • A.模型文件大小超限
  • B.推理请求互相冲突← 正确答案
  • C.模型权重更新过慢
  • D.训练数据噪声过大

答案:B。多个模型共享 GPU 的关键矛盾是推理请求同时执行时的资源冲突,而非文件大小、权重更新或训练数据问题。这就像共享厨房里最怕的是抢灶台,而不是菜谱好坏。

Q2. ONNX Runtime 的线程池主要负责什么?

单选题 · easy

  • A.管理并发执行的线程数← 正确答案
  • B.保存模型的训练历史
  • C.压缩模型权重大小
  • D.生成新的训练样本

答案:A。线程池的核心职责是复用有限数量的工作线程、控制并发执行规模,避免每次请求都创建新线程。保存训练历史、压缩权重、生成样本都不是线程池的功能。

Q3. 把多个模型的任务放到不同 CUDA 流上,主要目的是什么?

单选题 · easy

  • A.让同一时刻并行执行更多任务← 正确答案
  • B.让同一时刻串行等待更多任务
  • C.让同一时刻压缩模型的大小
  • D.让同一时刻加密推理的数据

答案:A。不同 CUDA 流之间的算子可以并行执行,多流的核心目的就是让互不依赖的模型任务同时推进。串行等待、压缩模型、加密数据都与流的机制无关。

Q4. GPU 显存配额控制最主要的作用是?

单选题 · mid

  • A.防止单个模型耗尽全部显存← 正确答案
  • B.加快模型在 GPU 上的推理速度
  • C.降低模型训练时的学习率
  • D.自动备份模型的权重参数

答案:A。内存配额为每个模型划定显存上限,防止某个模型的突发申请占满显存,导致其他模型无法加载。配额是资源隔离手段,不直接影响推理速度、学习率或备份。

Q5. 下列哪些属于 ONNX Runtime 多模型并发调度的核心手段?

多选题 · mid

  • A.线程池调度管理← 正确答案
  • B.流并行调度机制← 正确答案
  • C.内存配额控制机制← 正确答案
  • D.图像数据增强处理

答案:ABC。多模型并发调度的三大支柱是线程池管理、流并行和内存配额控制。图像数据增强属于数据预处理范畴,与调度无关。

Q6. 在加权公平调度中,权重较大的模型会获得什么?

单选题 · mid

  • A.更大的 GPU 资源份额← 正确答案
  • B.更大的网络带宽额度
  • C.更高的模型精度要求
  • D.更少的显存分配配额

答案:A。加权公平调度的核心公式是资源份额 = 权重占比 × 总资源,权重越大,资源份额越大。权重只影响资源分配,不改变网络带宽、模型精度或配额反向缩减。

Q7. 在吞吐量公式 TP = B × C / L 中,B 代表什么?

单选题 · hard

  • A.批处理请求的数量大小← 正确答案
  • B.GPU 显存总量的数值
  • C.线程池线程数量的多少
  • D.模型网络层数的总数

答案:A。TP = B × C / L 中,B 是批大小,C 是并发模型或请求数,L 是平均延迟。B 与批量聚合直接相关,和显存总量、线程数、网络层数无关。

Q8. 关于 GPU 利用率 U = T_busy / T_total,下列判断正确的是?

单选题 · mid

  • A.利用率越高说明资源利用越充分← 正确答案
  • B.利用率必须始终维持在 50%
  • C.利用率与并发请求数量无关
  • D.利用率只能一次性计算完成

答案:A。GPU 利用率是忙碌时间占总时间的比例,数值越高表示算力被利用得越充分。但并不追求 100%,需要留出缓冲避免排队抖动;利用率也可以持续采样计算。

Q9. 多租户场景中,并发调度器需要保证哪些能力?

多选题 · mid

  • A.租户之间实现资源隔离← 正确答案
  • B.保证每个租户公平份额← 正确答案
  • C.保障服务质量并避免波动← 正确答案
  • D.直接删除低效租户模型

答案:ABC。多租户调度器要保证资源隔离、公平份额和服务质量。删除低效租户模型是业务运维决策,不是调度器应具备的能力,调度器只能做资源层面的约束。

Q10. 下列哪些原因会导致多模型并发性能下降?

多选题 · hard

  • A.线程频繁进行上下文切换← 正确答案
  • B.过多流导致调度开销增大← 正确答案
  • C.显存碎片化变得日益严重← 正确答案
  • D.模型日志打印等级调低

答案:ABC。上下文切换过多、流数量超过 GPU 调度能力、显存碎片化严重都会拖慢整体并发推理。日志等级调低反而减少 CPU 开销,不会导致性能下降。