@@ -127,13 +127,65 @@ def radial_mix_reference(
127127def _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# ======================================================================
0 commit comments