Skip to content

Commit 2acc142

Browse files
author
Han Wang
committed
fix(pt_expt): derive graph trace edge_capacity from real edge count, not sel
The carry-all graph builder is sel-free (edges are cutoff-determined; sel is only a normalization constant), so the sel-derived static trace capacity ceil(1.25*nloc*sum(sel)) overflowed whenever the synthetic trace system's actual degree exceeded sel: compiled training with repinit/repformer nsel=10/6 raised 'edge overflow: 106 real edges > edge_capacity 89'; graph .pt2 export with sel=2 raised 'edge overflow: 36 real edges > edge_capacity 18'. The capacity now derives from the actual unpadded synthetic graph: build_synthetic_graph_inputs accepts e_max=None (the dynamic probe layout) and the new count_synthetic_graph_edges counts the real edges; export pads 25% (min +2) and keeps the concrete edge length duck-sizing-distinct from nframes/N, compiled training prime-pads it collision-free as before. The value remains trace-sample-only: the exported edge axis stays fully dynamic (Dim(nedge, min=2)) and compiled training builds run-time graphs with no capacity. Regressions: dpa2 nsel=10/6 compiled training; dpa1 sel=2 graph export.
1 parent b048e23 commit 2acc142

4 files changed

Lines changed: 186 additions & 18 deletions

File tree

