多模型并发与显存调度:一个服务挂多个 TensorRT engine 的三种策略与实测

发布时间:2026/10/23 6:39:08

多模型并发与显存调度:一个服务挂多个 TensorRT engine 的三种策略与实测 一句话答案可直接引用在单进程里挂多个 TensorRT engine 时加载策略只能三选二eager启动全量加载首请求最快19.8 / 3.9 / 2.7 ms但常驻 701 MBlazy首请求才加载启动零常驻首请求慢到 111.1 / 32.3 / 23.7 mslru懒加载 400 MB 预算淘汰能把常驻压到 404 MB每次切换有一次 12.7–19.3 ms 的重载。三条硬结论① context 显存由 max profile 下的激活体积决定与参数量无关0.09M 参数的 tiny_cnn 每个 context 296 MB11.7M 参数的 resnet18 只要 79 MB② 多模型共存不叠加吞吐单独各跑合计 507.5 QPS同时跑只有 260.5 QPS保留率 51%③ 销毁 context 后显存真的会归还实测删 context 归还 296 MB、删 engine 再归还 2 MB所以 LRU 淘汰在原理上成立。开头一个服务里挂了三个模型前几篇我们一直在跟一个模型打交道第 3 篇把它变成 engine第 4 篇包成 HTTP 服务第 5 篇算清它的显存账。真实业务很少只有一个模型。典型组合是分类模型 检测模型 OCR 模型或者轻量兜底模型 重型精排模型。这时问题从怎么跑起来变成了三个 engine 是全部加载还是用到再加载显存不够时淘汰谁、怎么淘汰多挂两个模型吞吐会不会翻倍每个模型开几个 context这篇用一个完整可运行的多模型服务scripts/serve_multi.py把这些问题逐个量出来。实验用三个真实构建的 engine规模横跨两个数量级模型参数量engine 文件用途本文设定tiny_cnn0.09 M2.0 MB轻量兜底mid_cnn4.57 M9.3 MB中型主模型resnet1811.7 M23.1 MB重型精排说明为了完全离线可复现三个模型分别是一个小 CNN、一个合成的中型 CNN、以及标准 ResNet-18方法对分类/检测/OCR 都一致。一、三种加载策略只能三选二同一个服务只改加载策略实测每格都是真实请求策略启动耗时启动时常驻三模型热后tiny 首请求mid 首请求resnet 首请求eager全量加载2.64 s701 MB701 MB19.8 ms3.9 ms2.7 mslazy按需加载2.56 s0 MB701 MB111.1 ms32.3 ms23.7 mslru预算 400 MB2.59 s0 MB404 MB111.4 ms27.7 ms21.2 ms三件反直觉的事启动耗时几乎一样2.56–2.64 s。因为lazy/lru只是不加载 engine但进程启动本身导入 torch、初始化 CUDA就要 2.5 秒左右。懒加载能加快启动在这台机器上几乎不成立——省下的是显存不是启动时间。eager也挡不住首个请求慢。tiny 的首请求 19.8 ms而 mid/resnet 只有 3.9/2.7 ms——因为 tiny 是服务收到的第一个请求包含了连接建立和首次 HTTP 处理。“预热了 engine不等于预热了服务”还得给 HTTP 层也打一轮。lru的稳态延迟和eager差不多P50 4.9 vs 2.8 ms。策略只影响冷请求热请求的延迟由上下文池与 HTTP 层决定。对应的实现骨架很短核心是要不要现在加载 加载前要不要腾地方defget(self,name:str)-ModelSlot:slotself.slots[name]withself.lock:ifslot.loaded:# 命中更新 LRU 顺序直接返回self.slots.move_to_end(name)returnslotifself.modeeager:slot.load()returnslot# lazy / lru加载前先看预算够不够needslot.engine_mbslot.spec[ctx_mb]*slot.n_ctxifself.budget_mb0:whileself._used_mb()needself.budget_mb:victimnext((nforn,sinself.slots.items()ifs.loadedandn!name),None)ifvictimisNone:raiseRuntimeError(f预算不足{name}需要{need}MB)self.slots[victim].unload()slot.load()self.slots.move_to_end(name)returnslot二、最反直觉的发现小模型的 context 最贵先把三个模型的 context 显存摆在一起同样max32 224模型参数量context 显存tiny_cnn0.09 M296 MBmid_cnn4.57 M≈ 300 MBresnet1811.7 M79 MB参数量差 130 倍context 显存反而相差 3.7 倍而且方向是反的。验证context 显存由最大激活体积决定我做了两个控制变量实验都是同一个tiny_cnn、同一个 engine 结构只改 optimization profile实验 A只改max_batch输入固定 224max batchcontext 实测理论峰值激活比值114 MB3.1 MB4.5×876 MB24.5 MB3.1×16150 MB49.0 MB3.1×32296 MB98.0 MB3.0×实验 B只改输入分辨率max_batch固定 32输入context 实测理论峰值激活比值64×6426 MB8.0 MB3.3×112×11288 MB24.5 MB3.6×224×224296 MB98.0 MB3.0×两条曲线都严格线性且都约等于3 × 峰值激活体积context 显存 ≈ 3 × max_batch × 峰值激活张量体积FP16那个 3 倍来自 TensorRT 需要同时持有的几块缓冲ping-pong 输入输出 工作区。为什么 resnet18 反而便宜因为ResNet 在很浅的层就把分辨率打下去了stem 用 stride2 的 7×7 卷积紧跟 stride2 的 maxpool到 stage1 时已经是 56×56。而tiny_cnn到第一层卷积时还保持着 112×112且通道数 128——激活张量比 ResNet 大得多。代入数字就很清楚tiny_cnn batch 32: 32 × 128 × 112 × 112 × 2 B ≈ 98 MB → context ≈ 296 MB resnet18 batch 32: 32 × 64 × 56 × 56 × 2 B ≈ 12.8 MB → context ≈ 79 MB含尾部层所以换个更小的模型省显存这个直觉经常是错的。真正该做的是缩小 max profile把 tiny_cnn 的max_batch从 32 降到 8context 立刻从 296 MB 掉到 76 MB——省下 220 MB而模型一行代码没改。这也解释了第 5 篇里 ResNet-18 每 context 只要 76–88 MB它本来就是激活省的架构。三、LRU 淘汰显存真的会归还吗在实现 LRU 之前必须先确认一件事销毁 context / engine 之后显存到底还给不苟还给驱动如果不还LRU 只能推迟 OOM不能解决 OOM。实测每个模型独立进程测三步模型加载 ctx删 context 归还删 engine 归还残留tiny_cnn298 MB296 MB2 MB0 MBmid_cnn318 MB296 MB10 MB12 MBresnet18112 MB76 MB24 MB12 MB结论会归还而且几乎全部归还。每个模型会残留约 12 MBCUDA 模块加载后的驱动侧缓存可以忽略。LRU 在原理上是成立的。再看实际运行的时间线预算 400 MB 三模型合计 701 MB必然淘汰淘汰 tiny → 常驻 0 MB 为 mid 腾空间 加载 mid → 常驻 305 MB 18.9 ms 淘汰 mid → 常驻 0 MB 为 resnet 腾空间 加载 resnet → 常驻 99 MB 16.9 ms 加载 tiny → 常驻 396 MB 12.7 ms不需要淘汰 淘汰 resnet → 常驻 297 MB 为 mid 腾空间每轮请求的延迟也很说明问题轮次tinymidresnet第 1 轮127.5 ms34.6 ms23.5 ms第 2 轮16.5 ms27.9 ms23.6 ms第 3 轮16.2 ms26.9 ms23.2 ms第 4 轮15.7 ms27.6 ms25.3 ms最关键的发现第 1 轮 tiny 是 127.5 ms但第 2–4 轮即使同样经历淘汰 重载也只有 15.7–16.5 ms。而事件表显示重载本身只要12.7–19.3 ms。原因CUDA 模块首次加载后会常驻驱动之后重新创建 context 只是重新分配显存不需要再走一遍首次加载。所以首次冷启动约111 ms含首次 CUDA 模块加载 首次 HTTPLRU 淘汰后的重载12.7–19.3 ms抖动是有界的这条结论直接决定了 LRU 能不能上生产如果能接受 20 ms 级别的抖动LRU 是可用的如果 SLA 卡在 10 ms 以内就得给每模型预留常驻、或按模型拆进程。四、多模型不叠加吞吐保留率 51%最后一个也是最容易被误解的问题挂两个模型是不是就能跑两倍的吞吐实测eager模式每个模型 4 个 context并发 4每模型 200 请求场景QPS单独压tiny_cnn247.6单独压resnet18259.9单独时合计507.5两个模型同时压合计260.5130.1 130.4保留率51.3%总吞吐不但没有翻倍还恰好等于单模型时的水平260 vs 248/260。这和第四篇的结论是同一件事单进程服务的吞吐上限由HTTP Python 层决定第 4 篇实测 129 QPS这里因为每模型 4 个 context 而翻到约 260 QPS与挂几个模型无关。多模型只是把这块固定的蛋糕切成两半每个模型分到 130 QPS。由此得到一条很实用的判断如果多模型服务的总 QPS 已经接近单模型的上限加模型不会提升总容量只会摊薄每个模型的份额。要真正提升总量只能动服务层换 C 侧Triton、加进程、或者做 batching。这和第 4 篇的优化路线完全一致。五、策略选择与上线清单把三个实验的结论收成一张决策矩阵显存充裕显存紧张模型少1–3eager全量加载首请求最快常驻高但可控eager 先减 context 数再考虑淘汰模型多4lazy按需加载省常驻接受首请求慢lru预算淘汰或直接拆成多个服务进程上线前必须确认的五件事每个模型的 context 数按各自 QPS 定不要一刀切—— 小模型的 context 可能比大模型还贵本文 296 MB vs 79 MB。同一 context 必须加锁或池化—— 这条是实测踩出来的我第一版服务里三个模型各一个 context并发压测时直接返回503日志是execute_async_v3 返回 False。改成每模型一个 context 池 每 context 一把锁后才稳定。显存预算按max profile算不按参数量算—— 公式基线 Σ(engine context × N)。淘汰策略要验证重载成本—— 本文实测 12.7–19.3 ms如果你的模型更大、engine 更大一定要单独测。多模型不要指望吞吐叠加—— 总量由服务层瓶颈决定实测保留率 51%。关键代码context 池 锁classModelSlot:def__init__(self,name,spec,n_ctx1):self.n_ctxmax(1,n_ctx)self.contexts,self.locks,self._rr[],[],0defload(self):self.contexts[self.engine.create_execution_context()for_inrange(self.n_ctx)]self.locks[threading.Lock()for_inself.contexts]def_acquire(self):withself._rr_lock:iself._rr%len(self.contexts)self._rr1returnself.contexts[i],self.locks[i]definfer(self,x):ctx,lockself._acquire()withlock:# 同 context 串行不同 context 并行...# set_input_shape → execute_async_v3千万不要省略那把锁。两个线程同时用同一个IExecutionContext时execute_async_v3会返回False不是抛异常请求表现为 503而日志里只有一行错误——排查成本很高。常见问题FAQQ1多个模型应该全部预加载还是用到再加载取决于显存余量与模型数量。实测三个模型全量加载常驻 701 MB、启动 2.64 s、首请求 2.7–19.8 ms懒加载启动常驻 0 MB但首请求慢到 21–111 ms。模型少且显存够就 eager首请求稳定模型多、多数是长尾请求就 lazy省常驻显存紧张用 lru本文预算 400 MB 时常驻压到 404 MB。Q2懒加载能加快服务启动吗基本不能。实测三种策略启动耗时分别是 2.64 / 2.56 / 2.59 s差异不到 0.1 s。因为进程启动的大头是导入 torch、初始化 CUDA 上下文这部分与是否加载 engine 无关。懒加载省的是显存不是启动时间。Q3为什么参数更少的模型反而占更多显存因为 context 显存由max optimization profile 下的激活张量体积决定与参数量无关。实测tiny_cnn0.09M 参数每 context 296 MB而resnet1811.7M 参数只要 79 MB——前者在高分辨率112×112、128 通道保留了大量激活后者在浅层就把分辨率降下去了。经验公式context ≈ 3 × max_batch × 峰值激活体积FP16。Q4想省 context 显存最有效的手段是什么缩小 max profile而不是换小模型。实测同一个tiny_cnnmax_batch从 32 降到 8context 从 296 MB 降到 76 MB省 220 MB输入从 224 降到 112context 从 296 MB 降到 88 MB。模型本身一行代码都不用改只要在create_optimization_profile里把 min/opt/max 调小。Q5LRU 淘汰之后显存真的会还给 GPU 吗会。实测销毁 context 时归还了几乎全部占用tiny_cnn归还 296 MB、mid_cnn归还 296 MB、resnet18归还 76 MB销毁 engine 再把剩下的 2–24 MB 归还。每个模型会残留约 12 MBCUDA 模块的驱动侧缓存。所以 LRU 在原理上可行不会出现越淘汰越少的情况。Q6LRU 淘汰会不会导致延迟抖动有但比想象的小。实测首次冷启动约 111 ms含首次 CUDA 模块加载而 LRU 淘汰后的重载只要 12.7–19.3 ms第 2–4 轮请求延迟稳定在 15.7–27.9 ms。原因是 CUDA 模块首次加载后常驻驱动后续重载只需重新分配显存。能接受 20 ms 级别抖动就可以用 LRU。Q7挂两个模型能让吞吐翻倍吗不能。实测单独压tiny_cnn247.6 QPS、单独压resnet18259.9 QPS合计 507.5而两个同时压合计只有 260.5 QPS保留率 51.3%。总吞吐由单进程 HTTP/Python 层的瓶颈决定与模型数量无关多模型只是把固定产能分配给不同模型。要提升总量需要换服务层Triton/C、加进程或做 batching。Q8多模型服务里并发请求同一个模型时报 503 是什么原因大概率是多个线程共用了同一个IExecutionContext。TensorRT 不保证 context 线程安全并发执行时execute_async_v3会返回False而不是抛异常服务若把它当成功处理就会返回错误或脏数据。正确做法是给每个 context 配一把锁串行复用或按预期并发数开一个 context 池。小结三种策略只能三选二eager首请求快、常驻高lazy省常驻、首请求慢lru压常驻、有 12.7–19.3 ms 重载抖动启动耗时三者几乎相同2.56–2.64 s。context 显存看激活体积不看参数量0.09M 参数的tiny_cnn每 context 296 MB11.7M 参数的resnet18只要 79 MB经验公式context ≈ 3 × max_batch × 峰值激活。省显存先缩 max profilemax_batch32→8 省 220 MB比换小模型有效得多且不用改模型代码。LRU 可行显存真会归还删 context 归还 296 MB、删 engine 再还 2–24 MB残留约 12 MB首次冷启动 111 ms、重载仅 12.7–19.3 ms。吞吐不叠加单独合计 507.5 QPS同时压只有 260.5 QPS保留率 51%多模型是切蛋糕要做大蛋糕得动服务层。下一篇我们聊精度与速度的取舍FP16 已经用过接下来是 INT8 量化——校准集怎么准备、IInt8Calibrator怎么写、INT8 到底能再快多少、精度掉多少以及什么情况下不该用 INT8。
延伸阅读

