当前位置: 首页 > news >正文

triton 2026-05-13

1. “按最宽的比例 padding 成一个大 batch”

先说背景:OCR 的识别模型(rec 模型)一次能吃一文字图片(这就是 batch),一起推理比一张一张推理快得多。但有个硬性要求:这一批里的图片尺寸必须完全一样,不然没法堆成一个规整的张量喂给 GPU。

问题来了——检测阶段切出来的文字框,形状千奇百怪:

框1: "你好" → 60 × 32 (短) 框2: "今天天气真不错" → 200 × 32 (长) 框3: "Hello World" → 180 × 32 (中等长) 框4: "A" → 20 × 32 (超短)

高度一般都 resize 到 32(模型固定的),但宽度差别巨大。怎么办?

做法:找出这批里最"扁最长"的那个的宽高比,记作max_wh_ratio。然后所有图片都按这个比例 resize + padding(不够宽的右边补灰色/黑色像素)到同一个宽度。

举例,假设max_wh_ratio = 200/32 = 6.25,那大家都按 6.25 的宽高比处理,最后全部变成200 × 32

框1: "你好" → resize 到一定宽度,右边补空白,凑到 200 × 32 框2: "今天天气..." → 它本来就是最长的,直接 200 × 32 框3: "Hello..." → 同理 padding 到 200 × 32 框4: "A" → 大量空白 padding 到 200 × 32

现在 4 张图都是200 × 32,可以堆成一个[4, 3, 32, 200]的张量(4 张图、3 个颜色通道、高 32、宽 200)一次性丢给 GPU。

这就是"按最宽的那个 padding 成一个 batch"。

为什么之前会爆显存?想象一下一张图检测出来 200 个框,里面混进一个超长的(比如一整行中文),那max_wh_ratio会被拉得很大,导致所有200 个框都被 padding 成又宽又大的图,最后张量是[200, 3, 32, 800]这种怪物——一次塞进 GPU 就可能 OOM。

2. CTC 解码是什么

CTC 是识别模型输出转成文字的那一步。

识别模型(rec_model)拿到一张32 × 200的文字图,它不会直接吐出字符串,它吐出来的是一个概率矩阵。大概长这样(假设宽度方向切成 50 个时间步,字表有 6000 个字):

时间步1 时间步2 时间步3 时间步4 ... 时间步50 "你" 0.9 0.85 0.1 0.05 ... "好" 0.02 0.05 0.8 0.9 ... "天" 0.01 0.01 0.01 0.01 ... ... " "(空) 0.05 0.08 0.08 0.03 ...

意思是:在每一个横向位置(时间步),模型都猜一下"这里可能是哪个字"。

直接取概率最高的字拼起来,会得到类似:

你 你 你 _ 好 好 好 好 _ _ 天 天 气 气 _ ...

_表示 blank/空白符)

CTC 解码要做的事:把这串重复和空白清理掉,变成人看得懂的文字。规则就两条:

  1. 相邻重复的合并成一个
  2. 去掉所有空白符

所以上面那串经过 CTC 解码后就变成 “你好天气…”。

为什么要这么搞?因为模型看不懂"字和字的边界在哪",它只能每个横向小切片都猜一下,同一个字可能跨了好几个切片都被猜中。CTC 就是专门处理这种"没对齐的序列输出"的算法。

一句话:CTC 解码 = 把模型吐出来的概率矩阵翻译成人类能读的字符串。

3. “一锤子大 batch” vs “分块推理再合并”

用一个具体场景让你秒懂。

场景:一张文档图,检测出来 100 个文字框。

老做法(一锤子大 batch)

100 个框 → 全部 padding 成同一尺寸 → 堆成 [100, 3, 32, 200] 的大张量 → 一次性送进 GPU 推理 → GPU 瞬间要吃下 100 张图的算力和显存 → 得到 [100, 50, 6000] 的 logits → CTC 解码得到 100 个字符串

问题:那一下"一次性送进 GPU"时,显存要同时装下 100 张图的中间计算结果。如果框再多点、图再大点,GPU 直接撑爆,或者忙不过来超时(就是你之前那个TimeoutError)。

新做法(分块推理再合并)

rec_ort_chunk_size = 32,那 100 个框分成 4 块:32 + 32 + 32 + 4。

块1: 前32个框 → [32, 3, 32, 200] → GPU 推理 → logits_1 [32, 50, 6000] 块2: 中32个框 → [32, 3, 32, 200] → GPU 推理 → logits_2 [32, 50, 6000] 块3: 再32个框 → [32, 3, 32, 200] → GPU 推理 → logits_3 [32, 50, 6000] 块4: 最后4个 → [4, 3, 32, 200] → GPU 推理 → logits_4 [4, 50, 6000] 把 logits_1/2/3/4 在 Python 里拼起来 → [100, 50, 6000] 一次性 CTC 解码 → 100 个字符串

