已关闭
[RFC]: 【重构】对多分支归一compat层做代码重构,完善校验机制 #4978
li_jing_hw创建于 9月21日关闭于 18 天前
9月21日 添加了label:rfc
9月21日 添加了label:triage-review
TorchNPU-Bot
9月21日 评论:
9月21日 评论:
issue待分派,添加triage-review标签


9月21日 修改了issue 的描述
9月21日 修改了issue 的描述
9月21日 关联了pull request:Refactor compat impl for strong validation instead of comment
9月21日 添加了label:bot-triaged;删除了label:triage-review
TorchNPU-Bot
9月21日 评论:
9月21日 评论:
检测到当前 issue 已关联 PR,自动添加标签:bot-triaged


li_jing_hw
9月23日 评论:
9月23日 评论:
1.key和符号一致,自动导出符号,需要权衡代码可读性。
2.__all__约束导出的符号。


9月24日 关联了里程碑:v26.2.0
19 天前 移除了里程碑
chenrayray
18 天前 评论:
18 天前 评论:
因为本 RFC 的实现 !46750 已于 2026-10-09 合入 master(e75e3d4285),兼容层登记、统一兑现与校验重构已进入官方代码,所以按实现已合入先关闭。
如仍有问题或仍需推进,请重新打开,并补充当前 torch/torch_npu/CANN 版本、受影响分支、最小复现或具体未完成项及最新日志,便于继续处理。


