@@ -1199,10 +1199,10 @@ def __init__(
11991199 # Accelerated backends may replace the distance-to-radial chain and the
12001200 # packed Wigner-D construction. The array-API reference leaves these
12011201 # hooks unbound and always retains the dense Wigner matrices.
1202- self ._cuda_radial_fn = None
1203- self ._cuda_wigner_fn = None
1204- self ._wigner_free_conv = False
1205- self ._packed_wigner_train = False
1202+ self .cuda_infer_l_1_radial = None
1203+ self .cuda_infer_l_1_wigner = None
1204+ self .cuda_infer_l_2_covers_all_blocks = False
1205+ self .cuda_train_covers_all_blocks = False
12061206
12071207 # === Optional descriptor-level attention residuals ===
12081208 self .final_block_attn_res = None
@@ -1542,9 +1542,31 @@ def _run_graph(
15421542 graph = apply_pair_exclusion (graph , atype_flat , self .emask )
15431543 if n_out_nodes is None :
15441544 n_out_nodes = atype_flat .shape [0 ]
1545+ packed_wigner_graph = self .prepare_packed_wigner_graph (
1546+ graph , atype_flat .shape [0 ]
1547+ )
1548+ packed_wigner = packed_wigner_graph is not None
1549+ if packed_wigner_graph is not None :
1550+ graph = packed_wigner_graph
15451551 edge_index = graph .edge_index
15461552 edge_vec = graph .edge_vec
15471553 edge_mask = graph .edge_mask
1554+ # Graph-owned endpoint orderings remain aligned with the edge payload
1555+ # and are shared by every segmented consumer through the edge cache.
1556+ graph_csr_cache = None
1557+ if all (
1558+ value is not None
1559+ for value in (
1560+ graph .destination_order ,
1561+ graph .destination_row_ptr ,
1562+ graph .source_order ,
1563+ graph .source_row_ptr ,
1564+ )
1565+ ):
1566+ graph_csr_cache = {
1567+ "dst" : (graph .destination_order , graph .destination_row_ptr ),
1568+ "src" : (graph .source_order , graph .source_row_ptr ),
1569+ }
15481570
15491571 xp = array_api_compat .array_namespace (edge_vec )
15501572 device = array_api_compat .device (edge_vec )
@@ -1592,14 +1614,18 @@ def _run_graph(
15921614 bridging_switch = self .bridging_switch ,
15931615 edge_envelope = self .edge_envelope ,
15941616 radial_basis = self .radial_basis ,
1595- fused_radial = None if training else self ._cuda_radial_fn ,
1596- fused_wigner = None if training else self ._cuda_wigner_fn ,
1617+ fused_radial = None if training else self .cuda_infer_l_1_radial ,
1618+ fused_wigner = None if training else self .cuda_infer_l_1_wigner ,
15971619 # Random local-Z roll is a training-only augmentation; the model
15981620 # is roll-equivariant, so inference fixes gamma.
15991621 random_gamma = self .random_gamma and training ,
16001622 wigner_calc = self .wigner_calc ,
1601- build_wigner = self ._build_full_wigner (),
1623+ build_wigner = self ._build_full_wigner () or packed_wigner ,
16021624 node_partial_exchange = node_partial_exchange ,
1625+ packed_wigner = packed_wigner ,
1626+ destinations_sorted = graph .destination_sorted ,
1627+ packed_wigner_fn = self .build_packed_wigner ,
1628+ csr_cache = graph_csr_cache ,
16031629 )
16041630
16051631 ebed_dim_0 = self .node_init_dim # (node_init_lmax+1)^2
@@ -1719,6 +1745,9 @@ def _run_graph(
17191745 edge_cache = edge_cache_to_dtype (
17201746 edge_cache , get_xp_precision (xp , self .precision )
17211747 )
1748+ edge_cache .cute_infer_so2_metadata = self .prepare_cute_infer_so2_metadata (
1749+ edge_cache , n_nodes
1750+ )
17221751 x = self ._forward_blocks (
17231752 x , edge_cache , rad_feat_per_block , comm_dict = comm_dict
17241753 )
@@ -1897,10 +1926,30 @@ def _apply_readout(self, x: Array, n_rows: int) -> Array:
18971926 )
18981927 for layer in self .readout_pre_layers :
18991928 x_ro = x_ro + layer (x_ro )
1929+ if not self .readout_pre_layers and self .so3_readout != "none" :
1930+ accelerated = self .run_cute_infer_readout (x_ro )
1931+ if accelerated is not None :
1932+ return xp .reshape (accelerated , (n_rows , 1 , 1 , self .channels ))
19001933 if self .so3_readout == "none" :
19011934 return (x_ro + self .output_ffn (x_ro ))[:, 0 :1 , :, :]
19021935 return x_ro [:, 0 :1 , :, :] + self .output_ffn .call_scalar (x_ro )
19031936
1937+ def run_cute_infer_readout (self , ffn_in : Array ) -> Array | None :
1938+ """Run the CuTe readout when its exact inference contract matches.
1939+
1940+ Parameters
1941+ ----------
1942+ ffn_in : Array
1943+ Equivariant readout input with shape ``(N, D, 1, C)``.
1944+
1945+ Returns
1946+ -------
1947+ Array or None
1948+ Residual-inclusive scalar output with shape ``(N, C)``, or ``None``
1949+ when the backend has no eligible implementation.
1950+ """
1951+ return None
1952+
19041953 def _edge_quaternion (self , edge_cache : EdgeCache ) -> Array :
19051954 """
19061955 Return the cached global->local edge quaternion, rebuilding if absent.
@@ -1931,8 +1980,73 @@ def _build_full_wigner(self) -> bool:
19311980 if not self ._need_full_wigner :
19321981 return False
19331982 if self ._in_training_mode ():
1934- return not self ._packed_wigner_train
1935- return not self ._wigner_free_conv
1983+ return not self .cuda_train_covers_all_blocks
1984+ return not self .cuda_infer_l_2_covers_all_blocks
1985+
1986+ def prepare_packed_wigner_graph (
1987+ self ,
1988+ graph : NeighborGraph ,
1989+ n_nodes : int ,
1990+ ) -> NeighborGraph | None :
1991+ """Prepare an edge graph for backend packed-Wigner storage.
1992+
1993+ Parameters
1994+ ----------
1995+ graph : NeighborGraph
1996+ Edge graph supplied to the descriptor.
1997+ n_nodes : int
1998+ Number of nodes addressed by the graph.
1999+
2000+ Returns
2001+ -------
2002+ NeighborGraph or None
2003+ A graph satisfying the packed layout contract, or ``None`` when
2004+ the backend does not select that representation.
2005+ """
2006+ return None
2007+
2008+ def build_packed_wigner (
2009+ self ,
2010+ edge_quat : Array ,
2011+ wigner_calc : Any ,
2012+ ) -> Array | None :
2013+ """Build backend-specific packed Wigner storage when available.
2014+
2015+ Parameters
2016+ ----------
2017+ edge_quat : Array
2018+ Global-to-local edge quaternions with shape ``(E, 4)``.
2019+ wigner_calc : Any
2020+ Wigner calculator carrying the degree and basis convention.
2021+
2022+ Returns
2023+ -------
2024+ Array or None
2025+ Packed per-edge Wigner storage, or ``None`` to retain dense storage.
2026+ """
2027+ return None
2028+
2029+ def prepare_cute_infer_so2_metadata (
2030+ self ,
2031+ edge_cache : EdgeCache ,
2032+ n_nodes : int ,
2033+ ) -> tuple [Array , Array , Array ] | None :
2034+ """Build the edge metadata consumed by the CuTe SO2 implementation.
2035+
2036+ Parameters
2037+ ----------
2038+ edge_cache : EdgeCache
2039+ Per-forward cache carrying the destination-major edge payload.
2040+ n_nodes : int
2041+ Number of nodes addressed by the edge payload.
2042+
2043+ Returns
2044+ -------
2045+ tuple[Array, Array, Array] or None
2046+ Destination row pointers, source order, and source row pointers, or
2047+ ``None`` when the backend does not select CuTe SO2.
2048+ """
2049+ return None
19362050
19372051 def _shared_wigner_runs (
19382052 self ,
@@ -1981,6 +2095,8 @@ def _build_gie_zonal_coupling(
19812095 the blocks are skipped (all-Cartesian model) the full coupling is
19822096 reconstructed from the edge quaternion via the m=0-only path.
19832097 """
2098+ if edge_cache .D_packed is not None :
2099+ return self .build_cute_infer_zonal_coupling (edge_cache )
19842100 if edge_cache .Dt_full is None :
19852101 calc = self .gie_zonal_wigner_calc or self .wigner_calc
19862102 shared = self ._shared_wigner_runs (edge_cache , calc .lmax )
@@ -2008,6 +2124,28 @@ def _build_gie_zonal_coupling(
20082124 )
20092125 return xp .concat ([mp_coupling , extra_coupling ], axis = 1 )
20102126
2127+ def build_cute_infer_zonal_coupling (self , edge_cache : EdgeCache ) -> Array :
2128+ """Extract the GIE zonal coupling from packed Wigner storage.
2129+
2130+ Parameters
2131+ ----------
2132+ edge_cache : EdgeCache
2133+ Per-forward cache carrying backend packed Wigner storage.
2134+
2135+ Returns
2136+ -------
2137+ Array
2138+ Zonal coupling with shape ``(E, D_node - 1)``.
2139+
2140+ Raises
2141+ ------
2142+ NotImplementedError
2143+ If a backend supplies packed Wigner storage without this extractor.
2144+ """
2145+ raise NotImplementedError (
2146+ "packed Wigner storage requires a backend zonal-coupling implementation"
2147+ )
2148+
20112149 def _apply_charge_spin_embedding (
20122150 self ,
20132151 type_ebed : Array ,
0 commit comments