deepmd/pt_expt/train/training.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -679,8 +679,6 @@ def _trace_and_compile_graph(
679679
Per-task buffers promoted to FX placeholders (see
680680
:func:`_detect_task_buffers`).
681681
"""
682-
import math
683-
684682
from torch._decomp import (
685683
get_decompositions,
686684
)
@@ -740,20 +738,30 @@ def _trace_and_compile_graph(
740738
while (trace_nf * nloc_trace) in (_forbidden | {trace_nf}):
741739
nloc_trace += 1
742740
trace_N = trace_nf * nloc_trace
743-
# Static edge capacity, prime-padded to stay distinct from nf and N.
744-
nnei = sum(model.get_sel())
745-
e_max_base = max(math.ceil(1.25 * nloc_trace * nnei), 7)
746-
e_max = _next_safe_prime(e_max_base, _forbidden | {trace_nf, trace_N})
747-
748741
# Shared with the .pt2 export trace (serialization.py) so the two graph
749742
# traces can never desync on the input schema. Training uses the run-time
750743
# float precision and device; optional tensors match the actual call.
751744
from deepmd.pt_expt.utils.serialization import (
752745
build_synthetic_graph_inputs,
753746
check_graph_trace_torch_version,
747+
count_synthetic_graph_edges,
754748
)
755749

756750
check_graph_trace_torch_version(model)
751+
752+
# Static edge capacity: derived from the ACTUAL edge count of the
753+
# synthetic trace system (the carry-all builder is sel-free; a
754+
# sel-derived estimate overflows whenever the real degree exceeds sel),
755+
# then prime-padded to stay distinct from nf and N. ``+ 2`` keeps at
756+
# least two masked padding rows so the padded-tail branch is traced.
757+
e_real = count_synthetic_graph_edges(
758+
model,
759+
nframes=trace_nf,
760+
nloc=nloc_trace,
761+
dtype=GLOBAL_PT_FLOAT_PRECISION,
762+
device=_model_trace_device(model),
763+
)
764+
e_max = _next_safe_prime(e_real + 2, _forbidden | {trace_nf, trace_N})
757765
sample = build_synthetic_graph_inputs(
758766
model,
759767
e_max=e_max,

deepmd/pt_expt/utils/serialization.py

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ def _make_sample_inputs(
362362

363363
def build_synthetic_graph_inputs(
364364
model: torch.nn.Module,
365-
e_max: int,
365+
e_max: int | None,
366366
nframes: int = 2,
367367
nloc: int = 7,
368368
*,
@@ -394,8 +394,13 @@ def build_synthetic_graph_inputs(
394394
----------
395395
model : torch.nn.Module
396396
The pt_expt energy model (must expose ``get_rcut``/``get_type_map``/...).
397-
e_max : int
398-
Static edge capacity ``E`` to pad the (masked) edge axis to.
397+
e_max : int or None
398+
Static edge capacity ``E`` to pad the (masked) edge axis to. Must be
399+
at least the system's real edge count (the carry-all builder raises
400+
``edge overflow`` otherwise) — derive it from
401+
:func:`count_synthetic_graph_edges`, never from ``sel`` (the builder
402+
is sel-free). ``None`` selects the dynamic layout (real edges plus
403+
``min_edges`` guard rows); used by the edge-count probe itself.
399404
nframes : int
400405
Number of frames in the sample system.
401406
nloc : int
@@ -471,6 +476,56 @@ def build_synthetic_graph_inputs(
471476
)
472477

473478

479+
def count_synthetic_graph_edges(
480+
model: torch.nn.Module,
481+
nframes: int,
482+
nloc: int,
483+
*,
484+
dtype: torch.dtype,
485+
device: torch.device | None = None,
486+
) -> int:
487+
"""Count the real (unpadded) edges of the synthetic trace system.
488+
489+
Probes :func:`build_synthetic_graph_inputs` with ``e_max=None`` (the
490+
dynamic carry-all layout, whose edge axis is the real edge count plus
491+
the ``min_edges`` guard rows) and counts the ``edge_mask`` real prefix.
492+
The carry-all builder is sel-free — edges are cutoff-determined, ``sel``
493+
is only a normalization constant — so the static trace ``edge_capacity``
494+
must derive from this geometry-determined count; a sel-based estimate
495+
overflows whenever the synthetic system's real degree exceeds ``sel``
496+
(small-``sel`` models).
497+
498+
Parameters
499+
----------
500+
model : torch.nn.Module
501+
The pt_expt energy model (must expose ``get_rcut``/``get_type_map``).
502+
nframes : int
503+
Number of frames of the synthetic system; must match the subsequent
504+
:func:`build_synthetic_graph_inputs` call.
505+
nloc : int
506+
Local atoms per frame; must match the subsequent call.
507+
dtype : torch.dtype
508+
Float precision of the probe coordinates; must match the subsequent
509+
call (the edge count is cutoff-thresholded).
510+
device : torch.device, optional
511+
Probe device; must match the subsequent call.
512+
513+
Returns
514+
-------
515+
int
516+
Number of real edges of the synthetic system at the model cutoff.
517+
"""
518+
edge_mask = build_synthetic_graph_inputs(
519+
model,
520+
e_max=None,
521+
nframes=nframes,
522+
nloc=nloc,
523+
dtype=dtype,
524+
device=device,
525+
)[4]
526+
return int(edge_mask.sum())
527+
528+
474529
def _build_graph_dynamic_shapes(
475530
*sample_inputs: torch.Tensor | None,
476531
) -> tuple:
@@ -1046,11 +1101,26 @@ def _trace_and_export(
10461101
# The edge axis is DYNAMIC (B2.0): the AOTI artifact accepts any edge
10471102
# count, so there is no capacity to bake. The trace sample is built at a
10481103
# concrete, padded edge size only to keep the trace tensors distinct
1049-
# from the other dynamic dims (nframes=2, N=14) under torch.export's
1050-
# duck-sizing; the value itself does NOT constrain runtime.
1104+
# from the other dynamic dims (nframes, N) under torch.export's
1105+
# duck-sizing; the value itself does NOT constrain runtime. The
1106+
# capacity derives from the ACTUAL edge count of the synthetic system
1107+
# (the carry-all builder is sel-free; a sel-derived estimate overflows
1108+
# for small-sel models): 25% headroom keeps the masked padded tail
1109+
# genuinely traced, the ``+ 2`` floor guarantees it even for tiny edge
1110+
# counts, and the final bump keeps the concrete edge length distinct
1111+
# from the other trace dims under duck-sizing.
10511112
nloc_sample = 7
1052-
nnei = sum(model.get_sel())
1053-
e_sample = math.ceil(1.25 * nloc_sample * nnei)
1113+
nframes_sample = 1 if with_comm_dict else 2
1114+
e_real = count_synthetic_graph_edges(
1115+
model,
1116+
nframes=nframes_sample,
1117+
nloc=nloc_sample,
1118+
dtype=torch.float64,
1119+
device=torch.device("cpu"),
1120+
)
1121+
e_sample = max(math.ceil(1.25 * e_real), e_real + 2)
1122+
while e_sample in (nframes_sample, nframes_sample * nloc_sample):
1123+
e_sample += 1
10541124

10551125
if with_comm_dict:
10561126
# Load libdeepmd_op_pt.so and register border_op fake/autograd

source/tests/pt_expt/model/test_dpa2_graph_lower.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,64 @@ def test_compiled_training_graph_smoke(self) -> None:
552552
**tol,
553553
)
554554

555+
def test_compiled_training_graph_small_sel(self) -> None:
556+
"""The compiled-training trace capacity derives from the synthetic
557+
system's REAL edge count, not from ``sel``.
558+
559+
The carry-all graph builder is sel-free (sel = normalization
560+
constant only), so a sel-derived static trace capacity
561+
(``ceil(1.25 * nloc * sum(sel))``) overflows whenever the synthetic
562+
trace system's actual degree exceeds ``sel``: with repinit/repformer
563+
``nsel=10/6`` the trace used to raise ``edge overflow: 106 real
564+
edges > edge_capacity 89``. The capacity is now probed from the
565+
actual unpadded synthetic graph, so the trace must succeed and the
566+
compiled lower must match the eager graph lower (compiled and eager
567+
are the SAME route, so parity holds even at binding sel).
568+
"""
569+
from deepmd.pt_expt.train.training import (
570+
_trace_and_compile_graph,
571+
)
572+
from deepmd.pt_expt.utils.serialization import (
573+
build_synthetic_graph_inputs,
574+
)
575+
576+
model = self._make_model(repinit_nsel=10, repformer_nsel=6).to("cpu")
577+
model.eval()
578+
579+
compiled_lower, _ = _trace_and_compile_graph(model, None, None, None)
580+
581+
sample = build_synthetic_graph_inputs(
582+
model,
583+
e_max=97,
584+
nframes=3,
585+
nloc=5,
586+
dtype=torch.float64,
587+
device=torch.device("cpu"),
588+
want_fparam=False,
589+
want_aparam=False,
590+
want_charge_spin=False,
591+
)
592+
atype, n_node, ei, ev, em, fp, ap, cs = sample
593+
compiled_out = compiled_lower(atype, n_node, ei, ev, em, fp, ap, cs)
594+
eager = model.forward_common_lower_graph(
595+
atype,
596+
n_node,
597+
ei,
598+
ev,
599+
em,
600+
do_atomic_virial=False,
601+
fparam=fp,
602+
aparam=ap,
603+
charge_spin=cs,
604+
)
605+
tol = {"rtol": 1e-10, "atol": 1e-10}
606+
torch.testing.assert_close(compiled_out["energy"], eager["energy_redu"], **tol)
607+
torch.testing.assert_close(
608+
compiled_out["force"],
609+
eager["energy_derv_r"].reshape(compiled_out["force"].shape),
610+
**tol,
611+
)
612+
555613
def test_graph_lower_fparam_symbolic_trace_and_compile(self) -> None:
556614
"""A graph-eligible DPA2 model with ``numb_fparam > 0`` must export
557615
(``make_fx`` symbolic) AND inductor-compile.

source/tests/pt_expt/utils/test_graph_pt2_metadata.py

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,24 @@
4949
}
5050