更多相关文章

2026/10/23 6:39:08

DeepInfer 昇腾适配与计算架构分析

完整交互版:https://besproma.github.io/deepinfer-ascend-analysis/ 代码快照:774ce24 分析日期:2026-10-21 分析范围:并行调度、模型级优化、昇腾适配、芯片内数据流、源码证据与公开性能数据 DeepInfer 是 DeepSeek 面向 DeepS…

2026/10/23 6:34:08

MySQL 8.0 联合索引只命中前两列,EXPLAIN 却显示全表扫描

本文摘要:联合索引满足左前缀却走全表扫描,慢查询排查经常被误判为索引失效。以 MySQL 8.0 成本模型与 EXPLAIN 输出为依据,区分优化器正确决策和统计信息过期两类根因。 一、问题与结论 MySQL 8.0.28 环境,orders 表建有联合索引…

2026/10/23 10:45:12

credui.dll丢失怎么修复?系统文件恢复与DLL报错排查全攻略

今天在帮朋友处理一台电脑时,又看到那个熟悉的弹窗:打开某个软件,屏幕中间窜出一句“无法启动此程序,因为计算机中丢失credui.dll”,点“确定”之后,软件要么直接没反应,要么勉强能进但功能残缺…

2026/10/23 10:45:12

GnConv注入YOLO26:高阶空间交互提升小目标检测实践