好处:

  • GPU 每次只需要装得下 32 张图的量,显存峰值固定、可预测。
  • 框再多也不怕,无非多跑几次,每次还是 32 张。
  • 最终结果和一锤子做法完全一样,因为 CTC 解码是在所有 logits 拼齐后一次做的,和"一次推完 100 个"效果等价。

不释放显存的原因

一句话:申请/释放 CUDA 显存很慢,所以 ONNX Runtime 用内存池复用,用完只标记空闲不还给 OS,下次直接拿来用,省掉反复跟驱动打交道的开销。 副作用就是显存占用只涨不缩,稳在历史峰值。


好,这个问题问到点子上了。我给你画一条完整的"显存超了之后到底发生什么"的链条,从最底层一路讲到你客户端看到什么。

场景设定

假设你的 GPU 有8 GB 显存,现在情况是这样:

已占用: ├── CUDA Context 0.5 GB ├── 模型权重 (det + rec) 2.0 GB ├── ONNX Runtime 内存池 3.5 GB (历史峰值涨到这儿) └── 其他 0.5 GB ───────────────────────── 合计已占: 6.5 GB 剩余可用: 1.5 GB

现在来了个大请求,rec_model 要推理一批超大的文字框,中间激活需要2.5 GB显存。

池子里空闲的不够(池子虽然有 3.5 GB 但大部分是已分配未释放的碎片,真正能复用的可能只有 0.5 GB),需要再找 CUDA 额外申请2 GB

但 GPU 只剩 1.5 GB。

显存超了,链条开始走。

链条第 1 环:CUDA 驱动层

ONNX Runtime 调cudaMalloc(2GB)找驱动要显存。

驱动一看:没这么多了。

驱动返回一个错误码:cudaErrorMemoryAllocation(也叫out of memory,错误码 2)。

关键点:CUDA 不会帮你"想办法挤一挤",没有就是没有,立刻返回失败。

链条第 2 环:ONNX Runtime 层

ONNX Runtime 拿到cudaErrorMemoryAllocation,它会做一次"挣扎":

  1. 尝试把内存池里所有标记为空闲的块还给 CUDA(这叫 shrink arena),然后再试一次cudaMalloc
  2. 如果还不够(通常确实不够,因为真正在用的块没法释放),彻底放弃。

ONNX Runtime 抛出一个异常,大概长这样:

onnxruntime::BFCArena::AllocateRawInternal Failed to allocate memory for requested buffer of size 2147483648 CUDA failure 2: out of memory

这次推理作废,没有输出。

链条第 3 环:Triton 的 ONNX backend

Triton 的onnxruntime_onnxbackend 捕获到这个异常,把它包装成 Triton 内部的推理失败:

Inference request failed: CUDA out of memory error in rec_model

**这里有个重要的事:**这次请求失败了,但 Triton服务本身不会崩。它只是把这次 infer 标记为失败,继续处理下一个请求。

但是——如果这个 OOM 发生在非常底层(比如 CUDA Context 本身损坏),有概率导致整个 CUDA 上下文进入错误状态,这种情况下所有后续推理都会失败,必须重启 Triton 进程才能恢复。这是最坏情况,不常见但存在。

链条第 4 环:BLS 调用(你这次重构特有的一环)

因为你的rec_pipeline是通过 BLS 调rec_model的,所以失败信息会按这条路回传:

rec_model 推理失败 ↓ Triton 把失败结果塞回 InferenceResponse ↓ rec_pipeline 里的 inference_request.exec() 返回一个带 error 的 response ↓ 你在 Python 代码里能通过 response.has_error() 检测到

这时候rec_pipeline有两个选择:

  • 不处理错误:异常往上冒,整个 ocr_ensemble 请求失败。
  • 处理错误:比如把 chunk_size 减半重试一次,或者跳过这个 chunk 返回部分结果。

看你的rec_pipeline代码里有没有写容错逻辑,大概率是直接失败。

链条第 5 环:ocr_ensemble 层

ocr_ensemble是个 ensemble,它串了 det + rec 流程。中间某一步失败了,整个 ensemble 的这次请求就失败,返回失败响应。

链条第 6 环:客户端(就是你那个报错的地方)

这里要分两种情况看客户端体验:

情况 A:失败响应及时回来了

客户端收到 HTTP 400 或 500,带着错误信息,类似:

tritonclient.utils.InferenceServerException:[500]CUDA out of memory errorinrec_model

这种情况你能立刻知道是 OOM。

情况 B:卡死 + 超时(你之前遇到的那个)

这个是最讨厌的。OOM 发生的瞬间,有时候会触发:

  • CUDA 内部进入半死不活状态,推理线程卡在某个同步点
  • 或者后续请求堆积在 Triton 的请求队列里,被卡住的那个请求占着槽位不释放

结果是 Triton没及时返回失败,客户端 socket 一直等响应头,最后触发你看到的:

TimeoutError: timed out

