Skip to content

Commit a3195b0

Browse files
wanghan-iapcmHan Wangpre-commit-ci[bot]
authored
feat: SFPG cross-rank completion — capabilities, ZBL bridging and spin+ZBL multi-rank (#5939)
Closes #5906. The DPA4/SeZM Source Freeze Propagation Gate computes each node's `eta_j = prod over outgoing edges of w(r_e)`; under MPI domain decomposition a rank only holds edges with owned destinations, so the src-keyed per-node partials are rank-incomplete and bridged models were single-rank only. This PR completes the gate across ranks and, as a prerequisite, promotes the export-time questions to atomic-model capabilities so compositions answer by aggregation. ## Phase 1 — capability aggregation (issue Task 4) - Split the conflated descriptor capability: `has_message_passing_across_ranks` (needs the per-block exchange; unconditionally true for SeZM) vs the new `supports_edge_parallel` (can run under domain decomposition). - Six capabilities on `BaseAtomicModel` with concrete defaults, descriptor delegation on `DPAtomicModel`, and any/all aggregation on `LinearEnergyAtomicModel`: `has_message_passing_across_ranks` (any), `supports_edge_parallel` (all), `dense_lower_supports_comm` (all), `uses_compact_edge_pairs` (any), `graph_edge_dtype` (float32 iff all children), `supports_graph_export` (all). - `forward_lower_graph_exportable_with_comm` hoisted from `EnergyModel` into `make_model` (one owner, next to the non-comm twin) so `LinearEnergyModel` compositions can export it. - The four `serialization.py` helpers now consult the atomic model — no `isinstance`-on-concrete-model checks, no `.descriptor` walks. Regression fixed: a linear composition of two DPA2 children now gets its with-comm artifact (previously denied by wrapper type). - Composition-safe reach-ins outside serialization: `.pt`-checkpoint eval no longer crashes on compositions (`ntypes` via the model API), `enable_compile` degrades gracefully, and pt_expt `get_standard_model` honors `bridging_method` like its dpmodel twin (`_compose_bridging` is the single composition owner). ## Phase 2 — SFPG cross-rank completion (issue Tasks 2 and 3) No new communication machinery: the fix is one extra invocation of the existing `deepmd_export::border_op_backward` + `border_op` pair (they are exact transposes, `R = B^T`) on an `(N, 2)` `[log_eta, zero_count]` tensor before the gate is applied — reverse-accumulate ghost partials into owners, then broadcast the completed values back. Zero C++ changes. - `border_op_backward` gains autograd (its gradient is `border_op`'s forward), so gate gradients cross ranks. - dpmodel: `compute_edge_src_gate` packs the partials through an optional `node_partial_exchange` hook; the dpmodel `_gate_partial_exchange` raises (single-process reference), the pt_expt subclass implements it on the border-op pair. - pt backend wired the same way. The red run of the new pt parity test demonstrated the issue's claim and more: pt's bridged parallel path did not just compute a silently wrong gate — it crashed outright (the ZBL injection indexed per-local types with extended ghost `src` indices); fixed by reading extended types. - Gates flipped: `supports_edge_parallel` is now `True` for bridged SeZM in both backends; bridged (and spin+ZBL) graph freezes embed the nested `forward_lower_with_comm.pt2`. ## Verification Anti-vacuous discipline throughout: every parity test places a sub-`r_outer` pair ACROSS the periodic/rank boundary (without it every cross-rank gate contribution is `log w = 0`), covers both bridging channels (hard-freeze `zero_count` at 0.4 Å, transition-zone `log_eta` at ~1 Å), and carries an identity-exchange ablation that must diverge. - Eager self-comm parity vs the folded reference at rtol/atol 1e-12 (energy, force, and force_mag for the spin variant), pt and pt_expt. - make_fx traces both border ops symbolically (21-input with-comm ABI unchanged); freeze embeds the nested artifact for ZBL and spin+ZBL compositions. - LAMMPS end-to-end on a Tesla T4: 2-rank vs 1-rank close-pair parity for ZBL (`pair_style deepmd`) and spin+ZBL (`pair_style deepspin`, incl. magnetic forces) — the spin+ZBL variant gets its first LAMMPS file. All 24 `*Dpa4Zbl*` C++ gtests pass (CPU + T4). - Variant-alignment coverage: ZBL empty-rank fail-fast twin, the first test of the DeepSpin owned-empty phantom path, charge-spin through `pair_style deepspin`, and default-CLI `dp freeze` resolution (nlist→graph auto-override + with-comm artifact) for both compositions. ## Known limitations 1. pt eager multi-rank bridging has no true-MPI pt test (no pt `.pth` LAMMPS ZBL fixtures exist); its parity rung is self-comm. 2. `graph_edge_dtype` composition rule (float32 iff ALL children) is conservative; fp64 is the universal ABI. 3. `supports_graph_export` keeps the hardcoded `"cuda"` probe inside pt_expt DPA1 (capability promoted; probe internals unchanged). 4. The `NativeSpinModelKind` marker-base check in `_needs_with_comm_artifact` remains (a cross-backend family test, not a concrete-type reach-through). 5. Model-deviation coverage stays absent for all dpa4 variants (pre-existing; Python `model_devi` has no spin support at all). 6. DeepPot vs DeepSpin empty-rank designs deliberately differ (fail-fast vs phantom-pad, PR #5485); both are now pinned per variant, not unified. 7. Found while testing, left for a separate fix: `source/api_c/include/deepmd.hpp` uses `&vec[0]` on possibly-empty vectors (~33 sites) — undefined behavior that SIGABRTs under `_GLIBCXX_ASSERTIONS` before the empty-rank guard's message can fire (benign on non-hardened builds). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added multi-rank inference for bridged DPA4/SeZM models, including native-spin and ZBL configurations. * Improved graph export detection, metadata, edge precision, and communication-aware export. * Added atomic-output-only inference for statistics workflows. * Standard model loading now preserves bridging configurations. * **Bug Fixes** * Improved handling of atom types, ghost atoms, empty MPI ranks, charge-spin inputs, and cross-rank calculations. * **Documentation** * Updated DPA4 and native-spin documentation for expanded multi-rank and graph export support. * **Tests** * Added regression coverage for MPI parity, graph exports, bridging, charge-spin behavior, and capability reporting. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Han Wang <wang_han@iapcm.ac.cn> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent c0c1f0c commit a3195b0

46 files changed

Lines changed: 3653 additions & 772 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

deepmd/dpmodel/atomic_model/base_atomic_model.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,54 @@ def supports_native_spin(self) -> bool:
190190
"""
191191
return False
192192

193+
def has_message_passing_across_ranks(self) -> bool:
194+
"""Whether multi-rank inference needs a cross-rank ghost exchange.
195+
196+
Generic capability (concrete default ``False``): the export layer
197+
consults it instead of reaching for a descriptor, so the answer
198+
stays correct for descriptor-less models and compositions.
199+
"""
200+
return False
201+
202+
def supports_edge_parallel(self) -> bool:
203+
"""Whether this atomic model can run under MPI domain decomposition.
204+
205+
Default ``True``; a model folding state no single rank observes
206+
(e.g. SFPG bridging before its exchange lands) overrides to False.
207+
"""
208+
return True
209+
210+
def dense_lower_supports_comm(self) -> bool:
211+
"""Whether the DENSE (nlist) lower implements comm_dict exchange.
212+
213+
Default ``True`` — dense comm is the production multi-rank path for
214+
dpa2/dpa3; DPA4's dense adapter raises on comm_dict and overrides
215+
via its descriptor.
216+
"""
217+
return True
218+
219+
def uses_compact_edge_pairs(self) -> bool:
220+
"""Whether the graph lower emits compact ``center_edge_pairs``
221+
(drives the torch>=2.6 unbacked-SymInt export guard).
222+
"""
223+
return False
224+
225+
def graph_edge_dtype(self) -> str:
226+
"""Edge-geometry dtype the graph deployment artifact accepts.
227+
228+
``"float64"`` is the model-agnostic ABI; geometrically compressed
229+
float32 descriptors override to ``"float32"``.
230+
"""
231+
return "float64"
232+
233+
def supports_graph_export(self) -> bool:
234+
"""Whether an exportable graph-lower implementation exists.
235+
236+
A compressed descriptor without its fused opaque operator cannot be
237+
traced through the reference tabulation kernel.
238+
"""
239+
return True
240+
193241
def get_default_fparam(self) -> list[float] | None:
194242
"""Get the default frame parameters."""
195243
return None

deepmd/dpmodel/atomic_model/dp_atomic_model.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,30 @@ def uses_graph_lower(self) -> bool:
167167
"""Delegates to this model's own descriptor."""
168168
return bool(self.descriptor.uses_graph_lower())
169169

170+
def has_message_passing_across_ranks(self) -> bool:
171+
"""Delegates to this model's own descriptor."""
172+
return bool(self.descriptor.has_message_passing_across_ranks())
173+
174+
def supports_edge_parallel(self) -> bool:
175+
"""Delegates to this model's own descriptor."""
176+
return bool(self.descriptor.supports_edge_parallel())
177+
178+
def dense_lower_supports_comm(self) -> bool:
179+
"""Delegates to this model's own descriptor."""
180+
return bool(self.descriptor.dense_lower_supports_comm())
181+
182+
def uses_compact_edge_pairs(self) -> bool:
183+
"""Delegates to this model's own descriptor."""
184+
return bool(self.descriptor.uses_compact_edge_pairs())
185+
186+
def graph_edge_dtype(self) -> str:
187+
"""Delegates to this model's own descriptor."""
188+
return str(self.descriptor.graph_edge_dtype())
189+
190+
def supports_graph_export(self) -> bool:
191+
"""Delegates to this model's own descriptor."""
192+
return bool(self.descriptor.supports_graph_export())
193+
170194
def supports_native_spin(self) -> bool:
171195
"""Delegates to this model's own descriptor (cached at construction)."""
172196
return self._supports_native_spin

deepmd/dpmodel/atomic_model/linear_atomic_model.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,31 @@ def has_message_passing(self) -> bool:
182182
"""Returns whether the atomic model has message passing."""
183183
return any(model.has_message_passing() for model in self.models)
184184

185+
def has_message_passing_across_ranks(self) -> bool:
186+
"""ANY child needing the exchange makes the composition need it."""
187+
return any(m.has_message_passing_across_ranks() for m in self.models)
188+
189+
def supports_edge_parallel(self) -> bool:
190+
"""EVERY child must tolerate decomposition; one veto vetoes all."""
191+
return all(m.supports_edge_parallel() for m in self.models)
192+
193+
def dense_lower_supports_comm(self) -> bool:
194+
"""The shared dense lower is only comm-capable if every child's is."""
195+
return all(m.dense_lower_supports_comm() for m in self.models)
196+
197+
def uses_compact_edge_pairs(self) -> bool:
198+
"""The export guard fires if ANY child emits compact pairs."""
199+
return any(m.uses_compact_edge_pairs() for m in self.models)
200+
201+
def graph_edge_dtype(self) -> str:
202+
"""One shared edge tensor: float32 only if EVERY child accepts it."""
203+
dtypes = {m.graph_edge_dtype() for m in self.models}
204+
return "float32" if dtypes == {"float32"} else "float64"
205+
206+
def supports_graph_export(self) -> bool:
207+
"""All children trace into one artifact; each must be exportable."""
208+
return all(m.supports_graph_export() for m in self.models)
209+
185210
def need_sorted_nlist_for_lower(self) -> bool:
186211
"""Returns whether the atomic model needs sorted nlist when using `forward_lower`."""
187212
return True

deepmd/dpmodel/descriptor/dpa1.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,21 @@ def has_message_passing_across_ranks(self) -> bool:
542542
"""
543543
return False
544544

545+
def graph_edge_dtype(self) -> str:
546+
"""float32 edges iff geometric compression runs float32 statistics.
547+
548+
Compressed DPA1 evaluates both descriptor directions in the
549+
statistics dtype and accepts float32 geometry directly.
550+
"""
551+
mean = getattr(self.se_atten, "mean", None)
552+
if (
553+
self.geo_compress
554+
and mean is not None
555+
and str(mean.dtype).endswith("float32")
556+
):
557+
return "float32"
558+
return "float64"
559+
545560
def need_sorted_nlist_for_lower(self) -> bool:
546561
"""Returns whether the descriptor needs sorted nlist when using `forward_lower`."""
547562
return self.se_atten.need_sorted_nlist_for_lower()

deepmd/dpmodel/descriptor/dpa2.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,7 @@ def init_subclass_params(sub_data: dict | Any, sub_class: type) -> Any:
604604
self.trainable = trainable
605605
self.add_tebd_to_repinit_out = add_tebd_to_repinit_out
606606
self.compress = False
607+
self.geo_compress = False
607608
# graph-native lower opt-out flag (mirrors DescrptDPA1); not
608609
# serialized, re-derived structurally at construction/deserialization.
609610
self._graph_lower_disabled = False

deepmd/dpmodel/descriptor/dpa4.py

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
annotations,
3636
)
3737

38+
import functools
3839
import math
3940
from typing import (
4041
TYPE_CHECKING,
@@ -1541,6 +1542,14 @@ def _run_graph(
15411542
type_ebed, spin, atype_flat, n_nodes=n_nodes
15421543
)
15431544

1545+
# Cross-rank SFPG completion (issue #5906): only a bridged model
1546+
# under domain decomposition needs it -- the gate's src-keyed
1547+
# per-node partials are rank-incomplete then.
1548+
node_partial_exchange = None
1549+
if comm_dict is not None and self.bridging_switch is not None:
1550+
node_partial_exchange = functools.partial(
1551+
self._gate_partial_exchange, comm_dict=comm_dict
1552+
)
15441553
# === Step 3. Build edge cache once (sparse edges) ===
15451554
edge_cache = _edge_cache_from_arrays(
15461555
type_ebed=type_ebed,
@@ -1562,6 +1571,7 @@ def _run_graph(
15621571
random_gamma=self.random_gamma and self._in_training_mode(),
15631572
wigner_calc=self.wigner_calc,
15641573
build_wigner=self._need_full_wigner,
1574+
node_partial_exchange=node_partial_exchange,
15651575
)
15661576

15671577
ebed_dim_0 = self.node_init_dim # (node_init_lmax+1)^2
@@ -2196,6 +2206,40 @@ def _canonicalize_charge_spin(
21962206
raise ValueError("`charge_spin` first dimension must match nframes.")
21972207
return charge_spin
21982208

2209+
def _gate_partial_exchange(
2210+
self,
2211+
partials: Array,
2212+
comm_dict: dict[str, Array],
2213+
) -> Array:
2214+
"""Complete the SFPG per-node partials across ranks.
2215+
2216+
Reverse-accumulate ghost rows into their owners, then broadcast the
2217+
completed owner values back — the backend-specific pt_expt subclass
2218+
implements it on ``border_op_backward``/``border_op``; dpmodel is
2219+
the single-process reference and rejects comm outright.
2220+
2221+
Parameters
2222+
----------
2223+
partials
2224+
(n_nodes, 2) float tensor of [log_eta, zero_count] partials.
2225+
comm_dict
2226+
The border-exchange control tensors.
2227+
2228+
Returns
2229+
-------
2230+
Array
2231+
The globally completed (n_nodes, 2) tensor.
2232+
2233+
Raises
2234+
------
2235+
NotImplementedError
2236+
Always, in the dpmodel backend.
2237+
"""
2238+
raise NotImplementedError(
2239+
"Multi-rank SFPG partial exchange (comm_dict) is not supported "
2240+
"in the dpmodel backend."
2241+
)
2242+
21992243
def _block_comm(
22002244
self,
22012245
block_idx: int,
@@ -2278,20 +2322,27 @@ def has_message_passing(self) -> bool:
22782322
return True
22792323

22802324
def has_message_passing_across_ranks(self) -> bool:
2281-
"""Whether multi-rank inference needs cross-rank ghost exchange.
2325+
"""SeZM reads ghost-neighbour features at every interaction block.
22822326
2283-
SeZM reads ghost-neighbour features at every interaction block; the
2284-
GRAPH lower implements the exchange via per-block ``border_op``
2285-
(pt_expt ``exchange_ghost_features``). Source Freeze Propagation
2286-
bridging is excluded: its per-node gate folds a node's entire
2287-
outgoing-edge set, which a single rank cannot observe for ghost
2288-
owners, so bridging models fail fast on multi-rank instead.
2327+
The GRAPH lower implements the exchange via per-block ``border_op``
2328+
(pt_expt ``exchange_ghost_features``), so multi-rank inference always
2329+
needs the with-comm artifact. Whether multi-rank is POSSIBLE at all
2330+
is :meth:`supports_edge_parallel`.
22892331
22902332
The DENSE (nlist) lower remains comm-less — see
22912333
:meth:`dense_lower_supports_comm`; the freeze machinery consults both
22922334
so nlist-kind artifacts carry ``has_comm_artifact=False``.
22932335
"""
2294-
return self.bridging_switch is None
2336+
return True
2337+
2338+
def supports_edge_parallel(self) -> bool:
2339+
"""Bridging included: multi-rank is supported for every SeZM config.
2340+
2341+
The SFPG per-node partials are completed across ranks by
2342+
``_gate_partial_exchange`` (reverse-accumulate + broadcast) before
2343+
the gate is applied (issue #5906).
2344+
"""
2345+
return True
22952346

22962347
def dense_lower_supports_comm(self) -> bool:
22972348
"""The DPA4 dense (nlist) lower has no comm_dict implementation.
@@ -2316,8 +2367,10 @@ def uses_graph_lower(self) -> bool:
23162367
spin and charge_spin are threaded through ``call_graph`` like any
23172368
other per-node/per-frame input, and bridging is applied inside
23182369
the shared ``_run_graph`` forward with no extra threading (it
2319-
reads ``self.bridging_switch`` directly). Bridging models still
2320-
fail multi-rank fast via ``has_message_passing_across_ranks``.
2370+
reads ``self.bridging_switch`` directly). Bridging models are
2371+
multi-rank capable too: their SFPG per-node partials are
2372+
completed across ranks by ``_gate_partial_exchange`` before the
2373+
gate is applied (issue #5906).
23212374
"""
23222375
return not self._graph_lower_disabled
23232376

deepmd/dpmodel/descriptor/dpa4_nn/edge_cache.py

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ def compute_edge_src_gate(
131131
n_nodes: int,
132132
bridging_switch: Callable[[Any], Any],
133133
edge_keep_f: Any = None,
134+
node_partial_exchange: Callable[[Any], Any] | None = None,
134135
) -> Any:
135136
"""
136137
Compute the per-edge source gate for SFPG from edge lengths.
@@ -182,6 +183,14 @@ def compute_edge_src_gate(
182183
Optional per-edge keep weights with shape (E, 1), with ``0`` on
183184
masked edges and ``1`` on kept edges. If provided, masked edges
184185
are rewritten to ``w = 1`` before the product reduction.
186+
node_partial_exchange
187+
Optional cross-rank completion hook for the per-node partials
188+
(issue #5906). Receives the ``(n_nodes, 2)`` float tensor
189+
``[log_eta, zero_count]`` and returns the globally completed one
190+
(reverse-accumulate ghost rows into owners, then broadcast the
191+
completed owner values back onto ghosts). ``None`` (the default)
192+
is the single-process path where the local partials are already
193+
complete.
185194
186195
Returns
187196
-------
@@ -209,18 +218,33 @@ def compute_edge_src_gate(
209218
log_eta = xp_add_at(
210219
xp.zeros((n_nodes,), dtype=edge_w.dtype, device=device), src, log_safe
211220
)
212-
eta_nonzero_path = xp.exp(log_eta)
213221

214222
# === Step 3. Exact-zero indicator per source node ===
215-
# ``scatter_add`` over an ``int64`` cast of the zero mask counts how
216-
# many frozen edges each source node owns. A strictly positive count
217-
# means the product is 0 by the hard-freeze rule.
223+
# ``scatter_add`` over the zero mask counts how many frozen edges each
224+
# source node owns. A strictly positive count means the product is 0 by
225+
# the hard-freeze rule. Float count (values are small integers, exact
226+
# in fp) so both partials ride ONE border-exchange tensor when
227+
# completing across ranks.
218228
zero_count = xp_add_at(
219-
xp.zeros((n_nodes,), dtype=xp.int64, device=device),
229+
xp.zeros((n_nodes,), dtype=edge_w.dtype, device=device),
220230
src,
221-
xp.astype(is_zero, xp.int64),
231+
xp.astype(is_zero, edge_w.dtype),
222232
)
223-
any_zero = zero_count > 0
233+
234+
# === Step 3b. Cross-rank completion of the per-node partials ===
235+
# A rank only holds edges whose dst is owned, so the src-keyed sums
236+
# above are PARTIAL for every node under domain decomposition. The hook
237+
# (reverse-accumulate ghost->owner, then forward-broadcast owner->ghost)
238+
# completes them; log-products are additive and each edge lives on
239+
# exactly one rank, so nothing double-counts (issue #5906).
240+
if node_partial_exchange is not None:
241+
packed = xp.stack([log_eta, zero_count], axis=-1) # (n_nodes, 2)
242+
packed = node_partial_exchange(packed)
243+
log_eta = packed[..., 0]
244+
zero_count = packed[..., 1]
245+
246+
eta_nonzero_path = xp.exp(log_eta)
247+
any_zero = zero_count > 0.5
224248

225249
# === Step 4. Combine and broadcast back to edges via source ===
226250
eta = xp.where(any_zero, xp.zeros_like(eta_nonzero_path), eta_nonzero_path)
@@ -244,6 +268,7 @@ def _edge_cache_from_arrays(
244268
wigner_calc: WignerCalculatorFn,
245269
build_wigner: bool = True,
246270
gamma: Any = None,
271+
node_partial_exchange: Callable[[Any], Any] | None = None,
247272
) -> EdgeCache:
248273
"""
249274
Build the global edge cache from a sparse edge list.
@@ -295,6 +320,10 @@ def _edge_cache_from_arrays(
295320
``random_gamma`` is True. When None, drawn with the backend's RNG
296321
(:func:`~deepmd.dpmodel.array_api.xp_uniform`) uniformly in
297322
``[0, 2*pi)``; callers may inject angles to pin a draw.
323+
node_partial_exchange
324+
Optional cross-rank completion hook forwarded to
325+
:func:`compute_edge_src_gate` (issue #5906); only meaningful when
326+
``bridging_switch`` is provided.
298327
299328
Returns
300329
-------
@@ -358,6 +387,7 @@ def _edge_cache_from_arrays(
358387
n_nodes=n_nodes,
359388
bridging_switch=bridging_switch,
360389
edge_keep_f=edge_keep_f,
390+
node_partial_exchange=node_partial_exchange,
361391
)
362392

363393
return _finalize_edge_cache(

deepmd/dpmodel/descriptor/hybrid.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,24 @@ def has_message_passing_across_ranks(self) -> bool:
212212
descrpt.has_message_passing_across_ranks() for descrpt in self.descrpt_list
213213
)
214214

215+
def supports_edge_parallel(self) -> bool:
216+
"""Returns whether the hybrid can run under domain decomposition.
217+
218+
A veto by any child vetoes the whole concatenation: the hybrid
219+
output contains that child's block, so the composite is only as
220+
parallel-capable as its least capable member.
221+
"""
222+
return all(descrpt.supports_edge_parallel() for descrpt in self.descrpt_list)
223+
224+
def dense_lower_supports_comm(self) -> bool:
225+
"""Returns whether every child's DENSE lower implements comm_dict.
226+
227+
ALL rather than ANY: the dense with-comm trace passes ``comm_dict``
228+
to every child, so one child whose dense adapter raises on it (DPA4)
229+
makes the whole hybrid's dense comm path non-viable.
230+
"""
231+
return all(descrpt.dense_lower_supports_comm() for descrpt in self.descrpt_list)
232+
215233
def need_sorted_nlist_for_lower(self) -> bool:
216234
"""Returns whether the descriptor needs sorted nlist when using `forward_lower`."""
217235
return True

0 commit comments

Comments
 (0)