Skip to content

Commit ed4c1be

Browse files
author
Han Wang
committed
fix(pt_expt): trace graph lower on the model's device; fix CUDA test device placement
The graph-lower compile/export path traces the model on CPU (make_fx keeps real parameters, and the export path moves the model to CPU to dodge a CUDA autograd-stream limitation). _trace_and_compile_graph built its synthetic trace inputs on the global DEVICE instead, so on a GPU host it mixed a CPU model with CUDA inputs and failed make_fx tracing. Build the synthetic inputs on the model's own device. (No effect on CUDA training, where the model already lives on DEVICE.) The graph-export tests then run the exported program, which _trace_and_export moves to env.DEVICE via move_to_device_pass, but fed it CPU inputs and compared against a CPU eager model. Place the graph inputs (and the eager reference) on env.DEVICE; the with-comm host control tensors stay on CPU by design. Also read the CUDA repinit.mean stats buffer through .detach().cpu() before np.asarray, and relax the escape-hatch force check to CUDA scatter-add reduction-order noise (energy stays bit-exact). All CPU-only, so the CPU CI never exercised these paths.
1 parent 13b098b commit ed4c1be

4 files changed

Lines changed: 52 additions & 19 deletions

File tree

deepmd/pt_expt/train/training.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -691,12 +691,20 @@ def _trace_and_compile_graph(
691691
# sel-derived estimate overflows whenever the real degree exceeds sel),
692692
# then prime-padded to stay distinct from nf and N. ``+ 2`` keeps at
693693
# least two masked padding rows so the padded-tail branch is traced.
694+
# Trace on the MODEL's device, not the global ``DEVICE``: make_fx keeps the
695+
# real model parameters (``_allow_non_fake_inputs``), so the synthetic trace
696+
# inputs must live where the model does. A CUDA training run keeps the model
697+
# on ``DEVICE`` (these match), but callers that trace a CPU-placed model
698+
# (e.g. the graph .pt2/export path, which moves the model to CPU to dodge a
699+
# CUDA autograd-stream limitation) would otherwise mix a CPU model with
700+
# CUDA inputs and fail only on a GPU host.
701+
_trace_device = next(model.parameters()).device
694702
e_real = count_synthetic_graph_edges(
695703
model,
696704
nframes=trace_nf,
697705
nloc=nloc_trace,
698706
dtype=GLOBAL_PT_FLOAT_PRECISION,
699-
device=DEVICE,
707+
device=_trace_device,
700708
)
701709
e_max = _next_safe_prime(e_real + 2, _forbidden | {trace_nf, trace_N})
702710
sample = build_synthetic_graph_inputs(
@@ -705,7 +713,7 @@ def _trace_and_compile_graph(
705713
nframes=trace_nf,
706714
nloc=nloc_trace,
707715
dtype=GLOBAL_PT_FLOAT_PRECISION,
708-
device=DEVICE,
716+
device=_trace_device,
709717
want_fparam=fparam is not None,
710718
want_aparam=aparam is not None,
711719
want_charge_spin=charge_spin is not None,

source/tests/pt_expt/model/test_dpa2_graph_lower.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -326,12 +326,19 @@ def test_disable_graph_lower_escape_hatch(self) -> None:
326326
box,
327327
neighbor_graph_method="legacy",
328328
)
329-
# with the hatch on, default (None) == legacy (dense), bit-identical
329+
# With the hatch on, default (None) takes the same dense path as legacy,
330+
# so the energy is bit-identical. The force goes through a scatter/index_add
331+
# in the backward, which is non-deterministic on CUDA across two separate
332+
# passes, so it matches only to reduction-order noise (~1e-18), far below
333+
# any real route divergence (>=1e-8, cf. test_binding_sel_diverges).
330334
torch.testing.assert_close(
331335
default_after["energy_redu"], legacy["energy_redu"], rtol=0, atol=0
332336
)
333337
torch.testing.assert_close(
334-
default_after["energy_derv_r"], legacy["energy_derv_r"], rtol=0, atol=0
338+
default_after["energy_derv_r"],
339+
legacy["energy_derv_r"],
340+
rtol=1e-10,
341+
atol=1e-12,
335342
)
336343

337344
def test_binding_sel_diverges(self) -> None:
@@ -745,7 +752,7 @@ def _build(set_davg_zero: bool) -> EnergyModel:
745752
"box": self.cell.clone(),
746753
}
747754
model.atomic_model.descriptor.compute_input_stats([sample])
748-
davg = np.asarray(model.atomic_model.descriptor.repinit.mean)
755+
davg = np.asarray(model.atomic_model.descriptor.repinit.mean.detach().cpu())
749756
assert np.abs(davg).max() > 0, "computed statistics must be nonzero"
750757

751758
# 1. the gate: nonzero-mean configs stay on the legacy route.

source/tests/pt_expt/model/test_graph_export.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,9 @@ def test_graph_export_aparam_flat_node_axis():
160160
lower_kind="graph",
161161
)
162162
loaded = exported.module()
163+
# ``_trace_and_export`` moves the exported program to ``env.DEVICE``; run the
164+
# eager reference there too so both sides use matching-device inputs.
165+
model.to(env.DEVICE)
163166

