Skip to content

Commit 802979b

Browse files
committed
Handle fallback gate and dense Wigner inputs
The fast GPU backend can be selected for channel widths and autocast dtypes that the packed FP32 Triton gate does not support. Keep the packed GEMM outputs, but route their activation through the existing PyTorch block implementation unless the actual outputs are FP32 with a power-of-two channel width. This avoids rejecting valid fast-backend configurations without repeating the GEMMs. Compact Wigner storage also made the exported fused operations reject their prior dense [E, 9, 9] inputs. Pack dense block-diagonal inputs before entering custom autograd so the public interface, dense gradient shape, and zero off-block gradients are preserved. Test Plan: ```bash PYTHONPATH="$PWD/src" pytest -q tests/core/models/uma/uma_fast/test_execution_backends.py -k "gate_activation_fallback or compact_edge_degree_matches_dense" PYTHONPATH="$PWD/src" pytest -q tests/core/models/uma/uma_fast/test_fused_edgewise.py -k "exported_fused_ops_accept_dense_wigner or wigner_conv1_fused_dynamic_compile" pre-commit run --files src/fairchem/core/models/uma/escn_md_block.py src/fairchem/core/models/uma/nn/execution_backends.py src/fairchem/core/models/uma/triton/fused_wigner.py tests/core/models/uma/uma_fast/test_execution_backends.py tests/core/models/uma/uma_fast/test_fused_edgewise.py ``` Authored with assistance from an AI coding tool.
1 parent 522ce9d commit 802979b

5 files changed

Lines changed: 172 additions & 23 deletions

File tree

