Merged
Add engram_gate_bwd/fwd torch.compile support and torch.compile skill. #49
hanxu88Add on 16 days ago
Add engram_gate_bwd/fwd torch.compile support and torch.compile skill. #49
Merged
hanxu88Add on 16 days ago
Total: 5 files changed+651-37
@@ -0,0 +1,177 @@
1+---
2+name: ops-tilelang-torch-compile
3+description: 为本仓库 Ascend TileLang 算子接入 torch.compile 的 npugraph_ex 后端,并验证正确性及 ACL Graph 捕获、重放行为。
4+---
5+ 
6+# Ops TileLang torch.compile 接入
7+ 
8+为 `src/cann_ops_tilelang/` 中的算子实现固定形状 NPU 图执行,以独立数值校验、正确的副作用和真实捕获/重放作为完成标准。
9+ 
10+## 1. 确认环境与算子契约
11+ 
12+- 在目标仓库和分支操作,读取适用的 `AGENTS.md`,保留已有修改。检查 Python、torch、torch_npu、TileLang、CANN 和设备版本,在执行测试的 shell 中加载实际安装的 Ascend 环境脚本。
13+- 阅读目标公共接口、wrapper、kernel、reference、测试及配置;参考 `src/cann_ops_tilelang/engram/engram_gate.py`、`src/cann_ops_tilelang/config.py` 和 `tests/engram/test_engram_gate_{fwd,bwd}.py` 的现有入图实现。
14+- 明确输入限制、输出结构与元信息、可选返回、`out=`、所有写入参数、存储别名、空输入及配置依赖。保留公共签名、默认值、返回语义和原有数值标准;优先调整封装,保留 kernel 数学逻辑及调度。
15+ 
16+## 2. 封装 custom_op 与 fake
17+ 
18+将元信息校验拆为 `_validate_*`,将运行环境检查、分配、TileLang JIT 和 launch 保留在 `_impl`。公共接口在 `torch.compiler.is_compiling()` 时调用注册 op,普通执行调用 `_impl`。
19+ 
20+以下模板以单输入、单输出且真实实现用 `empty_like(x)` 分配输出的函数式算子为例;按实际接口实现 `_target_impl`、校验和 fake 分配:
21+ 
22+```python
23+import torch
24+ 
25+ 
26+@torch.library.custom_op('cann_ops_tilelang::target', mutates_args=())
27+def _compiled_target(x: torch.Tensor) -> torch.Tensor:
28+ return _target_impl(x)
29+ 
30+ 
31+@_compiled_target.register_fake
32+def _target_fake(x):
33+ _validate_target_metadata(x)
34+ return torch.empty_like(x)
35+ 
36+ 
37+def target(x):
38+ if torch.compiler.is_compiling():
39+ return _compiled_target(x)
40+ return _target_impl(x)
41+```
42+ 
43+fake 只使用元信息,返回与真实执行一致的 shape、dtype、device、布局、stride、offset 和 Tensor/None 结构;按契约分配独立存储。fake 中不读取 Tensor 数值,不执行 `.item()`、设备查询、环境文件检查、JIT 或真实 kernel。按真实输出分配规则选用 `empty_like`、`empty` 等构造方式。
44+ 
45+`custom_op` 隔离 Dynamo 追踪,但真实实现仍须满足 NPU 捕获约束。设备到主机拷贝、同步和值依赖 Python 控制流等问题应在真实执行路径定位并解决,不能仅靠封装消除。
46+ 
47+### 特殊契约
48+ 
49+| 场景 | 实现要求 |
50+| --- | --- |
51+| 可选 Tensor 返回 | 使用准确的 schema,例如 `(Tensor x, bool save_stats=True) -> (Tensor, Tensor?)`。PyTorch 2.10 对部分可选 Tensor 元组不能自动推导;核对参数、默认值及每个返回分支。 |
52+| 原地更新 | `mutates_args` 列出所有被写入参数,包括 cache、out、scale 和辅助状态;仅不写输入时使用空元组。 |
53+| 更新后返回输入别名 | 参考 PyTorch 2.10 的高层 custom_op 不允许返回输入别名。内部 op 声明 mutation、返回 `None`,fake 也返回 `None`;公共 wrapper 在 op 调用后返回原缓冲区。验证 functionalization/copy-back 后的写入与存储别名。 |
54+| `out=None` 与 `out=tensor` | 分别注册分配输出、写入缓冲区的稳定契约,由公共接口分流并保留 `out=`。复杂别名按当前注册机制实现。 |
55+| 输出依赖硬件配置 | 从正确设备获取可追踪配置,以内部标量传入 op;fake、真实分配和 launch 使用同一值,公共接口不新增参数。保留 override 和有效性检查,不硬编码核数或其它算子的分片公式。 |
56+| 输出长度依赖数据 | 使用后端支持的表示;fake 不读设备数据或虚构固定 shape。容量化、填充或接口调整必须满足原有契约。 |
57+ 
58+编译路径中的 `posix.stat` 等环境探测应移出追踪,真实执行仍保留 `require_ascend()`;优先在对应算子内适配,避免改变通用配置行为。核实剩余配置查询可追踪;`cache/lru_cache` 不保证编译常量,也不应使用 `assume_constant_result` 冻结可变配置。配置改变后验证重新特化或明确的重建图行为。
59+ 
60+显式 `*_bwd` 入图不等于注册自动 autograd,也不代表 `.backward()` 捕获或高阶梯度支持;相关能力须单独实现和验证。随机数、多流、通信和特殊设备格式须按实际后端确认捕获支持。
61+ 
62+## 3. 选择图执行上下文
63+ 
64+```python
65+import torch_npu # 注册 NPU 设备及后端
66+ 
67+compiled = torch.compile(
68+ public_op, backend='npugraph_ex', fullgraph=True, dynamic=False,
69+)
70+outputs = compiled(*args, **kwargs) # 在调用方需要的梯度或 inference 上下文中执行
71+```
72+ 
73+- 保留调用方的梯度记录语义;需要 autograd 的任务不能默认切换到 `inference_mode()`。
74+- Engram gate 在已验证的 torch 2.10.0+cpu / torch_npu 2.10.0、CANN 9.3.0、Ascend 950DT_9574 环境中,先在 inference 模式捕获 fwd、再在模式外捕获 bwd 会于 `capture_begin` 报 inference Tensor 原地更新错误。该环境下,无需 autograd 记录的 fwd 和显式 bwd 测试使用相同的外层 `torch.inference_mode()` 作为兼容策略;其它算子、版本和训练场景应分别验证。
75+- autograd reference 的正常梯度叶子、前向和 `.backward()` 在 inference 上下文外执行;不要对需要 reference 求导的整个 bwd 测试加 inference 装饰器。
76+- 一次复用验证保持输入的 shape、dtype、device、stride、storage offset、`requires_grad`、`is_inference()` 及特化标量一致。inference 上下文中新建 Tensor 可能改变属性;替换输入时保持创建方式一致。
77+- 捕获异常后启动新 Python/pytest 进程复测。`Offset increment outside graph capture encountered unexpectedly` 可能是此前捕获失败的次生错误,先定位首次失败。
78+ 
79+以下最小复现从仓库根目录在新进程运行,仅在内存中令 bwd 的 compiled 调用退出 inference 模式,不修改测试文件:
80+ 
81+```bash
82+OMP_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 python - <<'PY'
83+import torch
84+from tests.engram import test_engram_gate_fwd as fwd
85+from tests.engram import test_engram_gate_bwd as bwd
86+ 
87+fwd.test_engram_gate_fwd_compile(False)
88+compile_gate = bwd.compile_gate_for_test
89+ 
90+def compile_without_inference(op):
91+ compiled = compile_gate(op)
92+ 
93+ def call(*args):
94+ with torch.inference_mode(False):
95+ return compiled(*args)
96+ return call
97+ 
98+bwd.compile_gate_for_test = compile_without_inference
99+bwd.test_engram_gate_bwd_compile(128)
100+PY
101+```
102+ 
103+## 4. 编写正确性与契约测试
104+ 
105+在目标测试文件新增 compile 用例,选择代表性非空形状、支持的空输入、dtype、可选输出和关键分支。每个模式连续验证:
106+ 
107+1. 首次 compiled 调用。
108+2. 在原存储上更新有效数据,刷新相应权重、统计量、索引及状态。
109+3. 换为同元信息的新存储,并明确改变有效数据。
110+ 
111+每轮使用当前数据重新计算独立 reference;索引、scale 和其它受约束输入保持合法。检查返回结构、Tensor/None、shape、dtype、device、必要的布局、stride 和 offset,再比较数值。正确处理零元素和零归约,避免 NaN 导致无效比较。
112+ 
113+- **显式反向:** 每轮新建 reference 梯度叶子,避免梯度累积;使用前向统计量时,将当前输入对应的 detached 统计量写入测试缓冲区。检查梯度输出及分片元信息,按 API 语义归并分片并比较最终梯度。
114+- **原地状态:** reference 与被测状态相互独立,同时保留各自应有的 view、offset 和别名;比较返回值、实际写入、未修改区域和只读输入,按相同序列演进状态。
115+- **输出生命周期:** 在下次调用前完成校验,释放 tuple/list、解包变量和循环变量持有的临时图输出;需保存时在图外复制快照。保留调用方持久状态,避免在 inference 上下文外原地修改 inference 输出。
116+- **组合模型:** 验证目标算子与合理的上下游 Tensor 运算;有配套前后向时验证统计量在同图传递到显式 backward。
117+ 
118+对实际注册的 `CustomOpDef` 或 `OpOverload` 检查 schema 和 fake,补齐内部标量参数:
119+ 
120+```python
121+torch.library.opcheck(
122+ registered_op, args, kwargs=kwargs,
123+ test_utils=('test_schema', 'test_faketensor'),
124+)
125+```
126+ 
127+声明动态 shape、autograd 等额外能力时补对应检查。opcheck 不替代数值和副作用验证。
128+ 
129+## 5. 证明捕获、重放与复用
130+ 
131+按算子和关键分支分别取证,使用确有设备工作的非空用例;空输入或纯别名可能不产生设备图。检查当前后端实现和配置,排除禁用编译、force-eager、捕获上限等回退。
132+ 
133+TorchNPU 2.10 可通过 CPU profiler 的 `acl_graph_capture`、`acl_graph_replay`、`fx_run_eagerly` 事件诊断。可逐次调用采集:
134+ 
135+```python
136+def inspect_call(compiled, *args, **kwargs):
137+ with torch.profiler.profile(
138+ activities=[torch.profiler.ProfilerActivity.CPU],
139+ ) as profile:
140+ result = compiled(*args, **kwargs)
141+ torch.npu.synchronize()
142+ events = {event.key: event.count for event in profile.key_averages()}
143+ names = ('acl_graph_capture', 'acl_graph_replay', 'fx_run_eagerly')
144+ return result, {name: events.get(name, 0) for name in names}
145+```
146+ 
147+由调用方在所需执行上下文中调用 `inspect_call`;逐轮在图外准备输入和 reference,调用后校验并释放临时输出。确认真实捕获、固定元信息的后续调用进入重放路径,且无 eager 回退;组合或批量汇总事件不能代替单算子证据。事件名按安装的 torch_npu 实现核实,缺失时使用对应后端日志或计数器,缺证据不能判定通过。
148+ 
149+`fullgraph=True` 只排除 Dynamo 图中断。首次调用内部可能同时捕获和重放,不预设固定 warmup/capture 顺序。区分 Dynamo 重新编译和 NPU 重捕获;函数式算子验证复用,原地状态换地址可能合法重捕获。不要删除 mutation 声明来减少捕获次数,也不要把内部计数器固定数量作为所有数值测试的断言。
150+ 
151+## 6. 回归与验收
152+ 
153+先运行新增 compile 用例,再运行受影响的非 benchmark 测试;配套前后向须在同一新进程按 fwd→bwd 顺序执行。共享 helper 变更覆盖直接使用方。命令以 Engram gate 为例,替换为目标算子的测试入口:
154+ 
155+```bash
156+OMP_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 OPS_TILELANG_TEST_LEVEL=0 python -m pytest \
157+ tests/engram/test_engram_gate_fwd.py::test_engram_gate_fwd_compile \
158+ tests/engram/test_engram_gate_bwd.py::test_engram_gate_bwd_compile -x -v --tb=short
159+ 
160+OMP_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 OPS_TILELANG_TEST_LEVEL=0 python -m pytest \
161+ tests/engram/test_engram_gate_fwd.py tests/engram/test_engram_gate_bwd.py \
162+ -m 'not benchmark' -x -v --tb=short
163+```
164+ 
165+从仓库根目录执行,保存实际命令、版本、退出码及日志;使用 `tee` 时以 `PIPESTATUS[0]` 判断 pytest 结果。运行项目要求的 lint/格式检查和 `git diff --check`。常规改造不刷新 benchmark 基线、显存画像或清理缓存;新进程不代表磁盘 JIT 缓存为空。
166+ 
167+| 失败现象 | 排查方向 |
168+| --- | --- |
169+| `capture_begin` 报 inference Tensor 原地更新错误 | 检查版本和同进程前序捕获;在上述 2.10 复现环境按第 3 节处理,保留调用方的梯度语义。 |
170+| Dynamo 遇到文件探测或设备查询 | 分离环境检查,保留真实执行校验,确认元信息查询可追踪。 |
171+| schema、fake 或别名错误 | 核对可选返回、分支元信息、mutation、存储别名及 copy-back。 |
172+| 数值正确但缺少重放证据 | 检查有效设备工作、事件语义和后端回退;按算子逐次取证。 |
173+| 反复编译或捕获 | 检查 Tensor 属性、标量、可变配置、状态地址和后端捕获限制。 |
174+ 
175+验收须同时满足:公共契约保留、独立数值与副作用校验通过、注册契约正确、真实 NPU 捕获及后续重放成立、相关回归通过。不得通过换后端、关闭 fullgraph、放宽精度、调整测试顺序或无条件 skip 掩盖失败。
176+ 
177+批量改造逐算子记录“已实机打通 / 已改造待实机 / 具体阻塞”,列明支持边界、测试入口和图证据。静态检查、CPU `aot_eager`、fake/schema 或单独数值通过均不能代替实机入图验收。
@@ -3,10 +3,12 @@
3from __future__ import annotations3from __future__ import annotations
4 4 
5import math5import math
6+import os
6from numbers import Real7from numbers import Real
7 8 
8import torch9import torch
9 10 
11+from cann_ops_tilelang import config
10from cann_ops_tilelang.config import get_num_vector_cores, require_ascend, should_print_kernel_source12from cann_ops_tilelang.config import get_num_vector_cores, require_ascend, should_print_kernel_source
11 13 
12from .engram_gate_kernel import get_engram_gate_bwd_kernel, get_engram_gate_fwd_kernel14from .engram_gate_kernel import get_engram_gate_bwd_kernel, get_engram_gate_fwd_kernel
@@ -14,6 +16,10 @@ from .engram_gate_kernel import get_engram_gate_bwd_kernel, get_engram_gate_fwd_
14_SUPPORTED_HC_MULT = 416_SUPPORTED_HC_MULT = 4
15_NUM_GRAD_V_CORES = 1217_NUM_GRAD_V_CORES = 12
16 18 
19+# The Ascend runtime is fixed for a compiled graph; only this check is constant.
20+# Real implementations retain the check, and mutable core counts stay traceable.
21+torch.compiler.assume_constant_result(require_ascend)
22+ 
17 23 
18def _check_real(name: str, value: float, *, positive: bool) -> None:24def _check_real(name: str, value: float, *, positive: bool) -> None:
19 if not isinstance(value, Real):25 if not isinstance(value, Real):
@@ -50,7 +56,6 @@ def _validate_common_inputs(
50 kv: torch.Tensor,56 kv: torch.Tensor,
51 weight_fused: torch.Tensor,57 weight_fused: torch.Tensor,
52) -> tuple[int, int, int]:58) -> tuple[int, int, int]:
53- require_ascend()
54 _check_npu_tensor('hidden_states', hidden_states, dtype=torch.bfloat16)59 _check_npu_tensor('hidden_states', hidden_states, dtype=torch.bfloat16)
55 _check_npu_tensor('kv', kv, dtype=torch.bfloat16, device=hidden_states.device)60 _check_npu_tensor('kv', kv, dtype=torch.bfloat16, device=hidden_states.device)
56 _check_npu_tensor('weight_fused', weight_fused, dtype=torch.float32, device=hidden_states.device)61 _check_npu_tensor('weight_fused', weight_fused, dtype=torch.float32, device=hidden_states.device)
@@ -75,33 +80,40 @@ def _split_kv(kv: torch.Tensor, hc_mult: int, hidden_size: int) -> tuple[torch.T
75 return k.view(num_tokens, hc_mult, hidden_size), v80 return k.view(num_tokens, hc_mult, hidden_size), v
76 81 
77 82 
78-def engram_gate_fwd(83+def _validate_fwd_inputs(
84+ hidden_states: torch.Tensor,
85+ kv: torch.Tensor,
86+ weight_fused: torch.Tensor,
87+ eps: float,
88+ clamp_value: float,
89+ save_for_backward: bool,
90+) -> tuple[int, int, int]:
91+ shape = _validate_common_inputs(hidden_states, kv, weight_fused)
92+ _check_real('eps', eps, positive=False)
93+ _check_real('clamp_value', clamp_value, positive=True)
94+ if not isinstance(save_for_backward, bool):
95+ raise TypeError('save_for_backward must be bool.')
96+ return shape
97+ 
98+ 
99+def _engram_gate_fwd_impl(
79 hidden_states: torch.Tensor,100 hidden_states: torch.Tensor,
80 kv: torch.Tensor,101 kv: torch.Tensor,
81 weight_fused: torch.Tensor,102 weight_fused: torch.Tensor,
82 eps: float,103 eps: float,
83 clamp_value: float,104 clamp_value: float,
84 save_for_backward: bool = True,105 save_for_backward: bool = True,
85-) -> tuple[106+ num_cores: int | None = None,
86- torch.Tensor,107+) -> tuple[torch.Tensor, torch.Tensor | None, torch.Tensor | None, torch.Tensor | None, torch.Tensor | None]:
87- torch.Tensor | None,108+ num_tokens, hc_mult, hidden_size = _validate_fwd_inputs(
88- torch.Tensor | None,109+ hidden_states,
89- torch.Tensor | None,110+ kv,
90- torch.Tensor | None,111+ weight_fused,
91-]:112+ eps,
92- """Apply the Engram gate and optionally return backward statistics.113+ clamp_value,
93- 114+ save_for_backward,
94- ``hidden_states`` has shape ``(num_tokens, 4, hidden_size)``. ``kv`` packs115+ )
95- four key heads followed by one shared value head and therefore has shape116+ require_ascend()
96- ``(num_tokens, 5 * hidden_size)``. All activation tensors use BF16;
97- ``weight_fused`` and the saved statistics use FP32.
98- """
99- 
100- num_tokens, hc_mult, hidden_size = _validate_common_inputs(hidden_states, kv, weight_fused)
101- _check_real('eps', eps, positive=False)
102- _check_real('clamp_value', clamp_value, positive=True)
103- if not isinstance(save_for_backward, bool):
104- raise TypeError('save_for_backward must be bool.')
105 117 
106 output = torch.empty_like(hidden_states)118 output = torch.empty_like(hidden_states)
107 stats_shape = (num_tokens, hc_mult)119 stats_shape = (num_tokens, hc_mult)
@@ -124,8 +136,8 @@ def engram_gate_fwd(
124 k, v = _split_kv(kv, hc_mult, hidden_size)136 k, v = _split_kv(kv, hc_mult, hidden_size)
125 k_stride_s, k_stride_h, _ = k.stride()137 k_stride_s, k_stride_h, _ = k.stride()
126 v_stride_s, _ = v.stride()138 v_stride_s, _ = v.stride()
127- num_cores = get_num_vector_cores()139+ num_cores = get_num_vector_cores() if num_cores is None else num_cores
128- if num_cores % hc_mult:140+ if num_cores <= 0 or num_cores % hc_mult:
129 raise RuntimeError(f'Ascend vector core count {num_cores} must be divisible by hc_mult={hc_mult}.')141 raise RuntimeError(f'Ascend vector core count {num_cores} must be divisible by hc_mult={hc_mult}.')
130 kernel = get_engram_gate_fwd_kernel(142 kernel = get_engram_gate_fwd_kernel(
131 hidden_size,143 hidden_size,
@@ -148,7 +160,73 @@ def engram_gate_fwd(
148 return output, None, None, None, None160 return output, None, None, None, None
149 161 
150 162 
151-def engram_gate_bwd(163+# PyTorch 2.10 cannot infer optional Tensor returns, so declare the schema.
164+@torch.library.custom_op(
165+ 'cann_ops_tilelang::engram_gate_fwd',
166+ mutates_args=(),
167+ schema='(Tensor hidden_states, Tensor kv, Tensor weight_fused, float eps, float clamp_value, '
168+ 'int num_cores, bool save_for_backward=True) -> (Tensor, Tensor?, Tensor?, Tensor?, Tensor?)',
169+)
170+def _compiled_gate_fwd(
171+ hidden_states: torch.Tensor,
172+ kv: torch.Tensor,
173+ weight_fused: torch.Tensor,
174+ eps: float,
175+ clamp_value: float,
176+ num_cores: int,
177+ save_for_backward: bool = True,
178+) -> tuple[torch.Tensor, torch.Tensor | None, torch.Tensor | None, torch.Tensor | None, torch.Tensor | None]:
179+ return _engram_gate_fwd_impl(hidden_states, kv, weight_fused, eps, clamp_value, save_for_backward, num_cores)
180+ 
181+ 
182+@_compiled_gate_fwd.register_fake
183+def _gate_fwd_fake(hidden_states, kv, weight_fused, eps, clamp_value, num_cores, save_for_backward=True):
184+ num_tokens, hc_mult, _ = _validate_fwd_inputs(
185+ hidden_states,
186+ kv,
187+ weight_fused,
188+ eps,
189+ clamp_value,
190+ save_for_backward,
191+ )
192+ if num_tokens and (num_cores <= 0 or num_cores % hc_mult):
193+ raise RuntimeError(f'Ascend vector core count {num_cores} must be divisible by hc_mult={hc_mult}.')
194+ output = torch.empty_like(hidden_states)
195+ if not save_for_backward:
196+ return output, None, None, None, None
197+ dot = torch.empty((num_tokens, hc_mult), dtype=torch.float32, device=hidden_states.device)
198+ return output, dot, torch.empty_like(dot), torch.empty_like(dot), torch.empty_like(dot)
199+ 
200+ 
201+def engram_gate_fwd(
202+ hidden_states: torch.Tensor,
203+ kv: torch.Tensor,
204+ weight_fused: torch.Tensor,
205+ eps: float,
206+ clamp_value: float,
207+ save_for_backward: bool = True,
208+) -> tuple[torch.Tensor, torch.Tensor | None, torch.Tensor | None, torch.Tensor | None, torch.Tensor | None]:
209+ """Apply the Engram gate and optionally return backward statistics.
210+ 
211+ ``hidden_states`` has shape ``(num_tokens, 4, hidden_size)``. ``kv`` packs
212+ four key heads followed by one shared value head and therefore has shape
213+ ``(num_tokens, 5 * hidden_size)``. All activation tensors use BF16;
214+ ``weight_fused`` and the saved statistics use FP32.
215+ 
216+ With a compatible torch_npu, supports fixed-shape inference with
217+ ``torch.compile(..., backend='npugraph_ex', fullgraph=True, dynamic=False)``.
218+ The compiled path uses an opaque custom operator with a fake implementation;
219+ tracing does not compile or launch the TileLang kernel. No autograd formula
220+ is registered, including when ``save_for_backward=True``.
221+ """
222+ if torch.compiler.is_compiling():
ChaoyangJi
ChaoyangJiChaoyangJi12 days ago

让前向图感知核数配置变化

实机先以56个vector core捕获前向图,再调用config.set_num_ai_cores(13)切换为26核:eager会按规则报"核数须被4整除”,同一个compiled函数却继续重放旧图(capture=0、replay=1)。核数读取和检查都留在opaque实现内,捕获后的重放不会再执行这些Python逻辑。建议像反向路径一样将核数作为内部标量传入,使配置变化触发重新特化和校验,并补充回归用例。

likedislike
223+ # Empty inputs return before core configuration is read in the eager path.
224+ num_cores = _get_gate_num_cores() if hidden_states.numel() else 0
225+ return _compiled_gate_fwd(hidden_states, kv, weight_fused, eps, clamp_value, num_cores, save_for_backward)
226+ return _engram_gate_fwd_impl(hidden_states, kv, weight_fused, eps, clamp_value, save_for_backward)
227+ 
228+ 
229+def _validate_bwd_inputs(
152 grad_out: torch.Tensor,230 grad_out: torch.Tensor,
153 hidden_states: torch.Tensor,231 hidden_states: torch.Tensor,
154 kv: torch.Tensor,232 kv: torch.Tensor,
@@ -158,14 +236,7 @@ def engram_gate_bwd(
158 rstd_x: torch.Tensor,236 rstd_x: torch.Tensor,
159 rstd_k: torch.Tensor,237 rstd_k: torch.Tensor,
160 clamp_value: float,238 clamp_value: float,
161-) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:239+) -> tuple[int, int, int]:
162- """Run the Engram gate backward kernel.
163- 
164- Returns gradients for ``hidden_states`` and packed ``kv``, plus partial
165- FP32 gradients for ``weight_fused``. Pass the latter to
166- :func:`engram_grad_w_reduce` together with the unfused weights.
167- """
168- 
169 num_tokens, hc_mult, hidden_size = _validate_common_inputs(hidden_states, kv, weight_fused)240 num_tokens, hc_mult, hidden_size = _validate_common_inputs(hidden_states, kv, weight_fused)
170 _check_npu_tensor('grad_out', grad_out, dtype=torch.bfloat16, device=hidden_states.device)241 _check_npu_tensor('grad_out', grad_out, dtype=torch.bfloat16, device=hidden_states.device)
171 if grad_out.shape != hidden_states.shape:242 if grad_out.shape != hidden_states.shape:
@@ -181,8 +252,25 @@ def engram_gate_bwd(
181 if tensor.shape != stats_shape:252 if tensor.shape != stats_shape:
182 raise ValueError(f'{name} must have shape {stats_shape}, got {tuple(tensor.shape)}.')253 raise ValueError(f'{name} must have shape {stats_shape}, got {tuple(tensor.shape)}.')
183 _check_real('clamp_value', clamp_value, positive=True)254 _check_real('clamp_value', clamp_value, positive=True)
255+ return num_tokens, hc_mult, hidden_size
184 256 
185- num_cores = get_num_vector_cores()257+ 
258+def _get_gate_num_cores() -> int:
259+ # Compiled wrappers need this metadata without tracing require_ascend.
260+ # Keep mutable tuning settings visible to Dynamo so they remain guarded.
261+ detected = config.get_num_ai_cores() * 2
262+ # Guard the key set as well, including an unset-to-set environment change.
263+ key = 'OPS_TILELANG_NUM_VECTOR_CORES'
264+ override = os.environ[key] if key in tuple(os.environ) else None
265+ if override is None:
266+ return detected
267+ requested = int(override)
268+ if requested <= 0 or requested > detected:
269+ raise ValueError(f'OPS_TILELANG_NUM_VECTOR_CORES must be in [1, {detected}], got {requested}.')
270+ return requested
271+ 
272+ 
273+def _get_bwd_num_partials(hc_mult: int, num_cores: int) -> int:
186 num_main_cores = num_cores - _NUM_GRAD_V_CORES274 num_main_cores = num_cores - _NUM_GRAD_V_CORES
187 num_head_groups = hc_mult // 2275 num_head_groups = hc_mult // 2
188 if num_main_cores <= 0 or num_main_cores % num_head_groups:276 if num_main_cores <= 0 or num_main_cores % num_head_groups:
@@ -190,7 +278,27 @@ def engram_gate_bwd(
190 f'Ascend vector core count {num_cores} is incompatible with '278 f'Ascend vector core count {num_cores} is incompatible with '
191 f'{_NUM_GRAD_V_CORES} grad-v cores and hc_mult={hc_mult}.'279 f'{_NUM_GRAD_V_CORES} grad-v cores and hc_mult={hc_mult}.'
192 )280 )
193- num_partials = num_main_cores // num_head_groups281+ return num_main_cores // num_head_groups
282+ 
283+ 
284+def _engram_gate_bwd_impl(
285+ grad_out: torch.Tensor,
286+ hidden_states: torch.Tensor,
287+ kv: torch.Tensor,
288+ weight_fused: torch.Tensor,
289+ dot: torch.Tensor,
290+ gate_score: torch.Tensor,
291+ rstd_x: torch.Tensor,
292+ rstd_k: torch.Tensor,
293+ clamp_value: float,
294+ num_cores: int | None = None,
295+) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
296+ require_ascend()
297+ num_tokens, hc_mult, hidden_size = _validate_bwd_inputs(
298+ grad_out, hidden_states, kv, weight_fused, dot, gate_score, rstd_x, rstd_k, clamp_value
299+ )
300+ num_cores = get_num_vector_cores() if num_cores is None else num_cores
301+ num_partials = _get_bwd_num_partials(hc_mult, num_cores)
194 302 
195 grad_hidden_states = torch.empty_like(hidden_states)303 grad_hidden_states = torch.empty_like(hidden_states)
196 grad_kv = torch.empty_like(kv)304 grad_kv = torch.empty_like(kv)
@@ -237,3 +345,78 @@ def engram_gate_bwd(
237 grad_w_partial,345 grad_w_partial,
238 )346 )
239 return grad_hidden_states, grad_kv, grad_w_partial347 return grad_hidden_states, grad_kv, grad_w_partial
348+ 
349+ 
350+@torch.library.custom_op('cann_ops_tilelang::engram_gate_bwd', mutates_args=())
351+def _compiled_gate_bwd(
352+ grad_out: torch.Tensor,
353+ hidden_states: torch.Tensor,
354+ kv: torch.Tensor,
355+ weight_fused: torch.Tensor,
356+ dot: torch.Tensor,
357+ gate_score: torch.Tensor,
358+ rstd_x: torch.Tensor,
359+ rstd_k: torch.Tensor,
360+ clamp_value: float,
361+ num_cores: int,
362+) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
363+ return _engram_gate_bwd_impl(
364+ grad_out, hidden_states, kv, weight_fused, dot, gate_score, rstd_x, rstd_k, clamp_value, num_cores
365+ )
366+ 
367+ 
368+@_compiled_gate_bwd.register_fake
369+def _gate_bwd_fake(grad_out, hidden_states, kv, weight_fused, dot, gate_score, rstd_x, rstd_k, clamp_value, num_cores):
370+ _, hc_mult, hidden_size = _validate_bwd_inputs(
371+ grad_out, hidden_states, kv, weight_fused, dot, gate_score, rstd_x, rstd_k, clamp_value
372+ )
373+ num_partials = _get_bwd_num_partials(hc_mult, num_cores)
374+ grad_w_partial = torch.empty(
375+ (num_partials, hc_mult, hidden_size),
376+ dtype=torch.float32,
377+ device=hidden_states.device,
378+ )
379+ return torch.empty_like(hidden_states), torch.empty_like(kv), grad_w_partial
380+ 
381+ 
382+def engram_gate_bwd(
383+ grad_out: torch.Tensor,
384+ hidden_states: torch.Tensor,
385+ kv: torch.Tensor,
386+ weight_fused: torch.Tensor,
387+ dot: torch.Tensor,
388+ gate_score: torch.Tensor,
389+ rstd_x: torch.Tensor,
390+ rstd_k: torch.Tensor,
391+ clamp_value: float,
392+) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
393+ """Run the Engram gate backward kernel.
394+ 
395+ Returns gradients for ``hidden_states`` and packed ``kv``, plus partial
396+ FP32 gradients for ``weight_fused``. Pass the latter to
397+ :func:`engram_grad_w_reduce` together with the unfused weights.
398+ 
399+ The explicit backward call supports fixed-shape
400+ ``torch.compile(..., backend='npugraph_ex', fullgraph=True, dynamic=False)``
401+ Tested with torch/torch_npu 2.10 using ``torch.inference_mode()`` for
402+ both graph calls; this does not add automatic differentiation support.
403+ Vector core count is an internal scalar argument so fake execution can infer
404+ the partial-gradient shape without querying the device. No autograd formula
405+ or higher-order gradient support is registered.
406+ """
407+ if torch.compiler.is_compiling():
408+ return _compiled_gate_bwd(
409+ grad_out,
410+ hidden_states,
411+ kv,
412+ weight_fused,
413+ dot,
414+ gate_score,
415+ rstd_x,
416+ rstd_k,
417+ clamp_value,
418+ _get_gate_num_cores(),
419+ )
420+ return _engram_gate_bwd_impl(
421+ grad_out, hidden_states, kv, weight_fused, dot, gate_score, rstd_x, rstd_k, clamp_value
422+ )
@@ -55,3 +55,24 @@ def engram_gate_ref(
55 if save_for_backward:55 if save_for_backward:
56 return output, raw_dot, gate_score, rstd_x, rstd_k56 return output, raw_dot, gate_score, rstd_x, rstd_k
57 return output57 return output
58+ 
59+ 
60+def compile_gate_for_test(operator):
61+ return torch.compile(operator, backend='npugraph_ex', fullgraph=True, dynamic=False)
62+ 
63+ 
64+def update_gate_inputs(step, x_data, kv_data, wh_data, we_data, weight_fused):
65+ if step == 1:
66+ # Change values without replacing the input storage.
67+ x_data.add_(0.125)
68+ kv_data.neg_()
69+ wh_data.neg_()
70+ weight_fused.copy_(wh_data.float() * we_data.float())
71+ elif step == 2:
72+ # Replace all input tensors while preserving their metadata.
73+ x_data = torch.randn_like(x_data)
74+ kv_data = torch.randn_like(kv_data)
75+ wh_data = torch.randn_like(wh_data)
76+ we_data = torch.randn_like(we_data)
77+ weight_fused = wh_data.float() * we_data.float()
78+ return x_data, kv_data, wh_data, we_data, weight_fused
@@ -5,7 +5,7 @@ import torch
5 5 
6import cann_ops_tilelang6import cann_ops_tilelang
7from cann_ops_tilelang.config import get_device7from cann_ops_tilelang.config import get_device
8-from tests.engram.engram_gate_ref import engram_gate_ref8+from tests.engram.engram_gate_ref import compile_gate_for_test, engram_gate_ref, update_gate_inputs
9from tests.testing.bench import make_param_id9from tests.testing.bench import make_param_id
10from tests.testing.generator import generate_hidden_sizes, generate_num_tokens, get_test_level10from tests.testing.generator import generate_hidden_sizes, generate_num_tokens, get_test_level
11from tests.testing.numeric import calc_diff, count_bytes11from tests.testing.numeric import calc_diff, count_bytes
@@ -84,6 +84,102 @@ def test_engram_gate_bwd(params):
84 assert diff_we < 1e-8, f'grad_we mismatch: {diff_we:.6e}'84 assert diff_we < 1e-8, f'grad_we mismatch: {diff_we:.6e}'
85 85 
86 86 
87+def _backward_compile_reference(
88+ x_data,
89+ kv_data,
90+ wh_data,
91+ we_data,
92+ grad_out,
93+ eps,
94+ clamp_value,
95+):
96+ x_ref = x_data.clone().requires_grad_(True)
97+ kv_ref = kv_data.clone().requires_grad_(True)
98+ wh_ref = wh_data.float().requires_grad_(True)
99+ we_ref = we_data.float().requires_grad_(True)
100+ output_ref, dot_ref, gate_score_ref, rstd_x_ref, rstd_k_ref = engram_gate_ref(
101+ hidden_states=x_ref,
102+ kv=kv_ref,
103+ weight_hidden=wh_ref,
104+ weight_embed=we_ref,
105+ clamp_value=clamp_value,
106+ eps=eps,
107+ save_for_backward=True,
108+ )
109+ gradients = torch.autograd.grad(output_ref, (x_ref, kv_ref, wh_ref, we_ref), grad_outputs=grad_out)
110+ return dot_ref.detach(), gate_score_ref.detach(), rstd_x_ref.detach(), rstd_k_ref.detach(), gradients
111+ 
112+ 
113+def _assert_compile_gradients(actuals, references, step):
114+ for name, actual, reference in zip(('grad_x', 'grad_kv', 'grad_wh', 'grad_we'), actuals, references, strict=True):
115+ assert reference is not None
116+ assert actual.shape == reference.shape
117+ assert actual.dtype == reference.dtype
118+ assert actual.device == reference.device
119+ diff = calc_diff(actual, reference)
120+ assert diff < 1e-8, f'{name} mismatch at step {step}: {diff:.6e}'
121+ 
122+ 
123+def _reduce_compile_gradients(outputs, wh_data, we_data):
124+ grad_x, grad_kv, grad_w_partial = outputs
125+ assert grad_w_partial.ndim == 3
126+ assert grad_w_partial.shape[1:] == wh_data.shape
127+ assert grad_w_partial.dtype == torch.float32
128+ assert grad_w_partial.device == wh_data.device
129+ grad_w_fused = grad_w_partial.sum(0)
130+ return grad_x, grad_kv, grad_w_fused * we_data.float(), grad_w_fused * wh_data.float()
131+ 
132+ 
133+@pytest.mark.parametrize('num_tokens', [0, 128])
134+def test_engram_gate_bwd_compile(num_tokens):
135+ x_data, kv_data, wh_data, we_data, weight_fused, grad_out, eps, clamp_value = generate_test_data(
136+ {'num_tokens': num_tokens, 'hc': 4, 'hidden': 4096}
137+ )
138+ dot = torch.empty((num_tokens, 4), dtype=torch.float32, device=x_data.device)
139+ gate_score, rstd_x, rstd_k = torch.empty_like(dot), torch.empty_like(dot), torch.empty_like(dot)
140+ compiled = compile_gate_for_test(cann_ops_tilelang.engram_gate_bwd)
141+ for step in range(3):
142+ x_data, kv_data, wh_data, we_data, weight_fused = update_gate_inputs(
143+ step, x_data, kv_data, wh_data, we_data, weight_fused
144+ )
145+ if step == 1:
146+ grad_out.neg_()
147+ elif step == 2:
148+ grad_out = torch.randn_like(grad_out)
149+ dot = torch.empty_like(dot)
150+ gate_score, rstd_x, rstd_k = torch.empty_like(dot), torch.empty_like(dot), torch.empty_like(dot)
151+ dot_ref, gate_score_ref, rstd_x_ref, rstd_k_ref, references = _backward_compile_reference(
152+ x_data=x_data,
153+ kv_data=kv_data,
154+ wh_data=wh_data,
155+ we_data=we_data,
156+ grad_out=grad_out,
157+ eps=eps,
158+ clamp_value=clamp_value,
159+ )
160+ dot.copy_(dot_ref)
161+ gate_score.copy_(gate_score_ref)
162+ rstd_x.copy_(rstd_x_ref)
163+ rstd_k.copy_(rstd_k_ref)
164+ # Match the forward graph's capture mode in the same process.
165+ with torch.inference_mode():
166+ outputs = compiled(
167+ grad_out,
168+ x_data,
169+ kv_data,
170+ weight_fused,
171+ dot,
172+ gate_score,
173+ rstd_x,
174+ rstd_k,
175+ clamp_value,
176+ )
177+ actuals = _reduce_compile_gradients(outputs, wh_data, we_data)
178+ _assert_compile_gradients(actuals, references, step)
179+ # Release graph outputs before their storage is reused by the next call.
180+ del outputs, actuals
181+ 
182+ 
87@pytest.mark.benchmark183@pytest.mark.benchmark
88@pytest.mark.parametrize('params', generate_test_params(0), ids=make_param_id)184@pytest.mark.parametrize('params', generate_test_params(0), ids=make_param_id)
89def test_engram_gate_bwd_benchmark(benchmark_timer, benchmark_record, params):185def test_engram_gate_bwd_benchmark(benchmark_timer, benchmark_record, params):
@@ -4,8 +4,9 @@ import pytest
4import torch4import torch
5 5 
6import cann_ops_tilelang6import cann_ops_tilelang
7+from cann_ops_tilelang import config
7from cann_ops_tilelang.config import get_device8from cann_ops_tilelang.config import get_device
8-from tests.engram.engram_gate_ref import engram_gate_ref9+from tests.engram.engram_gate_ref import compile_gate_for_test, engram_gate_ref, update_gate_inputs
9from tests.testing.bench import make_param_id10from tests.testing.bench import make_param_id
10from tests.testing.generator import generate_hidden_sizes, generate_num_tokens, get_test_level11from tests.testing.generator import generate_hidden_sizes, generate_num_tokens, get_test_level
11from tests.testing.numeric import assert_equal, calc_diff, count_bytes12from tests.testing.numeric import assert_equal, calc_diff, count_bytes
@@ -87,6 +88,142 @@ def test_engram_gate_fwd(params):
87 assert_equal(out_no_save, out_save)88 assert_equal(out_no_save, out_save)
88 89 
89 90 
91+@pytest.mark.parametrize('save_for_backward', [False, True], ids=['no_save', 'save'])
92+@torch.inference_mode()
93+def test_engram_gate_fwd_compile(save_for_backward):
94+ x_data, kv_data, wh_data, we_data, weight_fused, eps, clamp_value = generate_test_data(
95+ {'num_tokens': 128, 'hc': 4, 'hidden': 4096}
96+ )
97+ compiled = compile_gate_for_test(cann_ops_tilelang.engram_gate_fwd)
98+ for step in range(3):
99+ x_data, kv_data, wh_data, we_data, weight_fused = update_gate_inputs(
100+ step, x_data, kv_data, wh_data, we_data, weight_fused
101+ )
102+ 
103+ references = engram_gate_ref(
104+ x_data,
105+ kv_data,
106+ wh_data,
107+ we_data,
108+ clamp_value,
109+ eps,
110+ save_for_backward=True,
111+ )
112+ if not save_for_backward:
113+ references = (references[0], None, None, None, None)
114+ outputs = compiled(x_data, kv_data, weight_fused, eps, clamp_value, save_for_backward)
115+ for name, actual, reference in zip(
116+ ('output', 'dot', 'gate_score', 'rstd_x', 'rstd_k'),
117+ outputs,
118+ references,
119+ strict=True,
120+ ):
121+ if reference is None:
122+ assert actual is None, f'{name} must be None when save_for_backward=False'
123+ continue
124+ assert actual is not None
125+ assert actual.shape == reference.shape
126+ assert actual.dtype == reference.dtype
127+ assert actual.device == reference.device
128+ diff = calc_diff(actual, reference)
129+ assert diff < 2e-10, f'{name} mismatch at step {step}: {diff:.6e}'
130+ # Release every graph output reference before the next call reuses storage.
131+ del outputs, actual
132+ 
133+ 
134+def _make_fwd_core_inputs(num_tokens):
135+ data = generate_test_data({'num_tokens': num_tokens, 'hc': 4, 'hidden': 4096})
136+ x_data, kv_data, wh_data, we_data, _, eps, clamp_value = data
137+ references = engram_gate_ref(
138+ hidden_states=x_data,
139+ kv=kv_data,
140+ weight_hidden=wh_data,
141+ weight_embed=we_data,
142+ clamp_value=clamp_value,
143+ eps=eps,
144+ save_for_backward=True,
145+ )
146+ return data, references
147+ 
148+ 
149+def _assert_fwd_core_outputs(outputs, references, save_for_backward):
150+ assert len(outputs) == len(references) == 5
151+ for index, (actual, reference) in enumerate(zip(outputs, references, strict=True)):
152+ if index and not save_for_backward:
153+ assert actual is None
154+ continue
155+ assert actual is not None
156+ assert actual.shape == reference.shape
157+ assert actual.dtype == reference.dtype
158+ assert actual.device == reference.device
159+ if reference.numel():
160+ diff = calc_diff(actual, reference)
161+ assert diff < 2e-10, f'output {index} mismatch after a core-count change: {diff:.6e}'
162+ 
163+ 
164+def _assert_fwd_core_rejected(compiled, data, save_for_backward, num_cores):
165+ x_data, kv_data, _, _, weight_fused, eps, clamp_value = data
166+ for operator in (cann_ops_tilelang.engram_gate_fwd, compiled):
167+ with pytest.raises(RuntimeError, match='divisible') as error:
168+ operator(x_data, kv_data, weight_fused, eps, clamp_value, save_for_backward)
169+ assert str(num_cores) in str(error.value)
170+ 
171+ 
172+@pytest.mark.parametrize('save_for_backward', [False, True], ids=['no_save', 'save'])
173+@torch.inference_mode()
174+def test_engram_gate_fwd_compile_core_config(save_for_backward, monkeypatch):
175+ torch.compiler.reset()
176+ try:
177+ monkeypatch.delenv('OPS_TILELANG_NUM_VECTOR_CORES', raising=False)
178+ data, references = _make_fwd_core_inputs(128)
179+ x_data, kv_data, _, _, weight_fused, eps, clamp_value = data
180+ compiled = compile_gate_for_test(cann_ops_tilelang.engram_gate_fwd)
181+ default_ai_cores = config.get_num_ai_cores()
182+ assert default_ai_cores >= 4 and default_ai_cores % 2 == 0
183+ invalid_ai_cores = 13 if default_ai_cores >= 13 else default_ai_cores - 1
184+ for phase in ('default', 'reduced', 'invalid', 'reset'):
185+ if phase == 'reduced':
186+ config.set_num_ai_cores(default_ai_cores - 2)
187+ assert config.get_num_vector_cores() == 2 * default_ai_cores - 4
188+ if phase == 'invalid':
189+ config.set_num_ai_cores(invalid_ai_cores)
190+ if phase == 'reset':
191+ config.reset_runtime_config()
192+ if phase == 'invalid':
193+ _assert_fwd_core_rejected(compiled, data, save_for_backward, 2 * invalid_ai_cores)
194+ else:
195+ outputs = compiled(x_data, kv_data, weight_fused, eps, clamp_value, save_for_backward)
196+ _assert_fwd_core_outputs(outputs, references, save_for_backward)
197+ del outputs
198+ finally:
199+ config.reset_runtime_config()
200+ torch.compiler.reset()
201+ 
202+ 
203+@pytest.mark.parametrize('save_for_backward', [False, True], ids=['no_save', 'save'])
204+@torch.inference_mode()
205+def test_engram_gate_fwd_compile_empty_core_config(save_for_backward, monkeypatch):
206+ torch.compiler.reset()
207+ try:
208+ monkeypatch.delenv('OPS_TILELANG_NUM_VECTOR_CORES', raising=False)
209+ data, references = _make_fwd_core_inputs(0)
210+ x_data, kv_data, _, _, weight_fused, eps, clamp_value = data
211+ invalid_ai_cores = 13 if config.get_num_ai_cores() >= 13 else 1
212+ config.set_num_ai_cores(invalid_ai_cores)
213+ compiled = compile_gate_for_test(cann_ops_tilelang.engram_gate_fwd)
214+ for vector_cap in (None, 'abc'):
215+ if vector_cap is not None:
216+ monkeypatch.setenv('OPS_TILELANG_NUM_VECTOR_CORES', vector_cap)
217+ config.clear_runtime_caches()
218+ for operator in (cann_ops_tilelang.engram_gate_fwd, compiled):
219+ outputs = operator(x_data, kv_data, weight_fused, eps, clamp_value, save_for_backward)
220+ _assert_fwd_core_outputs(outputs, references, save_for_backward)
221+ del outputs
222+ finally:
223+ config.reset_runtime_config()
224+ torch.compiler.reset()
225+ 
226+ 
90@pytest.mark.benchmark227@pytest.mark.benchmark
91@pytest.mark.parametrize('params', generate_test_params(0), ids=make_param_id)228@pytest.mark.parametrize('params', generate_test_params(0), ids=make_param_id)
92def test_engram_gate_fwd_benchmark(benchmark_timer, benchmark_record, params):229def test_engram_gate_fwd_benchmark(benchmark_timer, benchmark_record, params):