164167
# nf=1, N=1 (single node: zero real edges, guard rows only) and a
165168
# multi-frame nf=3, N=15 system: both must pass the input guards and
@@ -171,7 +174,7 @@ def test_graph_export_aparam_flat_node_axis():
171174
nframes=nframes,
172175
nloc=nloc,
173176
dtype=torch.float64,
174-
device=torch.device("cpu"),
177+
device=env.DEVICE,
175178
)
176179
(
177180
a2,
@@ -191,9 +194,9 @@ def test_graph_export_aparam_flat_node_axis():
191194
# single-rank runtime: every node is owned
192195
nl2 = nn2.clone()
193196
# distinct per-row values so the sensitivity check below is real
194-
ap2 = torch.linspace(0.1, 0.9, ap2.numel(), dtype=torch.float64).reshape(
195-
ap2.shape
196-
)
197+
ap2 = torch.linspace(
198+
0.1, 0.9, ap2.numel(), dtype=torch.float64, device=ap2.device
199+
).reshape(ap2.shape)
197200
out = loaded(a2, nn2, nl2, ei2, ev2, em2, do2, drp2, so2, srp2, fp2, ap2, cs2)
198201
ref = model.forward_common_lower_graph(
199202
a2,

source/tests/pt_expt/model/test_graph_export_with_comm.py

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717
import pytest
1818

19+
from deepmd.pt_expt.utils.env import (
20+
DEVICE,
21+
)
1922
from deepmd.pt_expt.utils.serialization import (
2023
deserialize_to_file,
2124
)
@@ -239,15 +242,27 @@ def test_graph_with_comm_n_local_is_separate_device_input(
239242
atype = np.array([[i % 2 for i in range(n_total)]])
240243
graph = build_neighbor_graph(coord, atype, None, rcut, canonicalize=True)
241244

242-
atype_t = torch.tensor(atype.reshape(-1), dtype=torch.int64)
243-
n_node_t = torch.as_tensor(np.asarray(graph.n_node), dtype=torch.int64)
244-
ei = torch.as_tensor(np.asarray(graph.edge_index), dtype=torch.int64)
245-
ev = torch.as_tensor(np.asarray(graph.edge_vec), dtype=torch.float64)
246-
em = torch.as_tensor(np.asarray(graph.edge_mask), dtype=torch.bool)
247-
do_t = torch.as_tensor(np.asarray(graph.destination_order), dtype=torch.int64)
248-
drp_t = torch.as_tensor(np.asarray(graph.destination_row_ptr), dtype=torch.int64)
249-
so_t = torch.as_tensor(np.asarray(graph.source_order), dtype=torch.int64)
250-
srp_t = torch.as_tensor(np.asarray(graph.source_row_ptr), dtype=torch.int64)
245+
# Graph inputs live on the device the exported program was moved to
246+
# (env.DEVICE); the 8 comm tensors below stay on CPU (host control metadata).
247+
atype_t = torch.tensor(atype.reshape(-1), dtype=torch.int64, device=DEVICE)
248+
n_node_t = torch.as_tensor(
249+
np.asarray(graph.n_node), dtype=torch.int64, device=DEVICE
250+
)
251+
ei = torch.as_tensor(np.asarray(graph.edge_index), dtype=torch.int64, device=DEVICE)
252+
ev = torch.as_tensor(np.asarray(graph.edge_vec), dtype=torch.float64, device=DEVICE)
253+
em = torch.as_tensor(np.asarray(graph.edge_mask), dtype=torch.bool, device=DEVICE)
254+
do_t = torch.as_tensor(
255+
np.asarray(graph.destination_order), dtype=torch.int64, device=DEVICE
256+
)
257+
drp_t = torch.as_tensor(
258+
np.asarray(graph.destination_row_ptr), dtype=torch.int64, device=DEVICE
259+
)
260+
so_t = torch.as_tensor(
261+
np.asarray(graph.source_order), dtype=torch.int64, device=DEVICE
262+
)
263+
srp_t = torch.as_tensor(
264+
np.asarray(graph.source_row_ptr), dtype=torch.int64, device=DEVICE
265+
)
251266

252267
sendlist_indices = np.ascontiguousarray(
253268
np.arange(nghost, dtype=np.int32)
@@ -265,7 +280,7 @@ def test_graph_with_comm_n_local_is_separate_device_input(
265280
)
266281

267282
def run(n_local_val: int) -> torch.Tensor:
268-
n_local_t = torch.tensor([n_local_val], dtype=torch.int64)
283+
n_local_t = torch.tensor([n_local_val], dtype=torch.int64, device=DEVICE)
269284
out = loaded(
270285
atype_t,
271286
n_node_t,

0 commit comments

Comments
 (0)