src/fairchem/core/models/uma/escn_md_block.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -269,18 +269,12 @@ def _forward_chunk_fused(
269269
x_full, edge_index, wigner, x_edge, sphere_channels
270270
)
271271
if self.act_type == "gate":
272-
if x_full.dtype == torch.float32:
273-
x0, x1, x2 = self.so2_conv_1.gemm_outputs_from_packed(
274-
m0_buf, m1_buf, m2_buf
275-
)
276-
x_blocks = self.backend.fused_gate_activation(
277-
x0, x1, x2, self.hidden_channels
278-
)
279-
else:
280-
x_blocks, x_0_gating = self.so2_conv_1.gemm_blocks_from_packed(
281-
m0_buf, m1_buf, m2_buf
282-
)
283-
x_blocks = self.act.forward_m_blocks(x_0_gating, x_blocks)
272+
x0, x1, x2 = self.so2_conv_1.gemm_outputs_from_packed(
273+
m0_buf, m1_buf, m2_buf
274+
)
275+
x_blocks = self.backend.gate_activation(
276+
x0, x1, x2, self.hidden_channels, self.act
277+
)
284278
g0, g1, g2 = self.so2_conv_2.gemms_from_blocks(x_blocks)
285279
else:
286280
x_message, x_0_gating = self.so2_conv_1.gemms_from_packed(

src/fairchem/core/models/uma/nn/execution_backends.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from fairchem.core.models.uma.nn.unified_radial import UnifiedRadialMLP
1717

1818
if TYPE_CHECKING:
19+
from fairchem.core.models.uma.nn.activation import GateActivation
1920
from fairchem.core.units.mlip_unit.api.inference import (
2021
InferenceSettings,
2122
)
@@ -508,15 +509,26 @@ def fused_node_to_edge_conv1_pack(
508509
)
509510

510511
@staticmethod
511-
def fused_gate_activation(
512+
def gate_activation(
512513
x0_full: torch.Tensor,
513514
x1: torch.Tensor,
514515
x2: torch.Tensor,
515516
channels: int,
517+
activation: GateActivation,
516518
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
517-
from fairchem.core.models.uma.triton import packed_gate_op
518-
519-
return packed_gate_op(x0_full, x1, x2, channels)
519+
if (
520+
channels > 0
521+
and channels & (channels - 1) == 0
522+
and x0_full.dtype == torch.float32
523+
and x1.dtype == torch.float32
524+
and x2.dtype == torch.float32
525+
):
526+
from fairchem.core.models.uma.triton import packed_gate_op
527+
528+
return packed_gate_op(x0_full, x1, x2, channels)
529+
530+
gating, x0 = x0_full.split((2 * channels, 3 * channels), dim=-1)
531+
return activation.forward_m_blocks(gating, (x0, x1, x2))
520532

521533
@staticmethod
522534
def fused_conv2_inv_edge_to_node(

src/fairchem/core/models/uma/triton/fused_wigner.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,26 @@
4343

4444

4545
def _compact_l2_wigner(wigner: Tensor, num_edges: int) -> Tensor:
46-
if wigner.ndim != 2 or wigner.shape[1] != 35:
46+
if wigner.ndim != 2 or wigner.shape != (num_edges, 35):
4747
raise ValueError("wigner must have shape [E, 35]")
4848
return wigner.reshape(num_edges, 35)
4949

5050

51+
def _prepare_l2_wigner(wigner: Tensor, num_edges: int) -> Tensor:
52+
if wigner.ndim == 2 and wigner.shape == (num_edges, 35):
53+
return wigner
54+
if wigner.ndim == 3 and wigner.shape == (num_edges, 9, 9):
55+
return torch.cat(
56+
(
57+
wigner[:, :1, :1].flatten(1),
58+
wigner[:, 1:4, 1:4].flatten(1),
59+
wigner[:, 4:9, 4:9].flatten(1),
60+
),
61+
dim=1,
62+
)
63+
raise ValueError("wigner must have shape [E, 35] or [E, 9, 9]")
64+
65+
5166
# =============================================================================
5267
# Producer-side fused wigner -> conv1 (emits conv1's GEMM-ready packed buffers)
5368
# =============================================================================
@@ -268,13 +283,14 @@ def wigner_conv1_fused_op(
268283
Args:
269284
x_full: Node features [N, 9, C] (L-major).
270285
edge_index: Edge indices [2, E].
271-
wigner: Compact Wigner blocks [E, 35].
286+
wigner: Compact Wigner blocks [E, 35] or a dense matrix [E, 9, 9].
272287
radial: Per-layer conv1 radial embedding [E, 6*2C] (rad_func applied).
273288
C: sphere_channels.
274289
275290
Returns:
276291
(m0, m1, m2) GEMM-ready packed buffers.
277292
"""
293+
wigner = _prepare_l2_wigner(wigner, edge_index.shape[1])
278294
return WignerConv1FusedFunction.apply(x_full, edge_index, wigner, radial, C)
279295

280296

@@ -534,12 +550,13 @@ def wigner_inv_conv2_fused_op(
534550
g0: conv2 fc_m0 output [E, 3C] (rows M0,M1,M2).
535551
g1: conv2 m=1 block-GEMM output [E, 4C] (rows M3,M4,M5,M6).
536552
g2: conv2 m=2 block-GEMM output [E, 2C] (rows M7,M8).
537-
wigner: Compact inverse Wigner blocks [E, 35].
553+
wigner: Compact inverse Wigner blocks [E, 35] or a dense matrix [E, 9, 9].
538554
C: sphere_channels.
539555
540556
Returns:
541557
x_rotated [E, 9, C] (L-major).
542558
"""
559+
wigner = _prepare_l2_wigner(wigner, g0.shape[0])
543560
return WignerInvConv2FusedFunction.apply(g0, g1, g2, wigner, C)
544561

545562

@@ -621,6 +638,7 @@ def wigner_inv_conv2_scatter_op(
621638
Uses direct atomic accumulation by default and preserves PyTorch's
622639
deterministic-algorithm behavior through a materialized fallback.
623640
"""
641+
wigner = _prepare_l2_wigner(wigner, g0.shape[0])
624642
return WignerInvConv2ScatterFunction.apply(
625643
g0, g1, g2, wigner, scatter_target, num_nodes, C
626644
)

tests/core/models/uma/uma_fast/test_execution_backends.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from fairchem.core.datasets.ase_datasets import AseDBDataset
2727
from fairchem.core.datasets.atomic_data import AtomicData
2828
from fairchem.core.datasets.collaters.simple_collater import data_list_collater
29+
from fairchem.core.models.uma.nn.activation import GateActivation
2930
from fairchem.core.models.uma.nn.execution_backends import UMASFastGPUBackend
3031
from fairchem.core.models.uma.triton.constants import M_TO_L_GATHER_IDX
3132
from fairchem.core.models.uma.triton.node_to_edge_wigner_permute import (
@@ -142,6 +143,65 @@ def test_umas_fast_gpu_validation_accepts_hessian_loop():
142143
UMASFastGPUBackend.validate(lmax=2, mmax=2, settings=settings)
143144

144145

146+
@pytest.mark.gpu()
147+
@pytest.mark.parametrize(
148+
("channels", "dtype"),
149+
[(96, torch.float32), (128, torch.bfloat16)],
150+
)
151+
def test_umas_fast_gpu_gate_activation_fallback(channels, dtype):
152+
torch.manual_seed(42)
153+
num_edges = 16
154+
inputs = (
155+
torch.randn(num_edges, 5 * channels, device="cuda", dtype=dtype),
156+
torch.randn(num_edges, 4 * channels, device="cuda", dtype=dtype),
157+
torch.randn(num_edges, 2 * channels, device="cuda", dtype=dtype),
158+
)
159+
activation = GateActivation(2, 2, channels, m_prime=True).cuda()
160+
gating, x0 = inputs[0].split((2 * channels, 3 * channels), dim=-1)
161+
expected = activation.forward_m_blocks(gating, (x0, inputs[1], inputs[2]))
162+
actual = UMASFastGPUBackend.gate_activation(*inputs, channels, activation)
163+
164+
for actual_block, expected_block in zip(actual, expected, strict=True):
165+
torch.testing.assert_close(actual_block, expected_block, rtol=0, atol=0)
166+
167+
168+
@pytest.mark.gpu()
169+
def test_umas_fast_gpu_gate_activation_fallback_dynamic_compile(
170+
compile_reset_state,
171+
):
172+
torch.manual_seed(42)
173+
channels = 96
174+
activation = GateActivation(2, 2, channels, m_prime=True).cuda()
175+
176+
def fn(x0_full, x1, x2):
177+
return UMASFastGPUBackend.gate_activation(x0_full, x1, x2, channels, activation)
178+
179+
compiled = torch.compile(fn, fullgraph=True, dynamic=True)
180+
for num_edges in (17, 31):
181+
inputs = (
182+
torch.randn(num_edges, 5 * channels, device="cuda", requires_grad=True),
183+
torch.randn(num_edges, 4 * channels, device="cuda", requires_grad=True),
184+
torch.randn(num_edges, 2 * channels, device="cuda", requires_grad=True),
185+
)
186+
reference_inputs = tuple(
187+
value.detach().clone().requires_grad_() for value in inputs
188+
)
189+
actual = compiled(*inputs)
190+
expected = fn(*reference_inputs)
191+
grad_outputs = tuple(torch.randn_like(value) for value in actual)
192+
actual_grads = torch.autograd.grad(actual, inputs, grad_outputs)
193+
expected_grads = torch.autograd.grad(expected, reference_inputs, grad_outputs)
194+
195+
for actual_block, expected_block in zip(actual, expected, strict=True):
196+
torch.testing.assert_close(
197+
actual_block, expected_block, rtol=1e-6, atol=1e-6
198+
)
199+
for actual_grad, expected_grad in zip(
200+
actual_grads, expected_grads, strict=True
201+
):
202+
torch.testing.assert_close(actual_grad, expected_grad, rtol=1e-6, atol=1e-6)
203+
204+
145205
@pytest.mark.gpu()
146206
def test_compact_edge_degree_matches_dense():
147207
torch.manual_seed(42)

tests/core/models/uma/uma_fast/test_fused_edgewise.py

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,69 @@ def test_wigner_inv_conv2_fused_matches_pytorch(sphere_channels):
217217
), f"Max diff: {(ref_out - triton_out).abs().max()}"
218218

219219

220+
@pytest.mark.gpu()
221+
def test_exported_fused_ops_accept_dense_wigner(torch_deterministic):
222+
torch.manual_seed(42)
223+
num_nodes, num_edges, channels = 8, 16, 128
224+
edge_index = torch.randint(0, num_nodes, (2, num_edges), device="cuda")
225+
dense = _create_block_diagonal_wigner(num_edges, "cuda").requires_grad_()
226+
compact = _compact_l2_wigner(dense.detach()).requires_grad_()
227+
x_dense = torch.randn(num_nodes, 9, channels, device="cuda", requires_grad=True)
228+
x_compact = x_dense.detach().clone().requires_grad_()
229+
radial_dense = torch.randn(
230+
num_edges, 12 * channels, device="cuda", requires_grad=True
231+
)
232+
radial_compact = radial_dense.detach().clone().requires_grad_()
233+
234+
dense_outputs = wigner_conv1_fused_op(
235+
x_dense, edge_index, dense, radial_dense, channels
236+
)
237+
compact_outputs = wigner_conv1_fused_op(
238+
x_compact, edge_index, compact, radial_compact, channels
239+
)
240+
grad_outputs = tuple(torch.randn_like(value) for value in dense_outputs)
241+
dense_grads = torch.autograd.grad(
242+
dense_outputs, (x_dense, dense, radial_dense), grad_outputs
243+
)
244+
compact_grads = torch.autograd.grad(
245+
compact_outputs, (x_compact, compact, radial_compact), grad_outputs
246+
)
247+
248+
for actual, expected in zip(dense_outputs, compact_outputs, strict=True):
249+
torch.testing.assert_close(actual, expected, rtol=0, atol=0)
250+
torch.testing.assert_close(dense_grads[0], compact_grads[0], rtol=0, atol=0)
251+
torch.testing.assert_close(
252+
_compact_l2_wigner(dense_grads[1]), compact_grads[1], rtol=0, atol=0
253+
)
254+
torch.testing.assert_close(dense_grads[2], compact_grads[2], rtol=0, atol=0)
255+
256+
dense_inv = _create_block_diagonal_wigner(num_edges, "cuda").requires_grad_()
257+
compact_inv = _compact_l2_wigner(dense_inv.detach()).requires_grad_()
258+
dense_inputs = tuple(
259+
torch.randn(num_edges, multiple * channels, device="cuda", requires_grad=True)
260+
for multiple in (3, 4, 2)
261+
)
262+
compact_inputs = tuple(
263+
value.detach().clone().requires_grad_() for value in dense_inputs
264+
)
265+
dense_output = wigner_inv_conv2_fused_op(*dense_inputs, dense_inv, channels)
266+
compact_output = wigner_inv_conv2_fused_op(*compact_inputs, compact_inv, channels)
267+
grad_output = torch.randn_like(dense_output)
268+
dense_grads = torch.autograd.grad(
269+
dense_output, (*dense_inputs, dense_inv), grad_output
270+
)
271+
compact_grads = torch.autograd.grad(
272+
compact_output, (*compact_inputs, compact_inv), grad_output
273+
)
274+
275+
torch.testing.assert_close(dense_output, compact_output, rtol=0, atol=0)
276+
for actual, expected in zip(dense_grads[:-1], compact_grads[:-1], strict=True):
277+
torch.testing.assert_close(actual, expected, rtol=0, atol=0)
278+
torch.testing.assert_close(
279+
_compact_l2_wigner(dense_grads[-1]), compact_grads[-1], rtol=0, atol=0
280+
)
281+
282+
220283
@pytest.mark.gpu()
221284
@pytest.mark.parametrize("sphere_channels", [128, 256])
222285
def test_wigner_inv_conv2_scatter_matches_materialized(sphere_channels):
@@ -362,17 +425,19 @@ def test_wigner_conv1_fused_deterministic_backward(
362425

363426

364427
@pytest.mark.gpu()
365-
def test_wigner_conv1_fused_dynamic_compile(compile_reset_state):
428+
@pytest.mark.parametrize("dense_wigner", [False, True])
429+
def test_wigner_conv1_fused_dynamic_compile(compile_reset_state, dense_wigner):
366430
torch.manual_seed(42)
367431
num_nodes, channels = 8, 128
368432
compiled = torch.compile(wigner_conv1_fused_op, fullgraph=True, dynamic=True)
369433

370434
for num_edges in (17, 31):
371435
x = torch.randn(num_nodes, 9, channels, device="cuda", requires_grad=True)
372436
edge_index = torch.randint(0, num_nodes, (2, num_edges), device="cuda")
373-
wigner = _compact_l2_wigner(
374-
_create_block_diagonal_wigner(num_edges, "cuda")
375-
).requires_grad_()
437+
wigner = _create_block_diagonal_wigner(num_edges, "cuda")
438+
if not dense_wigner:
439+
wigner = _compact_l2_wigner(wigner)
440+
wigner.requires_grad_()
376441
radial = torch.randn(
377442
num_edges, 12 * channels, device="cuda", requires_grad=True
378443
)

0 commit comments

Comments
 (0)