1. 从一次模型迭代说起:为什么我会盯上GnConv 做目标检测的都知道,YOLO系列走到今天,骨干网络和颈部结构的成熟度已经非常高,常规的卷积替换、注意力插入、特征融合优化,能榨的汁基本都榨得差不多了。我今年在某个项目…

2026/10/23 10:45:12

ChatGPT提示词四模式:K12教师即学即用的Prompt工程指南

简介:本资源是一份面向K12教育工作者、AI初学者及自然语言处理实践者的ChatGPT高效交互指南,聚焦Prompt工程核心方法论,解决“如何精准提问、结构化指令、角色化引导以获得高质量输出”这一关键问题。PDF文档系统梳理了四大实用模式&#xff…

2026/10/23 10:45:12

IEC 61508-1功能安全母标准解读:安全生命周期与合规实践

简介:IEC 61508-1:2010《电气/电子/可编程电子安全相关系统的功能安全 第1部分:一般要求》是功能安全领域的基石性国际标准,面向汽车电子、工业控制、轨道交通等行业的系统工程师、安全评估人员及认证从业者。该标准为E/E/PE安全相关系统的设…

2026/10/23 10:45:12

大模型参数高效微调(PEFT)实战指南:从LoRA到QLoRA

大模型这波浪潮里,预训练是在“烧钱买地基”,全量微调则是“拿整栋楼重新装修”。几年前我们拿到一个开源底座模型,第一反应就是全量参数丢上去跑几个 epoch,效果不够就加数据加 GPU。现在这个玩法越来越奢侈:动辄几十…

