【Bug已解决】[DirectML] System Crash and Driver Corruption on AMD RX 580 during OCR Inference 解决方案
一、现象长什么样
在 Windows 上用 ONNX Runtime 的DirectML EP(走 DirectX 12 / 微软的 ML 抽象层)跑 OCR(光学字符识别)模型时,在AMD RX 580这块卡上,推理过程中系统直接崩溃(蓝屏/驱动重置/TDR),甚至伴随“驱动损坏”(需要重装驱动)。其他卡(NVIDIA、AMD 新卡)正常。现象:
# 现象 A:推理到某一步系统崩 # 跑 OCR 模型(含大量小卷积 / 逐元素)时,RX 580 触发 TDR # (Timeout Detection and Recovery:GPU 任务超时,驱动重置) # 现象 B:驱动损坏 # 多次崩溃后,DirectML 设备创建失败,提示驱动异常,需重装 AMD 驱动 # 现象 C:只在 RX 580 + DirectML + OCR 触发 # 同模型在 NVIDIA 正常;RX 580 跑其他模型(非 OCR)也正常; # 只有 OCR 这种特定算子组合在 RX 580 的 DML 路径上炸最坑的是现象 A/B:不是进程崩,是系统级崩 + 驱动损坏,比普通 segfault 严重得多,且只在特定老卡 + 特定模型组合出现,极难在 CI 覆盖(CI 没有 RX 580)。
二、背景
DirectML EP 把 ONNX 算子翻译成 DirectML 的算子(DML 的IDMLOperator),再经 DirectX 12 在 GPU 上执行。不同 GPU 厂商(AMD/NVIDIA/Intel)的 DML 后端实现有差异。
OCR 模型的特征是:大量小尺寸、高 channel 数的卷积/逐元素算子连续执行。在 AMD RX 580(较老的 GCN 架构)的 DML 路径上,某些算子组合(比如连续多个 depthwise conv + 逐元素,或特定 tensor 维度的 batched 算子)会触发 AMD 驱动的 bug:要么单算子执行时间异常长导致 TDR(现象 A),要么 DML 内部状态写到越界显存导致驱动损坏(现象 B)。
根因通常在 ORT 侧有两个层面:① ORT 给 DML 的某个算子传了 RX 580 不支持的 tensor 布局/参数(DML 规范里这种情况应回退,但 ORT 没回退直接下发,触发驱动 bug);② 某个 DML 算子被“融合”成了 RX 580 驱动处理不好的大算子,单步超时。
这是 EP/驱动兼容性审查里典型的坑:某 EP 在特定老硬件上因为下发了该硬件驱动处理不好的算子/布局,触发系统级崩溃,且缺少该硬件的回归覆盖。
三、根因
未对 RX 580 不支持的算子/布局做回退:ORT 给 DML 下了 RX 580 驱动有 bug 的组合,没走“不支持就拆成小算子/回退 CPU”的安全路径(现象 A)。
融合出大算子导致 TDR:连续小算子被融合成一个超大 DML 算子,RX 580 单步执行超时触发 TDR(现象 A)。
缺少特定硬件回归:CI 没有 RX 580 环境,驱动级崩溃从未被测试发现(现象 C)。
本质:是DirectML EP 在特定老硬件(RX 580)上下发了驱动处理不好的算子/融合,未做安全回退,且缺该硬件回归,导致系统崩溃/驱动损坏。
四、最小可运行复现
下面用 Python 模拟“某硬件不支持的算子组合未被回退,直接下发导致‘崩溃’”:
class GpuModel: def __init__(self, supported_ops): self.supported = supported_ops # 该卡 DML 能安全跑的算子集 def schedule_buggy(model, op_graph): """buggy: 不检查硬件支持,直接融合下发。""" for op in op_graph: if op not in model.supported: # 直接下发到不支持的硬件 -> 驱动崩 return "CRASH: unsupported op on this GPU" return "ok" def schedule_fixed(model, op_graph): """fixed: 不支持的算子拆小/回退 CPU。""" for op in op_graph: if op not in model.supported: # 安全:回退成小算子或 CPU,绝不直接下发 return f"fallback:{op}" return "ok" rx580 = GpuModel(supported={"conv", "add"}) # 不支持 fused_big ocr_graph = ["conv", "add", "fused_big"] # OCR 含融合大算子 print(schedule_buggy(rx580, ocr_graph)) # CRASH print(schedule_fixed(rx580, ocr_graph)) # fallback:fused_bigbuggy直接下发崩溃,fixed回退安全。
五、解决方案(第一层:最小直接修复)
最小修复:DirectML EP 在调度前检查该 GPU 是否支持某算子/融合,不支持就拆成基础算子或回退 CPU,绝不直接下发到会崩的硬件:
// 修正:下发前检查硬件能力,不支持则拆/回退 bool DmlOperatorDesc::IsSupportedOn(const GpuCaps& caps) const { if (op_type_ == DML_OPERATOR_FUSED_BIG && !caps.supports_fused_big) { return false; // 让调度器拆成小算子或回退 CPU } return true; } // 调度时:不支持的算子不融合、不直发 if (!desc.IsSupportedOn(rx580_caps)) { DispatchAsSmallOps(desc); // 拆小,避免单步超时 TDR }这一层改动最小:加硬件能力检查 + 回退,系统崩溃消失。但依赖“每类算子都登记能力”,下看第二层。
六、解决方案(第二层:结构性改进)
把“DirectML 算子在 Specific GPU 上的能力检查 + 不支持即安全回退”固化成单一事实来源。下面这个 dataclass 集中管理:
from dataclasses import dataclass, field from typing import Dict, Set @dataclass class OrtDirectMlCrashPolicy: """单一事实来源:DirectML 算子在 GPU 上的能力与安全回退契约。""" # gpu -> 不支持的算子集(会触发驱动崩溃的组合) _unsupported: Dict[str, Set[str]] = field(default_factory=lambda: { "AMD_RX580": {"fused_big", "depthwise_conv_batched"}, }) def safe_schedule(self, gpu: str, op_graph: list) -> list: plan = [] for op in op_graph: if op in self._unsupported.get(gpu, set()): # 不直接下发,拆小/回退 plan.append(f"fallback:{op}") else: plan.append(op) return plan def assert_no_crashing_op(self, gpu: str, op_graph: list) -> None: bad = set(op_graph) & self._unsupported.get(gpu, set()) if bad: raise AssertionError(f"would dispatch crashing ops {bad} on {gpu}")这一层的关键收益:
- 能力黑名单:
AMD_RX580不支持的算子集中登记,一目了然; - 安全回退:
safe_schedule对不支持的算子拆/回退,绝不直发; - 断言:
assert_no_crashing_op防把崩溃算子下发到该硬件; - 单一事实来源:所有 DirectML 硬件兼容约定收口在
OrtDirectMlCrashPolicy。
七、解决方案(第三层:断言 / CI 守护)
把第二层钉成 pytest,挂进 CI,覆盖硬件兼容:
import pytest from your_package.ort_directml_crash import OrtDirectMlCrashPolicy def test_rx580_unsupported_falls_back(): # 断言 1:RX580 不支持的算子被安全回退,不直发 p = OrtDirectMlCrashPolicy() plan = p.safe_schedule("AMD_RX580", ["conv", "add", "fused_big"]) assert "fallback:fused_big" in plan def test_no_crashing_op_on_rx580(): # 断言 2:调度前断言不会下发崩溃算子 p = OrtDirectMlCrashPolicy() with pytest.raises(AssertionError): p.assert_no_crashing_op("AMD_RX580", ["fused_big"]) def test_other_gpu_ok(): # 断言 3:其他 GPU(无黑名单)正常下发 p = OrtDirectMlCrashPolicy() plan = p.safe_schedule("NVIDIA_RTX4090", ["conv", "add", "fused_big"]) assert plan == ["conv", "add", "fused_big"] def test_ocr_graph_safe_on_rx580(): # 断言 4:OCR 图在 RX580 上整体安全(无崩溃算子直发) p = OrtDirectMlCrashPolicy() ocr = ["conv", "add", "fused_big", "depthwise_conv_batched", "softmax"] plan = p.safe_schedule("AMD_RX580", ocr) assert all(not o.startswith(("fused_big", "depthwise_conv_batched")) or o.startswith("fallback:") for o in plan)四条断言从“RX580 回退”“崩溃算子被抓”“其他 GPU 正常”“OCR 图安全”四面把系统崩溃钉死在 CI。
八、排查清单
DirectML EP 在 RX 580 跑 OCR 系统崩溃/驱动损坏时:
- 是否 TDR/驱动重置?确认是 GPU 单步超时或驱动 bug(现象 A/B)。
- 是否只在特定老卡 + 特定模型组合触发?是就查该卡 DML 不支持的算子/融合被直发(现象 C)。
- 不支持的算子是否做了回退/拆小?没做就直接下发触发驱动崩。
- 用第二层
OrtDirectMlCrashPolicy:硬件能力黑名单 + 安全回退 + 断言防直发。 - 加第三层 pytest,断言“RX580 回退、崩溃算子被抓、其他 GPU 正常、OCR 图安全”。
- 特定老硬件的驱动级崩溃,EP 必须对该硬件不支持的算子做安全回退,绝不能直发。
九、小结
DirectML EP 在 AMD RX 580 跑 OCR 系统崩溃/驱动损坏,本质是ORT 把 RX 580 驱动处理不好的算子/融合(大算子单步超时)直接下发给 DML,没做“不支持就拆小/回退”的安全路径,导致 TDR 甚至驱动损坏;且 CI 无 RX 580 回归。修复分三层——第一层调度前检查硬件能力、不支持即拆/回退;第二层用OrtDirectMlCrashPolicy这个 dataclass 把“硬件能力黑名单 + 安全回退 + 断言防直发”收口成单一事实来源;第三层用四条 pytest 把“RX580 回退、崩溃算子被抓、其他 GPU 正常、OCR 图安全”钉死在 CI。核心心法:EP 在特定老硬件上必须把该硬件驱动处理不好的算子做安全回退(拆小/回退 CPU),绝不能直发,否则会触发系统级崩溃与驱动损坏。