对 DVM 子图的每个节点查 emitter 函数,生成 k.add(...) 这样的 DVM IR 字符串
✅ DvmMlirFusion codegen 路径一定走
重要更正:点 ① 实际上 默认不启用。MFusionPatch.enable() 需 TORCHINDUCTOR_ENABLE_MFUSION=1;DvmGraphFusionPatch.enable() 只在显式 with context 才启用。DCNv2 默认配置下只有 DvmMlirFusionPatch 启用,所以 DVM_OP_REGISTRY 实际只在点 ② 和 ③ 起作用。
gen_set = set()
for fn in config.GENERATE_LIST:
gen_set.add(fn)
ifisinstance(fn, torch._ops.OpOverloadPacket):
for overload in fn.overloads():
gen_set.add(getattr(fn, overload))
# gen_set 之外的 op 全部走 fallback
DVM 适配图模式(torch.compile)流程总结
1. 角色与目录结构
DVM 适配
torch.compile涉及两个并列模块,加上一个上层 IR 框架 anir:2. 完整编译流程(按时间线)
3. DVM_OP_REGISTRY
位置:
torch_npu/_inductor/dvm/op_emitter.py:11结构:
{aten_op: (emitter_func, rule_func)}字典注册方式:用
@register_dvm_op(...)装饰器:@register_dvm_op(aten.add.Tensor, aten.add.Scalar) def add(x, y): return f"k.add({x}, {y})" @register_dvm_op(aten.mm.default, aten.bmm.default, rule=mm_rule) def matmul(x, y, trans_a, trans_b): return f"k.matmul({x}, {y}, {trans_a}, {trans_b})"装饰器参数(如
aten.add.Tensor)是 registry 的 key;函数名只是 Python 标识符,与 ATen op 名称无关(注意op_emitter.py中def select(...)其实注册的是aten.where,是命名陷阱)。rule 函数:在
op_emitter.py中定义:common_rulemm_rulewhere_rulefull_rule/cast_rule作用点(最多 3 处,但默认只生效 2 处):
dvm/graph_fusion.py:125(DvmOpSupport.is_node_supported)dvm_graph_fusion流程中判定节点能否进 DVM 子图,需先在GRAPH_FUSION_SUPPORT_OP里,再查 registry 取 rule 检查DvmGraphFusionPatch显式启用时(with DvmGraphFusionPatch():)mlir_fusion.py:295(_fallback_node_due_to_unsupported_type)fallback_node_due_to_unsupported_type时被替换为这段:查 registry 取 rule,决定是否走 fallbackDvmMlirFusionPatch模块加载时自动启用dvm/graph_build.py:138(DvmCodegenInterpreter.call_function)k.add(...)这样的 DVM IR 字符串重要更正:点 ① 实际上 默认不启用。
MFusionPatch.enable()需TORCHINDUCTOR_ENABLE_MFUSION=1;DvmGraphFusionPatch.enable()只在显式withcontext 才启用。DCNv2 默认配置下只有DvmMlirFusionPatch启用,所以 DVM_OP_REGISTRY 实际只在点 ② 和 ③ 起作用。4. GENERATE_LIST
位置:
torch_npu/_inductor/dvm/mlir_fusion.py:40(赋值),原始默认值在ascend_npu_ir/config.py:246结构:list of
aten.Xop当前 DCNv2 上的内容(部分):
anir_config.GENERATE_LIST = [ aten.mul, aten.add, aten.sub, aten.div, aten.clamp_min, aten.clamp_max, aten.maximum, aten.minimum, aten.abs, aten.reciprocal, aten.log, aten.exp, aten.pow, aten.sqrt, aten.rsqrt, aten.neg, aten.lt, aten.le, aten.gt, aten.ge, aten.eq, aten.ne, aten.where, aten.expand, aten.var_mean, aten.sum, aten.mean, aten.full, aten.relu, aten.scalar_tensor, aten.unsqueeze, aten.squeeze, # aten.reshape, ← 注释掉(启用会触发 dyn_shape 稳定性 bug) # aten.clone, prims.convert_element_type, torch.ops.npu.npu_dtype_cast, ... triton_kernel_wrapper_mutation, ]作用点(唯一):
ascend_npu_ir/ascend_npu_ir/npu/npu_lowering.py:46gen_set = set() for fn in config.GENERATE_LIST: gen_set.add(fn) if isinstance(fn, torch._ops.OpOverloadPacket): for overload in fn.overloads(): gen_set.add(getattr(fn, overload)) # gen_set 之外的 op 全部走 fallback含义:anir 层"哪些 op 走 generate 路径,哪些 fallback"的白名单。
5. 两个 list 的差异速查
dvm/op_emitter.pydvm/mlir_fusion.py:40(设置)anir/npu_lowering.py:46(读取)dict[op, (emitter, rule)]list[op]@register_dvm_op(...)装饰器两者必须配套:op 在 GENERATE_LIST 里告诉 anir "给我生成代码",在 DVM_OP_REGISTRY 里告诉 DVM "怎么生成"。任何一边缺了,节点最终都会 fallback。
6. 一个 op 如何最终决定命运(决策表)
每个 fx 节点要过四道关:
实际命运分类:
aten.unsqueeze(fp32) → 改写为aten.reshapeaten._native_batch_norm_legit_no_training→ sub/mul/sqrt/add7. fallback 机制详解
含义:当 op 不能被 inductor 编译/融合时,wrapper 代码直接生成
torch.ops.aten.X.default(...)调用,运行时通过 ATen dispatcher 走标准实现。wrapper 代码示例:
# 融合(不 fallback): dvm_fused_add_mul_0.run(buf1, buf2, buf3, stream=stream0) # fallback: buf0 = torch.ops.aten.unsqueeze.default(arg0_1, 0)fallback 路径成本:每次 ~5-10us host
reinterpret_tensor(轻量替代):当节点是 view、下游能吃 strided 输入时,inductor 在 wrapper 里直接生成:
buf6 = reinterpret_tensor(buf5, (s0, 352, 1), (352, 1, 1), 0) # ~0.1us,C++ inline比 fallback 便宜 ~50 倍。
8. DCNv2 实例:每个 op 的命运分析
8.1 进 DVM 融合块的 op
dvm_fused_BN_addmm_relu_*dvm_fused_add_mul_*aten.unsqueeze (fp32)×2dvm_fused_add_mul_*8.2 fallback 的 op(每个 ~5-10us host)
aten.unsqueeze (i64)aten.add.Tensor (i64)aten.embedding.defaultaten.permute.defaultaten.select.intaten.mm.defaultaten.cat.default8.3 走 reinterpret_tensor 的 op(启用 reshape 后)
启用
aten.reshape后,4 个 cross matmul decomposition 边界的 reshape 从 dispatch 调用变成reinterpret_tensor,host 各 ~5us → ~0.1us。9. 关键源码位置索引
dvm/op_emitter.py:11@register_dvm_op装饰器dvm/op_emitter.py:167common_ruledvm/op_emitter.py:65mm_ruledvm/op_emitter.py:106dvm/mlir_fusion.py:40ascend_npu_ir/.../npu_lowering.py:46mfusion/graph_fusion.py:924dvm/mlir_fusion.py:357_patch_lowering_type_checksdvm/mlir_fusion.py:264dvm/graph_fusion.py:122dvm/graph_build.py:133_codegen_dvm_kerneldvm/mlir_fusion.py:85_define_dvm_kerneldvm/mlir_fusion.py:112patch_decompdvm/decomp.py:15510. 调试与验证套路
看 DVM 子图边界
torch_compile_debug/*/torchinductor/*/output_code.py看 wrapper 里有哪些torch.ops.aten.*调用(fallback)和dvm_fused_*.run(...)调用(融合块)ir_post_fusion.txt看ExternKernelSchedulerNode(FallbackKernel)节点看 DVM kernel 内部 op
output_code.py里每个def dvm_fused_*_build(k):函数体即为 DVM IR 调用序列"""class <lambda>:..."""注释是该子图的 fx graph 源(lowering 后的 target)修改后必须步骤
dvm/mlir_fusion.py或dvm/op_emitter.pyfind .../torch_npu/_inductor -name '__pycache__' -exec rm -rf {} +rm -rf $TORCHINDUCTOR_CACHE_DIR验证一个 op 进 DVM 的条件
from torch_npu._inductor.dvm.op_emitter import DVM_OP_REGISTRY print(aten.X.Y in DVM_OP_REGISTRY) # 是否注册 emitter # 再查 GENERATE_LIST 是否包含 # 再看 fx graph 里实际的 target(lowering 可能改写)11. 已知瓶颈与扩展方向(按 DCNv2 优化收益排序)
dyn_shape=Truereshape 稳定性,让 GENERATE_LIST 启用 reshape 默认开