这就是为什么 OOM 有时候表现为"明确的错误信息",有时候表现为"神秘的超时"——取决于 OOM 发生时 CUDA 和 Triton 的状态有多糟。

完整链条总结

推理需要 2.5 GB,剩余 1.5 GB ↓ [CUDA 层] cudaMalloc 失败,返回 OOM 错误码 ↓ [ONNX Runtime 层] 尝试 shrink arena,还是不够,抛异常 ↓ [Triton backend 层] 这次推理标记失败;Triton 进程一般不崩 ↓ [BLS 层] rec_pipeline 收到带 error 的 response ↓ [Ensemble 层] ocr_ensemble 整个请求失败 ↓ [客户端] ├── 顺利情况:收到明确的 OOM 错误 └── 倒霉情况:请求卡住 → TimeoutError: timed out

几个关键认知

1. OOM 不会让 GPU 永久挂掉,但会让这次请求挂掉。下次请求(如果不那么大)通常还能正常跑。

2. 但 OOM 会留下"伤痕"。因为内存池可能在 OOM 过程中被搞乱、产生碎片,导致后面即使总量够,也可能因为没有连续大块而再次失败。所以你有时候会看到"一开始好好的,某次大请求 OOM 后就频繁出错"这种情况。

3. 超时不等于 OOM,但 OOM 可能表现为超时。所以你之前那个TimeoutError,OOM 是嫌疑之一,但也可能是纯粹推理慢、网络问题、队列堆积。要去看 tritonserver 日志里有没有out of memory字样才能确认。

4. 这就是为什么分块推理特别重要。重构后峰值固定在小范围内,每次cudaMalloc要的量不大,几乎不可能 OOM,整条链条根本触发不了。相当于把这颗雷从源头拆了。

建议你做的事

去 tritonserver 的日志里 grep 一下:

grep -i "out of memory\|oom\|cuda.*error\|allocation" triton.log

如果能找到,那就坐实了之前的 TimeoutError 是 OOM 引起的;找不到,那超时就是别的原因(推理慢、队列、网络),得另外查。

http://www.jsqmd.com/news/815913/

相关文章:

  • 终极指南:5步在Windows电脑上直接安装安卓APK应用
  • Learn Git Branching:提交的技巧
  • 【仅限交通行业白名单用户】ElevenLabs地铁专用语音模型v2.1泄露版:支持粤语/闽南语实时变调+站台噪声抑制(限时开放3天)
  • 5个理由告诉你:为什么Bebas Neue是设计师必备的免费商用字体
  • 横向评测:东莞AI培训主流供应商性价比
  • 上传论文给降AI工具会被拿去训练吗?嘎嘎降AI自研引擎不用你数据!
  • 宝宝钙铁锌排行榜 2025权威实测TOP10榜单揭晓 - 新闻快传
  • 不用大改原文,也能安稳通过朱雀 AI
  • STM32H7的QSPI内存映射模式实战:把W25Q64当内部Flash用(含CubeMX配置)
  • 回收加油卡的流程与技巧,新手必读! - 团团收购物卡回收
  • LoongArch架构Qt开发实战:从交叉编译到2K0300部署全流程
  • 基于LLM与无障碍服务的Android自动化助手Panda:原理、部署与应用
  • Agent开发10个常见陷阱及避免方法(血泪总结)
  • 手把手教你用Simulink搭建Buck变换器:从元件库搜索到波形分析(MATLAB 2023b)
  • 基于浪浪云轻量服务器与宝塔面板的CMS快速部署实践
  • SkillLite Channel 与 Gateway 配置完全指南:Webhook、环境变量与桌面助手
  • 信号隔离的“高速公路”:奥特AT6N137如何实现高性能隔离的极限挑战?
  • 苏州蔷薇吊装搬运:苏州搬家搬厂推荐几家 - LYL仔仔
  • 免费开源CAD软件LitCAD:面向新手的完整二维绘图指南
  • 蓝牙开发避坑指南:NRF52832的Notify属性服务,为什么你的数据发不出去?
  • 开源革命:ESP32如何重塑无人机远程识别的技术格局
  • 基于MCP协议的航空安全风险智能评估工具:架构、应用与自动化集成
  • Python电子考场结构解析:输入处理输出三环节
  • 井下防护装备佩戴检测新突破!CGALS‑YOLO 让煤矿安全监控更智能
  • WinUtil终极指南:如何用一款工具解决90%的Windows系统管理难题?
  • 小型罗茨风机厂家权威排行榜TOP1:十二年源头工厂全国发货13969110277 - 新闻快传
  • 重新定义AI自动化:Midscene.js如何重塑下一代人机交互范式
  • 商业地产和高端酒店该怎么选综合布线解决方案?
  • 从STLINK-V2到V3E:老鸟带你快速上手NUCLEO板载调试器的升级体验与MDK版本选择
  • 基于自然语言处理的本地智能助手Jarvis-v3:架构解析与实战搭建