2026/10/23 10:40:09

OPcache配置陷阱:代码更新不生效?

1. 引言很多朋友在部署 PHP 项目时都遇到过这样一个诡异的现象:明明把代码改好了,上传到服务器,刷新页面却还是老样子。清浏览器缓存没用,重启 Nginx 也没用,折腾半天才发现问题出在 OPcache 上。今天这篇文章就结合我…

2026/10/22 11:29:28

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/23 0:02:26

Verilog实现FPGA密码锁:矩阵键盘与状态机设计实战

说实话,这几年接触了不少想入门FPGA的朋友,如果你问哪个项目能一口气把Verilog的核心基本功串起来,我通常会推荐电子密码锁。这个项目看着不起眼,却把矩阵键盘扫描、按键消抖、状态机设计、动态显示刷新、外设控制这些数字逻辑里的…

2026/10/23 0:02:26

数据服务监控运维实战:从指标体系到告警治理与故障排查

周末凌晨两点,手机震了。第一反应是“数据服务挂了”。打开电脑看了下,果然,某张核心报表的数据产出时间已经超过SLA两小时,下游三个业务方连着发来问询。这种场景,做过大数据运维的人都不会陌生。数据服务的监控与运维…

2026/10/23 0:02:26

FreeRTOS任务通知实战详解:内核原理、API与避坑指南

裸机编程切到RTOS之后,任务之间的通信和同步永远是躲不开的坎。前面聊过队列、信号量、互斥锁、事件组,这次把FreeRTOS任务通知(Task Notification)单独拎出来讲透。原因很简单:任务通知是FreeRTOS里最容易用错、也最容…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