18 天前 关闭了 issue
18 天前 issue状态由 TODO 改变为 DONE
18 天前 添加了label:resolved
状态(Status): Draft
作者(Authors): <待填>
创建日期(Created): 2026-09-21
更新日期(Updated): 2026-09-21
相关 Issue/PR: <待填>
1. 概述
1.1 简介
本提案重构
torch_npu/_compat/(多版本兼容适配层)的组织方式:把当前散落在各文件里的版本判断,改成"装饰器登记 + 文件末尾统一兑现"的统一结构。
一个兼容点由三部分组成:一个
key、若干"侧"(每侧声明自己的命中条件)、以及模块末尾的一行兑现语句。装饰器只登记、不执行;模块末尾按登记顺序选中当前版本命中的那
一侧,
resolve()只挑不调(把生效时机留给消费方),apply()选中并当场调用。核心价值是:版本阈值、上游依据、何时可删这些"事实"只有一个来源,并且由代码而不
是注释承载——写错在
import torch_npu时就会失败,而不是等到某个版本上补丁静默失效。
1.2 动机
torch_npu用一份代码同时支持多个上游 PyTorch 版本(当前下限见version.py的MIN_SUPPORTED_VERSION),版本差异全部隔离在_compat/里。重构前这里有 13 个兼容点、三种写法并存(函数体内的早期返回、模块级
if/else绑名字、别名透传),痛点是:
COMPAT(>= 2.14)注释、if CURRENT_VERSION >= (2, 14)代码、以及CAN REMOVE when MIN_SUPPORTED >= (2, 14)的删除提示。三者靠人保持一致,改一处漏一处不会报错。
< 2.14与>= 2.14差一,仓库自己的检查工具也明确拒绝>/<=这两种操作符,说明这是公认的坑。lt=(2, 15)写成lt=(2, 16),会出现两个版本区间同时命中或留空档,运行期不会有任何提示,表现为"补丁静默不生效"或"在新版本上错误生效
"。
torch_npu是跨团队协作、开源贡献的生态,贡献者通常照着最近的代码抄。三种写法并存时,"最近的代码"不一定是正确样本;而注释里的约
定(阈值、
CAN REMOVE)在多人协作下退化很快。不做本提案的后果:随着上游 2.14 / 2.15 持续变动,兼容点会继续增加,上述问题按点
数放大;而且 review 一个兼容点 PR 时,maintainer 需要人工核对"阈值是否互补、方向
是否写反、注释是否与代码一致",这类表单式检查会持续消耗 review 精力。
1.3 目标
目标
数;注释只写"上游改了什么、为什么需要这一侧"。
_compat下所有版本差异都由同一种结构表达——一个key+ 若干执行函数 + 文件末尾一行兑现。
码(
from torch_npu._compat.xxx import yyy的消费方)零改动。非目标
torch_npu/csrc/_compat/,C++ 侧继续用TORCH_NPU_VERSION_GE编译期分支。
tools/compat_check/version_compat_check.py(它目前对本目录已静默失效,见§7)。
2. 用例分析
兼容层只有一类使用者——业务模块通过
from torch_npu._compat.xxx import yyy取统一接口。按"这个点在当前版本下要不要留一个可调用的东西给外部",现有 13 个兼容点分
成三类,这三类对应三种使用场景,也决定了各自用
resolve()还是apply():import后调用resolve():只挑不调,由外部调用patch_*、2 个 dynamo 入口、device_to_aten、get_default_generator、_mm_like_strategy(共 9 个)apply():选中并当场调用npu()补丁、ProcessGroup 的 sequence number 补丁(共 2 个)apply(),执行函数return出要绑定的值_new_process_group_helper、_add_ephemeral_timeout_for_all_pgs(共 2 个)对 DFX 的要求直接来自这三类场景:
入期就被拦下,不能留到运行期。
推出来。
3. 方案设计
3.1 总体方案
一个兼容点的构成
一个兼容点 = 一个
key+ 若干"侧",每侧声明自己的命中条件;模块末尾统一兑现。# 1) 一个 key 下写两"侧",各自声明命中条件 @compat_impl(key="patch_xxx", ge=(2, 16)) # CURRENT_VERSION >= (2, 16) def patch_xxx_upstream(): return # 上游已具备,什么都不用做 @compat_impl(key="patch_xxx", lt=(2, 16)) # CURRENT_VERSION < (2, 16) def patch_xxx_local(): ... # 老版本要打的补丁(body 原样搬进来) # 2) 文件末尾兑现 patch_xxx = compat_impl_container["patch_xxx"].resolve() # 3) 消费方照旧 import 这个 key 并调用,不需要改三条基本约定:装饰器只登记(import 期没有副作用,且返回原函数);文件末尾
才兑现 (全模块只做一次选择);执行函数的函数体就是"这一侧要做的事"(原样
搬过来,不需要再包一层内层函数)。
兼容点的标识:
keykey有三个作用:一是分组,所有校验按key分组,同一个 key 的若干侧才构成一个兼容点;二是查表(
compat_impl_container[key]);三是命名约定,key应当等于该点对外绑定的名字,也就是文件末尾那行左边。现有 13 个点里 11 个的
key就是导出名;2 个一类点没有对外名字,取一个能说明它在干什么的私有名。
key打错或撞名会让校验失效——两个单侧点撞名会静默合并,这是唯一靠机制发现不了的错。
注册表结构(类图)
classDiagram direction TB class compat_impl { <<decorator>> +compat_impl(key, ge, lt, judgement) } class _Container { -_points : dict~str, CompatPoint~ +__getitem__(key) CompatPoint +point(key, module) CompatPoint +keys() list -_reset_for_test() None } class CompatPoint { +key : str +module : str -_sides : list~_Side~ -_add(side) None -_check_versioned(side) None -_pick() _Side +resolve() Any +apply() Any +describe_all() list } class _Side { +key : str +module : str +fn : Callable +op : str +version : tuple +matched : bool +matches() bool +describe() str } class CompatError { <<exception>> } compat_impl ..> _Side : 构造 compat_impl ..> _Container : point(key, module) _Container "1" *-- "0..*" CompatPoint : _points[key] CompatPoint "1" *-- "1..2" _Side : _sides(登记顺序) CompatPoint ..> CompatError : 校验失败时抛出 _Container ..> CompatError : 未注册的 key note for _Side "op=ge/lt 用 version;<br/>op=judgement 用 matched(登记期已求值)" note for CompatPoint "resolve() 只选不调(二类)<br/>apply() 选中并零参调用(一类/混合)"_Container:全局单例,按key索引,只负责"取或建CompatPoint"。CompatPoint:一个key的全部侧,负责校验(§3.3)与选择。_Side:一侧的全部数据(fn+ 命中条件),自己知道怎么判断命中。注册表不做调度(没有执行顺序表)、不做自动发现(不扫描目录、不 import 业务模
块),
_compat/__init__.py保持空文件。时序:登记 → 兑现 → 调用
sequenceDiagram autonumber participant T as torch_npu 启动链路 participant M as _compat.某模块 participant D as compat_impl participant C as 注册表 participant S as _Side participant U as 消费方 rect rgb(245, 245, 245) Note over T,D: ① 登记 —— 装饰器只登记,不执行 T->>M: import(谁先用到谁先触发) M->>D: @compat_impl(key="x", lt=(2, 15)) D->>D: _validate → 归一为 (op, version, matched) D->>C: point(key, module)._add(_Side(...)) C->>S: 校验:跨模块?混用?方向重复?阈值互补?过期? D-->>M: return fn(名字仍可用) end rect rgb(245, 245, 245) Note over M,C: ② 兑现 —— 模块末尾按登记顺序选中一侧 M->>C: container["key"] C->>S: 逐个 matches() S-->>C: 第一个命中的侧 alt 二类:resolve() 只选不调 C-->>M: side.fn(不调用) else 一类 / 混合:apply() 选中并零参调用 C->>S: side.fn() S-->>C: 返回值 = 该 key 要绑定的值 C-->>M: 返回值 end end rect rgb(245, 245, 245) Note over M,U: ③ 调用 —— 补丁类的生效时机由消费方决定 M->>U: 模块属性就绪(补丁类此刻还没执行) U->>M: 调用 —— 补丁类真正生效 end两条要记住的:装饰器只登记,import 期不会有副作用;二类补丁的生效时机由消
费方决定 (
resolve()不调用它),这是保住"延迟执行"效果的关键——例如 inductor的补丁不能在 import 期生效,否则只为 triton 路径准备的补丁会波及 ascendc 后端。
3.2 技术选型
放弃:把实现包进内层函数再返回(所有点统一成"零参工厂")
这样能让所有执行函数形状一致,但需要把大块实现整体缩进一层。现有最大的两份实现是
325 行和 116 行,纯缩进的 diff 无法有效 review,且回归风险不低。最终只对确实需要
"造函数"的点(一类/混合)使用零参工厂,其余保持平铺。
放弃:把注册表做成调度器
仓库里已有
PatchManager(按 group 顺序统一 apply)的先例,但_compat的生效时机由import 关系决定(谁先用到谁先触发),且存在跨块顺序依赖,再造一套调度会变
成"两个调度器互相影响"。注册表只做"登记 + 选择"。
3.3 功能与性能设计
命中条件三选一
@compat_impl(key="patch_xxx", ge=(2, 16)) # CURRENT_VERSION >= (2, 16) @compat_impl(key="patch_xxx", lt=(2, 16)) # CURRENT_VERSION < (2, 16) @compat_impl(key="some_point", judgement=<bool 表达式>) # 逃生门ge/lt两种操作符:>与<=和>=/<差一,是最容易翻车的地方,所以从 API 层面就不提供。
key里不能混用ge/lt和judgement,混了无法判断互补性。judgement是逃生门,只给"同一个 minor 内部还有分叉"的点用(例如上游改动落在 2.14 中段的 nightly,而
CURRENT_VERSION只保留 major.minor)。写法是把条件提成模块级变量、两侧用
not互补;版本判断必须写在前面,靠短路保证旧版本不去跑
inspect.getsource()。使用judgement的点豁免全部阈值校验,因此有配额(当前 4 处)。
判定逻辑就是这三种条件的直接实现:
flowchart TD A["_Side.matches()"] --> B{"op == judgement ?"} B -- 是 --> C["返回 matched(登记期已求值)"] B -- 否 --> D{"op == ge ?"} D -- 是 --> E["CURRENT_VERSION >= version"] D -- 否 --> F["CURRENT_VERSION < version"]兑现语义
# 二类:只选择,不执行 patch_xxx = compat_impl_container["patch_xxx"].resolve() # 一类 / 混合:选择并当场落实 compat_impl_container["sharded_tensor_npu_method"].apply() _new_process_group_helper = compat_impl_container["_new_process_group_helper"].apply()resolve():按登记顺序返回第一个命中的函数对象(不调用)。没有一侧命中时抛错。
apply():零参调用命中的执行函数,返回值即该 key 要绑定的值。没有一侧命中时什么都不做。
校验规则(注册期,发现问题直接抛
CompatError,import torch_npu即失败)key非空;ge/lt/judgement恰好一个(int, int)(2, "15")<= MIN_SUPPORTED_VERSIONge一个lt、阈值相同ge/lt与judgementjudgement侧最多一侧命中解析期:
resolve()没有命中侧时报错(二类的名字必须有值);apply()没有命中侧时静默(一类允许"这个版本不需要我")。
性能影响
if CURRENT_VERSION >= (2, 15): return,现在由resolve()在导入期选定,调用时不再比较版本。
3.4 安全隐私与DFX设计
安全隐私:本提案只调整版本差异的组织形式,不新增数据通路、不处理用户数据、不
改变任何对外接口的可见性,因此不涉及新的安全与隐私面。
兼容性
对业务代码完全兼容:所有
from torch_npu._compat.xxx import yyy的消费方零改动,导出名字与签名不变。
已知的行为变化(已逐条核对):
_mm_like_strategy、_new_process_group_helper两侧原本在模块导入时执行的 import,改为首次调用时执行distributed.py的 5 个点从"文件中间各自的位置"移到"文件末尾的兑现行";顺序仍等于书写顺序,语义不变_mm_like_strategy的本地实现从if块里抬到模块级(8 格 → 4 格,33 行)<key>_local/<key>_upstream,对__all__无影响CURRENT_VERSION引用收敛judgement逃生门test/test_torch_npu_init.py::test_07断言了 shim 的模块属性,需同步调整(该断言在torch >= 2.14分支上仍然成立)破坏性变更:无。对外名字、签名、生效时机与重构前一致。
可维护性
处)共享同一个
key,改阈值只改一处。<= MIN_SUPPORTED_VERSION的侧在导入期直接报错,等于一份自动生成的待办清单,不依赖工具或记忆。
可测试性
test/test_compat_registry.py(20 条)只需能import torch,版本分支通过 monkeypatchCURRENT_VERSION覆盖。",产出可diff 的指纹,用于重构前后对比。
3.5 编程与调用设计
3.5.1 编程模型基本设计
开发环境:
torch_npu源码树 +bash ci/build.sh --python=<ver> --torch=<ver>编译出wheel,pip install dist/torch_npu-*.whl后即可验证。改_compat属于纯 Python 改动,不涉及 C++ 编译。
开发约束(写新兼容点时必须遵守,也是 review 的检查点):
resolve()按这个顺序取第一个命中项;不要按阈值或字母重排。
judgement在装饰器那一行求值,探测要读到未打补丁的上游)。
distributed.py里 ProcessGroup 补丁依赖_new_process_group_helper先执行)。torch_npu启动流程里统一调用(
from X import Y取的是那一刻的值,不是活引用)。一类/混合。
apply()零参调用执行函数,所以一类/混合的执行函数不能带业务参数。key必须与文件末尾绑定行的左边一致。可验收设计:见 §4 测试设计。
3.5.2 接口定义与设计
3.5.2.1
compat_impl(装饰器)compat_impl(*, key, ge=None, lt=None, judgement=None)→ decoratorkeystrgetuple[int, int]CURRENT_VERSION >= ge时命中(major, minor),须> MIN_SUPPORTED_VERSIONlttuple[int, int]CURRENT_VERSION < lt时命中judgementboolTrue/False,且与ge/lt互斥ge/lt/judgement三者必须恰好给一个。decoratorCallableCompatError,import torch_npu直接失败。
key最多两侧(一个ge、一个lt,阈值相同);所有侧必须来自同一模块、且不混用
ge/lt与judgement。if CURRENT_VERSION >= (X, Y)写法。@compat_impl(key="patch_xxx", ge=(2, 16)) # 新版本侧 def patch_xxx_upstream(): return @compat_impl(key="patch_xxx", lt=(2, 16)) # 老版本侧 def patch_xxx_local(): ...3.5.2.2
compat_impl_container[key].resolve()费方在自己的时机调用。
compat_impl_container[key].resolve()→CallablekeystrCallablekey未注册或当前版本没有任何一侧命中时抛CompatError。_compat模块体内、文件末尾。if/else绑定名字"。patch_cache_base_get_system = compat_impl_container["patch_cache_base_get_system"].resolve()3.5.2.3
compat_impl_container[key].apply()key 要绑定的值。
compat_impl_container[key].apply()→AnykeystrAnyNone(不绑定名字)None(一类允许"这个版本不需要我")。
return出要绑定的值。compat_impl_container["sharded_tensor_npu_method"].apply() _new_process_group_helper = compat_impl_container["_new_process_group_helper"].apply()3.5.3 编程手册设计
贡献者需要的不是完整设计文档,而是"照着抄 + 别踩坑"。因此在
torch_npu/_compat/下随代码放一份
README.md,与代码同目录、改动时顺手可见。内容包含:resolve()还是apply())key的约定MIN_SUPPORTED_VERSION时怎么删改(含三处版本号与四类点的删法)该文档为独立输出,不并入现有用户手册;后续如有对外发布需求,可作为内嵌章节的底
稿。
4. 测试设计
单元测试(不需要 NPU 设备)
test/test_compat_registry.py,20 条,覆盖注册表的全部行为:ge侧在>=时选中、lt侧在<时选中;每个版本恰好命中一侧CompatErrorCompatErrorjudgement命中 / 重叠 / 与ge/lt混用CompatErrorresolve未命中报错 /apply未命中静默 /apply零参调用 / 两种 thunk 形状key为空 / 装饰器返回原函数 / 未注册 keyCompatError或返回原函数版本分支通过
monkeypatch.setattr(_impl, "CURRENT_VERSION", (2, 14))覆盖。集成 / 回归测试(需要 NPU 设备)
重构前后各跑一遍同一组既有用例,逐条对比通过情况:
test/test_torch_npu_init.py(14)test/distributed/test_c10d_default_backend_type.py(3)_new_process_group_helpertest/distributed/test_c10d_get_sequence_number.py(2)test/distributed/test_c10d_add_ephemeral_timeout.py(2)_add_ephemeral_timeout_for_all_pgstest/npu/test_npu.py的 accelerator generator 两项get_default_generatortest/dynamo/test_compile_trigger.py(27)test/_inductor/test_add.py/test_cat.py/test_sum.pypatch_*与device_to_aten冒烟对照
一个 23 条的观测脚本,逐点记录"命中哪一侧 + 补丁是否真的生效",输出可 diff 的指
纹;重构前后指纹应逐字一致(目前实测一致)。
环境说明
本机验证时遇到一个与本提案无关的环境问题:torch 2.14 的
ATen/ATen.h要求C++20,而 triton 的 ascend backend 硬编码
-std=c++17,导致 inductor 用例在编译阶段全部失败。用预编译
npu_utils.so塞进 triton 缓存可规避,不影响本提案的验证结论。
5. 缺点和风险
_impl.py一个文件tools/compat_check/version_compat_check.py扫的是CURRENT_VERSION >= (X, Y)字面量,迁移后对本目录静默失效6. 现有技术
(本提案未做横向调研,暂缺。)
7. 未解决问题
tools/compat_check/version_compat_check.py需要从"扫
CURRENT_VERSION >= (X, Y)字面量"改为"扫装饰器参数",并把注册期已有的规则(阈值互补、方向唯一、过期、
judgement配额、每个 key 必须有兑现行)搬进去。这对外部贡献者尤其重要:PR 门禁通常跑不了
import torch_npu(需要 torch +CANN),文本层的静态检查不可替代。
judgement逃生门配额:目前 4 处(2 个点 × 两侧),需要工具侧盯住,防止被滥用。
test/下其余用例尚未做全量前后对比。
MIN_SUPPORTED_VERSION时目前是逐条报错(一次报一个过期侧),可考虑一次性列出全部待清理项。
附录
参考资料链接
pytorch#186667(_mm_like_strategy)、#179901(NPU 默认backend)、
#187939(ShardedTensor 硬件无关)、#190138(sequence number 白名单)、
#191980(backend 通用 ephemeral timeout)、#190615(DeviceOpOverrides / DeviceInterface)、
#193904(ATen device 映射)、#193908(cache system metadata)、#192345(dynamo_dynamo_backend_init钩子)
torch_npu/_compat/README.md(随代码发布的使用说明)术语表
key和若干"侧"构成ge/lt/judgement三选一)resolve()或apply()选定当前版本该用的那一侧judgement,用于版本号判不出来的场景(如同 minor 内部的 nightly 分叉)文档更新计划
欢迎加入社区,感谢您对社区的贡献 🎉!