Repository navigation
perf(pt): banded Wigner-D storage for the fused inference consumers #6071
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
cde9690
3c65fbd
7889643
93472fa
8ccbbf1
85d18f4
e29fbbf
3ac4f66
29d4be5
25bb484
b615b42
8605f6c
3291de4
813b37c
d2616eb
0eb7874
f9e5fbb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,10 @@ | |
| rearrange, | ||
| ) | ||
|
|
||
| from deepmd.pt_expt.kernels.utils import ( | ||
| use_packed_d_infer, | ||
| ) | ||
|
|
||
| from .utils import ( | ||
| get_promoted_dtype, | ||
| nvtx_range, | ||
|
|
@@ -93,6 +97,12 @@ class EdgeFeatureCache(NamedTuple): | |
| edge_quat | ||
| Per-edge global-to-local quaternion actually used to build ``D_full`` and | ||
| ``Dt_full`` with shape (E, 4). Includes the optional random local-Z roll. | ||
| D_packed | ||
| Structural Wigner band with shape ``(E, 3*D-2)`` (the ``m = 0/-1/+1`` | ||
| rows per degree block, flash packed convention). Present only when the | ||
| banded inference storage is enabled and supported; then ``D_full`` and | ||
| ``Dt_full`` are ``None`` and the fused reduced-layout consumers address | ||
| this band instead. None otherwise. | ||
| D_to_m_cache | ||
| Lazy cache for projected D matrices keyed by a normalized | ||
| ``"lmax:mmax"`` identifier. | ||
|
|
@@ -120,6 +130,7 @@ class EdgeFeatureCache(NamedTuple): | |
| csr_cache: dict[str, tuple[torch.Tensor, torch.Tensor]] | None = None | ||
| edge_quat: torch.Tensor | None = None | ||
| node_gate: torch.Tensor | None = None | ||
| D_packed: torch.Tensor | None = None | ||
|
|
||
|
|
||
| def cached_edge_csr( | ||
|
|
@@ -328,6 +339,7 @@ def build_edge_cache( | |
| random_gamma: bool, | ||
| wigner_calc: WignerCalculatorFn, | ||
| build_wigner: bool = True, | ||
| packed_wigner: bool = False, | ||
| ) -> EdgeFeatureCache: | ||
| """ | ||
| Build the global edge cache from DeePMD padded neighbor list. | ||
|
|
@@ -435,13 +447,14 @@ def build_edge_cache( | |
|
|
||
| # === Step 6. Edge quaternion -> Wigner-D blocks === | ||
| with nvtx_range("wigner_d"): | ||
| D_full, Dt_full, edge_quat = _build_edge_wigner( | ||
| D_full, Dt_full, edge_quat, D_packed = _build_edge_wigner( | ||
| edge_vec=edge_vec, | ||
| edge_len=edge_len, | ||
| eps=eps, | ||
| random_gamma=random_gamma, | ||
| wigner_calc=wigner_calc, | ||
| build_full=build_wigner, | ||
| packed_wigner=packed_wigner, | ||
| ) # (E, D, D), (E, D, D), (E, 4) | ||
|
|
||
| edge_type_feat = build_edge_type_feat(type_ebed, src, dst) # (E, C) | ||
|
|
@@ -457,6 +470,7 @@ def build_edge_cache( | |
| D_full=D_full, | ||
| Dt_full=Dt_full, | ||
| edge_quat=edge_quat, | ||
| D_packed=D_packed, | ||
| deg_norm_floor=deg_norm_floor, | ||
| ) | ||
|
|
||
|
|
@@ -482,6 +496,7 @@ def build_edge_cache_from_edges( | |
| random_gamma: bool, | ||
| wigner_calc: WignerCalculatorFn, | ||
| build_wigner: bool = True, | ||
| packed_wigner: bool = False, | ||
| node_partial_exchange: Callable[[torch.Tensor], torch.Tensor] | None = None, | ||
| fused_radial: FusedRadialFn | None = None, | ||
| fused_wigner: WignerCalculatorFn | None = None, | ||
|
|
@@ -584,13 +599,14 @@ def build_edge_cache_from_edges( | |
|
|
||
| # === Step 4. Edge quaternion -> Wigner-D blocks === | ||
| with nvtx_range("wigner_d"): | ||
| D_full, Dt_full, edge_quat = _build_edge_wigner( | ||
| D_full, Dt_full, edge_quat, D_packed = _build_edge_wigner( | ||
| edge_vec=edge_vec, | ||
| edge_len=edge_len, | ||
| eps=eps, | ||
| random_gamma=random_gamma, | ||
| wigner_calc=fused_wigner if fused_wigner is not None else wigner_calc, | ||
| build_full=build_wigner, | ||
| packed_wigner=packed_wigner, | ||
| ) # (E, D, D), (E, D, D), (E, 4) | ||
|
|
||
| # === Step 5. Edge type features === | ||
|
|
@@ -634,6 +650,7 @@ def build_edge_cache_from_edges( | |
| D_full=D_full, | ||
| Dt_full=Dt_full, | ||
| edge_quat=edge_quat, | ||
| D_packed=D_packed, | ||
| deg_norm_floor=deg_norm_floor, | ||
| node_gate=node_gate, | ||
| ) | ||
|
|
@@ -647,7 +664,13 @@ def _build_edge_wigner( | |
| random_gamma: bool, | ||
| wigner_calc: WignerCalculatorFn, | ||
| build_full: bool = True, | ||
| ) -> tuple[torch.Tensor | None, torch.Tensor | None, torch.Tensor]: | ||
| packed_wigner: bool = False, | ||
| ) -> tuple[ | ||
| torch.Tensor | None, | ||
| torch.Tensor | None, | ||
| torch.Tensor, | ||
| torch.Tensor | None, | ||
| ]: | ||
| """ | ||
| Build packed Wigner-D blocks from edge vectors. | ||
|
|
||
|
|
@@ -669,13 +692,25 @@ def _build_edge_wigner( | |
| False (all message-passing blocks take the Cartesian path), only the | ||
| quaternion is returned and the blocks are ``None``; the geometric | ||
| initial embedding reconstructs the zonal coupling from the quaternion. | ||
| packed_wigner | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Non-blocking (docstring): the |
||
| Whether every selected rotation consumer addresses the banded | ||
| layout. The banded storage is only chosen when this holds; any | ||
| dense-layout consumer keeps the dense surfaces even with | ||
| ``DP_PACKED_D_INFER`` set. See | ||
| :func:`deepmd.pt.model.descriptor.sezm.block_supports_packed_wigner` | ||
| for the full exclusion set (the cuTile value path, the fused CUDA | ||
| convolution, the CUDA training value path, the plain SO2 reference | ||
| path, and the aggregation routes that rotate messages back through | ||
| ``Dt_full``; training is dense by construction). | ||
|
|
||
| Returns | ||
| ------- | ||
| tuple[torch.Tensor | None, torch.Tensor | None, torch.Tensor] | ||
| tuple[torch.Tensor | None, torch.Tensor | None, torch.Tensor, torch.Tensor | None] | ||
| Packed Wigner-D matrices ``(D_full, Dt_full)`` with shape ``(E, D, D)`` | ||
| (or ``None`` when ``build_full`` is False) and the quaternion used to | ||
| build them with shape ``(E, 4)``. | ||
| (or ``None`` when ``build_full`` is False), the quaternion used to | ||
| build them with shape ``(E, 4)``, and the banded storage ``D_packed`` | ||
| with shape ``(E, 3 * D - 2)`` when the banded layout was selected | ||
| (``None`` otherwise, including every training build). | ||
| """ | ||
| # === Step 1. Build edge-aligned quaternions === | ||
| edge_quat = build_edge_quaternion( | ||
|
|
@@ -695,9 +730,24 @@ def _build_edge_wigner( | |
|
|
||
| # === Step 3. Convert quaternions to packed Wigner-D blocks === | ||
| if not build_full: | ||
| return None, None, edge_quat | ||
| return None, None, edge_quat, None | ||
| # ``wigner_calc`` may arrive as the calculator instance (callable through | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Blocking: no test exercises the flag end to end. No test sets |
||
| # ``nn.Module.__call__``) or as a bound forward; accept both. | ||
| calc = getattr(wigner_calc, "__self__", wigner_calc) | ||
| if ( | ||
| use_packed_d_infer() | ||
| and packed_wigner | ||
| and callable(getattr(calc, "forward_band", None)) | ||
| and calc.band_supported() | ||
| ): | ||
| # Banded inference storage: the fused reduced-layout consumers (the | ||
| # rotate-mix operator and the flash aggregation) read only the three | ||
| # structural rows per degree block, so the dense surfaces are never | ||
| # materialized. Reference paths that need the dense layout must not | ||
| # enable the flag. | ||
| return None, None, edge_quat, calc.forward_band_fused(edge_quat) | ||
| D_full, Dt_full = wigner_calc(edge_quat) | ||
| return D_full, Dt_full, edge_quat | ||
| return D_full, Dt_full, edge_quat, None | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
|
|
||
|
|
||
| def _finalize_edge_cache( | ||
|
|
@@ -714,6 +764,7 @@ def _finalize_edge_cache( | |
| edge_quat: torch.Tensor, | ||
| deg_norm_floor: float, | ||
| node_gate: torch.Tensor | None = None, | ||
| D_packed: torch.Tensor | None = None, | ||
| ) -> EdgeFeatureCache: | ||
| """ | ||
| Assemble the shared `EdgeFeatureCache` layout. | ||
|
|
@@ -781,6 +832,7 @@ def _finalize_edge_cache( | |
| csr_cache={}, | ||
| edge_quat=edge_quat, | ||
| node_gate=node_gate, | ||
| D_packed=D_packed, | ||
| ) | ||
|
|
||
|
|
||
|
|
@@ -938,15 +990,19 @@ def edge_cache_to_dtype( | |
| _D_full = cache.D_full | ||
| _Dt_full = cache.Dt_full | ||
| _edge_quat = cache.edge_quat | ||
| _D_packed = cache.D_packed | ||
| D_full: torch.Tensor | None = None | ||
| Dt_full: torch.Tensor | None = None | ||
| edge_quat: torch.Tensor | None = None | ||
| D_packed: torch.Tensor | None = None | ||
| if _D_full is not None: | ||
| D_full = _D_full.to(dtype=dtype) | ||
| if _Dt_full is not None: | ||
| Dt_full = _Dt_full.to(dtype=dtype) | ||
| if _edge_quat is not None: | ||
| edge_quat = _edge_quat.to(dtype=dtype) | ||
| if _D_packed is not None: | ||
| D_packed = _D_packed.to(dtype=dtype) | ||
|
|
||
| # CSR views contain only integer topology. Preserve them across the dtype | ||
| # conversion so every accelerated consumer shares the per-step sort. | ||
|
|
@@ -966,4 +1022,5 @@ def edge_cache_to_dtype( | |
| csr_cache=None if cache.csr_cache is None else dict(cache.csr_cache), | ||
| edge_quat=edge_quat, | ||
| node_gate=cache.node_gate, | ||
| D_packed=D_packed, | ||
| ) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The band is built at the descriptor degree but indexed at each block's degree.
The cache holds a single
D_packedbuilt byself.wigner_calc, whose degree ismp_init_lmax = l_schedule[0]— the largest block. Each block's rotate-mix and flash kernels derive their per-edge stride and theirm = -1 / +1group boundaries fromDIM = (conv.lmax + 1) ** 2, i.e. from that block's own degree.l_scheduleis validated as non-increasing, so later blocks may have a smaller degree than the band's. In that case a block reads a band of width3 * D_block - 2out of a buffer whose per-edge stride is3 * D_max - 2, and itsm != 0groups start at the wrong offsets. The gate here decides per block but never compares the block's degree against the degree the band was built at.Trigger. A pyramid schedule such as
l_schedule: [3, 2], withmmax: 1,model.eval(), CUDA with Triton,DP_PACKED_D_INFERtruthy andDP_TRITON_INFER >= 1, and a frozen channel basis so the gate admits the band.Call chain.
sezm.py:1490/:1807passpacked_wigner=self._packed_wigner_ok()into the cache build ->edge_cache.py:737-748buildsD_packedatcalc.lmax == mp_init_lmax-> the block's_rotate_mix_impl/ flash launch withconv.lmax->so2_value_path.py:534DIM = (L + 1) ** 2and:544-545d_base = w_ptr + edge * (3 * DIM - 2), withoffm / offpatDIM + base - 1and2 * DIM + base - 2.Consequence. Wrong Wigner entries, hence wrong reduced messages and wrong energies and forces, silently, for every block whose degree is below
l_schedule[0]. Uniform schedules — what the production checkpoints use — are unaffected, and the flag is off by default.Evidence boundary. Established by reading the construction (band built at
mp_init_lmax), the gate (no degree comparison), the kernel offset formulas and the non-increasingl_schedulevalidation; no CUDA device was available to run the combination.Note on scope: the dense rotate-mix path already assumes
wignerwas built at the block's degree (so2.py:1791states "must be compatible with this block's lmax" as a precondition), so this is not introduced by the banded storage. The packed path inherits the assumption, and the admission gate added here is the natural place to enforce it, since it is the first code that decides whether a precomputed rotation surface may be shared across blocks.