Skip to content

Commit a266a3b

Browse files
author
Han Wang
committed
docs(dpa2): document pt_expt eager/training default-flip + legacy escape hatch
Review (iProzd behavior-change note, njzjz-bot P2): pt_expt now defaults graph-eligible DPA2 to the carry-all graph route in eager inference AND compiled training, not just --lower-kind graph freezing, changing existing pt_expt configs' numerics (negligible at non-binding sel; other backends unaffected). Document the change (release-note-worthy) and the supported legacy-dense escape hatches: - inference/eval: neighbor_graph_method="legacy"; - training: descriptor.disable_graph_lower() (flips uses_graph_lower() False, honored by both eager forward and the compiled lower -- same mechanism the spin model uses). A first-class training-config knob is a follow-up. Adds test_disable_graph_lower_escape_hatch pinning that the hatch makes the default (None) eager forward take the dense route bit-identically to explicit 'legacy' at a binding sel (where graph and dense genuinely differ).
1 parent 1265c66 commit a266a3b

2 files changed

Lines changed: 62 additions & 0 deletions

File tree

doc/model/dpa2.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,15 @@ dp --pt_expt freeze -o model.pt2 --lower-kind graph
102102

103103
As with DPA-1's graph path (see [Difference among different backends](train-se-atten.md#difference-among-different-backends)), the graph route considers all neighbors within the cutoff rather than a fixed, padded selection, so its numeric result can differ slightly (down to the AOTInductor floating-point noise floor at non-binding `sel`, larger if `sel` is binding) from the dense/`nlist` path.
104104

105+
:::{note}
106+
**Default route change in pt_expt (eager & training).** For a graph-eligible DPA-2 descriptor, the pt_expt backend now defaults to the carry-all graph route not only for `--lower-kind graph` freezing but also in **eager inference/evaluation and in (compiled) training** (`neighbor_graph_method=None` resolves to the graph). This changes the numerical behavior of existing pt_expt configurations relative to the dense neighbor-list route (by the amounts described above — negligible at non-binding `sel`). The other backends (dpmodel/PyTorch/Paddle/TensorFlow/JAX) are unaffected: they keep the dense route as their only path.
107+
108+
To retain the legacy dense route on pt_expt:
109+
110+
- **Inference / evaluation:** pass `neighbor_graph_method="legacy"` to `forward_common` / `call_common` (forces the dense neighbor-list path).
111+
- **Training:** call `model.atomic_model.descriptor.disable_graph_lower()` on the constructed model before training. This flips `uses_graph_lower()` to `False`, which both the eager forward and the compiled-training lower honor, so both run the dense route consistently (the same mechanism the spin model uses to stay on the dense path). A first-class training-config knob for this is planned as a follow-up.
112+
:::
113+
105114
:::{note}
106115
**Smoothness at the cutoff.** The graph route is exactly smooth at the cutoff, like the dense path. The non-attention channels (environment matrix, switch envelope, convolution, drrd/grrg, g1g1, symmetrization) are smooth by construction. The repformer *attention* channels (`update_g1_has_attn`, `update_g2_has_attn`) additionally use a fixed-phantom-count softmax: the dense smooth-attention denominator keeps exactly `sel − n_real` padding terms at $e^{-\mathrm{attnw\_shift}}$ (a geometry-independent count); the graph kernels reproduce this by excluding masked pairs from the softmax and adding $\max(\mathrm{sel} - n_\mathrm{real}, 0)$ phantom denominator terms per center. An edge entering the cutoff sphere does so at logit $-\mathrm{attnw\_shift}$ exactly while the phantom count drops by one, so the swap is value-preserving and the energy/force are continuous (verified at the float64 noise floor, $\lesssim 10^{-13}$). This also makes the carry-all graph attention agree with the dense attention term-for-term at non-binding `sel`. The only residual $e^{-20}$-scale discontinuity remains for a center with `sel` or more *real* neighbors within the block cutoff — a regime where the dense path itself suffers a far larger discontinuity from truncating a real neighbor, i.e. where `sel` is misconfigured.
107116
:::

source/tests/pt_expt/model/test_dpa2_graph_lower.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,59 @@ def test_force_virial_parity_vs_legacy(self) -> None:
224224
graph["energy_derv_c_redu"], legacy["energy_derv_c_redu"], **tol
225225
)
226226

227+
def test_disable_graph_lower_escape_hatch(self) -> None:
228+
"""``descriptor.disable_graph_lower()`` is the documented legacy-dense
229+
escape hatch: it flips ``uses_graph_lower()`` to ``False`` so the
230+
default (``neighbor_graph_method=None``) eager forward takes the DENSE
231+
route -- bit-identical to explicitly requesting ``"legacy"``.
232+
233+
Uses a BINDING repformer sel so graph and dense genuinely differ: with
234+
the hatch engaged, the default must match dense (not graph), which a
235+
binding sel makes an unambiguous (non-bit-tight) distinction.
236+
"""
237+
generator = torch.Generator(device=self.device).manual_seed(GLOBAL_SEED)
238+
nloc = 12
239+
box_size = 3.0
240+
coord = (
241+
torch.rand(
242+
[nloc, 3], dtype=torch.float64, device=self.device, generator=generator
243+
)
244+
* box_size
245+
).unsqueeze(0)
246+
atype = torch.tensor(
247+
[[ii % self.nt for ii in range(nloc)]],
248+
dtype=torch.int64,
249+
device=self.device,
250+
)
251+
box = (
252+
torch.eye(3, dtype=torch.float64, device=self.device) * box_size
253+
).reshape(1, 9)
254+
255+
model = self._make_model(repformer_nsel=3, repformer_attn=True)
256+
model.eval()
257+
assert model.atomic_model.descriptor.uses_graph_lower() is True
258+
259+
# engage the escape hatch
260+
model.atomic_model.descriptor.disable_graph_lower()
261+
assert model.atomic_model.descriptor.uses_graph_lower() is False
262+
263+
default_after = model.forward_common(
264+
coord.clone().requires_grad_(True), atype, box
265+
)
266+
legacy = model.forward_common(
267+
coord.clone().requires_grad_(True),
268+
atype,
269+
box,
270+
neighbor_graph_method="legacy",
271+
)
272+
# with the hatch on, default (None) == legacy (dense), bit-identical
273+
torch.testing.assert_close(
274+
default_after["energy_redu"], legacy["energy_redu"], rtol=0, atol=0
275+
)
276+
torch.testing.assert_close(
277+
default_after["energy_derv_r"], legacy["energy_derv_r"], rtol=0, atol=0
278+
)
279+
227280
def test_binding_sel_diverges(self) -> None:
228281
"""At binding repformer sel, the carry-all graph (sel-independent)
229282
keeps neighbors the dense body truncates, so the two routes diverge.

0 commit comments

Comments
 (0)