Skip to content

Commit 1dcb1c5

Browse files
committed
fix freeze
1 parent 1c3f110 commit 1dcb1c5

2 files changed

Lines changed: 120 additions & 13 deletions

File tree

deepmd/pt/model/descriptor/sezm_nn/triton/radial_mix.py

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,65 @@ def radial_mix_reference(
127127
def _radial_mix_backward_reference(
128128
grad_out: Tensor, compact: Tensor, x_local: Tensor, channel_basis: Tensor, lmax: int
129129
) -> tuple[Tensor, Tensor]:
130-
"""Eager backward returning ``(grad_compact, grad_x_local)`` via autograd."""
131-
with torch.enable_grad():
132-
compact_req = compact.detach().requires_grad_(True)
133-
x_req = x_local.detach().requires_grad_(True)
134-
out = radial_mix_reference(compact_req, x_req, channel_basis, lmax)
135-
grad_compact, grad_x = torch.autograd.grad(out, [compact_req, x_req], grad_out)
136-
return grad_compact, grad_x
130+
"""Closed-form eager backward of :func:`radial_mix_reference`.
131+
132+
Gradients are evaluated analytically per diagonal block, mirroring the
133+
contractions of the Triton backward. A closed form is required rather than a
134+
nested ``autograd.grad``: this routine is the CPU backend of the
135+
``radial_mix_block_bwd`` operator, which carries no autograd formula and is
136+
consequently dispatched under ``_AutoDispatchBelowAutograd`` whenever the
137+
force graph is replayed without grad (the SeZM ``.pt2`` freeze does so under
138+
:func:`torch.no_grad`). That guard excludes the autograd key, so a nested
139+
``autograd.grad`` would observe an output without a ``grad_fn``.
140+
141+
Parameters
142+
----------
143+
grad_out : Tensor
144+
Upstream gradient with shape ``(E, reduced_dim, C)``.
145+
compact : Tensor
146+
Projected radial degree kernel with shape ``(E, degree_kernel_size, R)``.
147+
x_local : Tensor
148+
Edge-local reduced features with shape ``(E, reduced_dim, C)``.
149+
channel_basis : Tensor
150+
Per-rank channel basis with shape ``(R, C)``.
151+
lmax : int
152+
Maximum spherical-harmonic degree.
153+
154+
Returns
155+
-------
156+
tuple[Tensor, Tensor]
157+
Gradients ``(grad_compact, grad_x_local)``, matching ``compact`` and
158+
``x_local`` in shape respectively.
159+
"""
160+
n_edge, reduced_dim, channels = x_local.shape
161+
grad_x_local = torch.zeros_like(x_local)
162+
grad_compact = torch.zeros_like(compact)
163+
for coeff0, comp0, num_l in _block_layout(int(lmax)):
164+
# Forward of this block (see ``radial_mix_reference``):
165+
# out[e, o, c] = sum_{i, r} K[e, o, i, r] * x[e, i, c] * cb[r, c]
166+
# with K[e, o, i, r] = compact[e, comp0 + i * num_l + o, r].
167+
k_block = (
168+
compact[:, comp0 : comp0 + num_l * num_l, :]
169+
.reshape(n_edge, num_l, num_l, -1)
170+
.permute(0, 2, 1, 3)
171+
) # (E, o, i, R)
172+
x_block = x_local[:, coeff0 : coeff0 + num_l, :] # (E, i, C)
173+
g_block = grad_out[:, coeff0 : coeff0 + num_l, :] # (E, o, C)
174+
175+
# grad_x[e, i, c] = sum_r cb[r, c] * sum_o K[e, o, i, r] * g[e, o, c].
176+
gx = torch.einsum("eoir,eoc->eicr", k_block, g_block) # (E, i, C, R)
177+
grad_x_local[:, coeff0 : coeff0 + num_l, :] += torch.einsum(
178+
"eicr,rc->eic", gx, channel_basis
179+
)
180+
181+
# grad_K[e, o, i, r] = sum_c cb[r, c] * x[e, i, c] * g[e, o, c], scattered
182+
# back to the compact slot comp0 + i * num_l + o. The shared m = +-1
183+
# blocks address the same slots, so the in-place add accumulates both.
184+
gk = torch.einsum("eoc,eic,rc->eoir", g_block, x_block, channel_basis)
185+
grad_compact[:, comp0 : comp0 + num_l * num_l, :] += gk.permute(
186+
0, 2, 1, 3
187+
).reshape(n_edge, num_l * num_l, -1)
188+
return grad_compact, grad_x_local
137189

138190

139191
# ======================================================================

source/tests/pt/model/test_sezm_export.py

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import contextlib
1414
import copy
1515
import json
16+
import os
1617
import tempfile
1718
import unittest
1819
import zipfile
@@ -216,18 +217,24 @@ class TestSeZMExportPipeline(_ClearDefaultDeviceTestCase):
216217
it must reproduce the eager result exactly. Drift here implies a
217218
bug in ``forward_common_lower_exportable`` or the dynamic-shape
218219
spec, not in AOTI. The pipeline is built once per class because
219-
``make_fx`` and ``.pte`` round-trip dominate wall time.
220+
``make_fx`` and ``.pte`` round-trip dominate wall time. Subclasses
221+
set ``TRITON_INFER`` to drive the identical pipeline through the
222+
opt-in Triton inference kernels.
220223
"""
221224

225+
# ``DP_TRITON_INFER`` policy applied while the model is constructed.
226+
TRITON_INFER = "0"
227+
222228
@classmethod
223229
def setUpClass(cls) -> None:
224230
super().setUpClass()
225231
try:
226-
cls.model = _build_tiny_sezm_model()
227-
cls.sample_inputs = _make_sample(cls.model, nloc=7, start=2)
228-
cls.traced, cls.loaded, cls._pte_tmp = cls._build_pipeline(
229-
cls.model, cls.sample_inputs
230-
)
232+
with mock.patch.dict(os.environ, {"DP_TRITON_INFER": cls.TRITON_INFER}):
233+
cls.model = _build_tiny_sezm_model()
234+
cls.sample_inputs = _make_sample(cls.model, nloc=7, start=2)
235+
cls.traced, cls.loaded, cls._pte_tmp = cls._build_pipeline(
236+
cls.model, cls.sample_inputs
237+
)
231238
except Exception:
232239
super().tearDownClass()
233240
raise
@@ -327,6 +334,54 @@ def test_loaded_pte_matches_eager_different_shape(self) -> None:
327334
)
328335

329336

337+
class TestSeZMExportPipelineTritonInfer(TestSeZMExportPipeline):
338+
"""The same trace / ``.pte`` pipeline exercised with ``DP_TRITON_INFER=1``.
339+
340+
Inheriting the parity suite asserts the Triton-enabled model still traces,
341+
exports, and reloads, and that the loaded ``.pte`` reproduces its eager
342+
forward — including the force path, whose custom ``*_bwd`` ops run inside
343+
``_AutoDispatchBelowAutograd`` during the export's no-grad replay and must
344+
therefore be closed-form. Two checks are added: the captured graph routes
345+
through the custom ops, and the Triton ``.pte`` matches the dense (Triton-off)
346+
inference, proving ``DP_TRITON_INFER`` swaps the implementation without
347+
changing results.
348+
"""
349+
350+
TRITON_INFER = "1"
351+
352+
@classmethod
353+
def setUpClass(cls) -> None:
354+
super().setUpClass()
355+
try:
356+
with mock.patch.dict(os.environ, {"DP_TRITON_INFER": "0"}):
357+
dense_model = _build_tiny_sezm_model()
358+
cls.dense_out = _eager_forward(dense_model, cls.sample_inputs)
359+
except Exception:
360+
super().tearDownClass()
361+
raise
362+
363+
@classmethod
364+
def tearDownClass(cls) -> None:
365+
try:
366+
if hasattr(cls, "dense_out"):
367+
delattr(cls, "dense_out")
368+
finally:
369+
super().tearDownClass()
370+
371+
def test_force_graph_carries_triton_ops(self) -> None:
372+
"""``DP_TRITON_INFER=1`` must route the descriptor through the custom ops."""
373+
code = self.traced.code
374+
self.assertIn("radial_mix_block_bwd", code)
375+
self.assertIn("rotate_to_local", code)
376+
377+
def test_loaded_pte_matches_dense(self) -> None:
378+
"""The Triton-on ``.pte`` reproduces the dense-path inference."""
379+
loaded_out = self.loaded(*self.sample_inputs)
380+
self._assert_dict_allclose(
381+
self.dense_out, loaded_out, context="triton .pte vs dense eager"
382+
)
383+
384+
330385
class _FrozenPt2Fixture(_ClearDefaultDeviceTestCase):
331386
"""Shared setUp/tearDown: freeze a tiny SeZM checkpoint to ``.pt2`` once.
332387

0 commit comments

Comments
 (0)