5151

52-
def _build_dpa1_data() -> dict:
53-
"""Build a serialized dpmodel data dict for a dpa1(attn_layer=0) energy model."""
52+
def _build_dpa1_data(config: dict | None = None) -> dict:
53+
"""Build a serialized dpmodel data dict for a dpa1(attn_layer=0) energy model.
54+
55+
Parameters
56+
----------
57+
config : dict, optional
58+
Model config to build from. Defaults to ``DPA1_CONFIG``.
59+
"""
5460
from deepmd.dpmodel.model.model import (
5561
get_model,
5662
)
5763

58-
model = get_model(copy.deepcopy(DPA1_CONFIG))
64+
if config is None:
65+
config = DPA1_CONFIG
66+
model = get_model(copy.deepcopy(config))
5967
return {
6068
"model": model.serialize(),
61-
"model_def_script": copy.deepcopy(DPA1_CONFIG),
69+
"model_def_script": copy.deepcopy(config),
6270
"backend": "dpmodel",
6371
"software": "deepmd-kit",
6472
"version": "3.0.0",
@@ -94,6 +102,30 @@ def test_graph_pt2_has_lower_input_kind_graph(dpa1_dpmodel_data) -> None:
94102
assert "edge_capacity" not in meta
95103

96104

105+
def test_graph_pt2_small_sel_exports() -> None:
106+
"""Graph-form ``.pt2`` export succeeds for a small-``sel`` model.
107+
108+
The graph trace capacity derives from the synthetic trace system's
109+
REAL edge count; the former sel-derived estimate
110+
(``ceil(1.25 * nloc * sum(sel))``) overflowed the sel-free carry-all
111+
builder whenever the actual degree exceeded ``sel`` (``edge overflow:
112+
36 real edges > edge_capacity 18`` at ``sel=2``).
113+
"""
114+
cfg = copy.deepcopy(DPA1_CONFIG)
115+
cfg["descriptor"]["sel"] = 2
116+
data = _build_dpa1_data(cfg)
117+
with tempfile.TemporaryDirectory() as d:
118+
p = os.path.join(d, "m_graph_small_sel.pt2")
119+
deserialize_to_file(
120+
p,
121+
data,
122+
do_atomic_virial=True,
123+
lower_kind="graph",
124+
)
125+
meta = _read_metadata(p)
126+
assert meta["lower_input_kind"] == "graph"
127+
128+
97129
def test_dense_pt2_has_lower_input_kind_nlist(dpa1_dpmodel_data) -> None:
98130
"""Default (``lower_kind="nlist"``) -> metadata ``lower_input_kind == "nlist"``."""
99131
with tempfile.TemporaryDirectory() as d:

0 commit comments

Comments
 (0)