Repository navigation
[BUG] Multi-frame DeepPot.eval produces NaNs and corrupted outputs for frozen native-spin DPA4 graph models #5969
Copy link
Copy link
Open
Labels
Description
Bug summary
Issue body
Summary
Multi-frame inference with a frozen native-spin DPA4 graph-form .pt2 model produces NaNs and extremely large corrupted values.
Single-frame inference with the same model and the same input structures works correctly.
The affected public API is:
from deepmd.infer import DeepPot
dp = DeepPot("dpa4_step128000.pt2")
energy, force, virial, force_mag, mask_mag = dp.eval(
coords, # (nframes, natoms, 3)
cells, # (nframes, 9)
atom_types, # (natoms,)
spin=spins, # (nframes, natoms, 3)
)
The issue appears when multiple frames are passed to DeepPot.eval() in a single call.
## Environment
- DeepMD-kit: 3.2.0b1.dev211+gf573ca045
- Backend: PyTorch
- PyTorch: 2.11.0+cu126
- Model type: DPA4 native-spin model
- Frozen model format: graph-form .pt2
- Number of atoms per frame: 54
The model was frozen using dp --pt freeze.
## Observed behavior
The input coordinates, cells, atom types, and spins contain no NaNs or infinite values.
I set DP_INFER_BATCH_SIZE=4096, which is large enough to keep the tested frames in a single internal inference batch.
The results depend on the number of frames passed in one call:
Frames in one eval() call Observed result
━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1 All outputs are finite and correct
─────────────────────────── ─────────────────────────────────────────────────────────────────────────────
2 The second frame contains NaNs in energy, force, virial, and magnetic force
─────────────────────────── ─────────────────────────────────────────────────────────────────────────────
3 Two frames contain NaNs
─────────────────────────── ─────────────────────────────────────────────────────────────────────────────
4 All outputs happened to be finite in this test
─────────────────────────── ─────────────────────────────────────────────────────────────────────────────
8 NaNs and extremely large finite values appear
─────────────────────────── ─────────────────────────────────────────────────────────────────────────────
16 NaNs and extremely large finite values appear
For two frames, the NaN counts were:
Output Number of NaNs
━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━
Energy 1
──────────────── ────────────────
Force 162
──────────────── ────────────────
Virial 9
──────────────── ────────────────
Magnetic force 162
──────────────── ────────────────
Magnetic mask 0
Here, 162 equals 54 atoms × 3, and 9 corresponds to one full virial tensor. Therefore, one complete frame is corrupted.
For eight frames, I observed:
- 2 NaN energy values
- 801 NaN force components
- 45 NaN virial components
- 324 NaN magnetic-force components
- finite but unphysical energy values up to approximately 2.7e9 eV
- finite but unphysical force-like values of approximately 7.7e7
The magnetic mask remains finite, while the model-computed outputs are corrupted.
The behavior is shape-dependent: a four-frame call happened to be finite, so the problem does not occur monotonically for every batch size.
## Expected behavior
Calling DeepPot.eval() with multiple frames should give the same result as evaluating those frames individually and concatenating the results:
batched_result = dp.eval(
coords,
cells,
atom_types,
spin=spins,
)
should be numerically equivalent to:
single_frame_results = []
for i in range(len(coords)):
result = dp.eval(
coords[i:i + 1],
cells[i:i + 1],
atom_types,
spin=spins[i:i + 1],
)
single_frame_results.append(result)
## Reliable workaround
Explicitly evaluating one frame per call works correctly:
results = []
for i in range(len(coords)):
result = dp.eval(
coords[i:i + 1],
cells[i:i + 1],
atom_types,
spin=spins[i:i + 1],
)
results.append(result)
I tested 2,118 frames using this workaround. All frames completed successfully with finite outputs.
## Relevant inference path
Based on Python introspection, the multi-frame call follows this path:
DeepPot.eval
→ deepmd.pt_expt.infer.deep_eval.DeepEval.eval
→ _eval_model_spin
→ _eval_model_graph_spin
→ batched NeighborGraph construction
→ AOTICompiledModel
The .pt2 runner is:
torch.export.pt2_archive._package.AOTICompiledModel
The graph-form native-spin inference path constructs flattened batched graph inputs, including:
atype
n_node
n_local
edge_index
edge_vec
edge_mask
destination_order
destination_row_ptr
source_order
source_row_ptr
spin
fparam
aparam
charge_spin
Therefore, the issue appears to be localized to multi-frame inference through the graph-form native-spin .pt2 AOTI path.
It may involve either:
1. node/edge offsets or row pointers in the batched NeighborGraph, or
2. dynamic-shape handling or shape specialization in the AOT-compiled .pt2 model.
I have not yet isolated whether the corruption originates in graph construction or in the compiled AOTI model.
## Scope
This does not appear to indicate corrupted model parameters:
- every tested frame works when evaluated individually;
- the same input frame can fail in a multi-frame call and succeed in a single-frame call;
- all 2,118 frames were successfully evaluated using single-frame calls.
The issue has currently only been confirmed for the following combination:
- development version of DeepMD-kit;
- DPA4 native-spin model;
- graph-form frozen .pt2 model;
- DeepPot.eval() with more than one frame per call.
I have not confirmed whether the same issue affects non-spin DPA4 models, non-graph input formats, or unfrozen/raw checkpoints.
### DeePMD-kit Version
v3.2.0b1.dev211+gf573ca045
### Backend and its version
PyTorch 2.11.0+cu126
### How did you download the software?
Offline packages
### Input Files, Running Commands, Error Log, etc.
上面有
### Steps to Reproduce
上面有
### Further Information, Files, and Links
_No response_Reactions are currently unavailable
Activity
dosubot commentedon Aug 14, 2026
Hi @iduygnay! I'm Dosu and I'm helping the DeepModeling team.
This looks like a real bug in the multi-frame Python inference path for native-spin DPA4 graph
.pt2models. The corruption pattern you describe — frame 0 correct, subsequent frames producing NaNs, non-monotonic dependence on batch size — matches a class of per-frame offset bugs that have already been found and fixed on two other backends.Closely related bugs already fixed
DeepSpinC++ path had exactly this symptom: extension buffers and output copyback only handled frame zero, so force, force_mag, atomic energy, and virial for frames beyond 0 were garbage. The fix added explicitframe * natoms * strideoffsets throughout. [1]DeepEvaladapter had the same issue: the flatspintensor was not being reshaped and sliced per frame underAutoBatchSize, so multi-frame spin evaluations silently used only frame 0's spin for every frame. [2]Why the pt_expt graph-spin path is affected
The native-spin graph route was introduced in PR #5884. Its validation covered dpmodel parity, C++ gtests (single-frame), and LAMMPS single-rank runs, but did not include multi-frame Python
DeepEvalregressions for the graph-spin path [3]. TheNeighborGraphbatches multiple frames by stacking nodes/edges on a flat axis withn_node,destination_row_ptr, andsource_row_ptrencoding per-frame offsets [4]. If those offsets are applied incorrectly to thespintensor or edge indices whennframes > 1, the compiled AOTI kernel indexes into the wrong nodes — producing exactly the NaN/garbage pattern you see.Additionally, #5965 identifies that the graph-native spin path is still missing
forward_ragged/call_common_raggedseams and does not propagate the padding mask, which are related gaps in the same code area [5].Most likely root cause
Based on the pattern and the analogous fixes in #5851 and #5852, the probable culprit is that the
spintensor fed to the AOTI runner has shape(nf * nloc, 3)but per-frame node-offset bookkeeping inn_node/ row pointers isn't applied consistently to it — so frame N's descriptor reads frame 0's spin values, or edgesrc/dstindices from frame N point into frame 0's node range.Debugging suggestions
NeighborGraphfields (n_node,destination_row_ptr,source_row_ptr,spin) for a 2-frame call and verify the second frame's slice starts at offsetnloc, not 0.spintensor passed to the AOTI runner has correct frame stacking(nf * nloc, 3)or is inadvertently(nloc, 3)from only frame 0.spinto(numb_test, natoms, 3)beforeAutoBatchSizeslicing — is directly applicable here [2].Your workaround of evaluating one frame at a time is correct. The fix belongs in the multi-frame
NeighborGraphconstruction or spin-tensor slicing logic indeepmd/pt_expt/infer/deep_eval.py.To reply, just mention @dosu.
Docs are dead. Just use Dosu.
njzjz commentedon Aug 14, 2026
Thanks for the detailed report. I checked the exact
f573ca045code path and currentmaster, and I think we can narrow this down further.I do not think the earlier “missing per-frame offset / wrong spin slicing” diagnosis matches the pt_expt graph-spin code:
neighbor_graph_from_ijs()explicitly converts frame-local indices to the flattened graph withi + nframe_id * nloc/j + nframe_id * nloc, and the CSR views are built over the fullnf * nlocnode axis._eval_model_graph_spin()reshapesatypeandspinonto that same flat node axis; in particularspinis passed as(nframes * natoms, 3).aparam, when present, is flattened the same way._build_graph_dynamic_shapes(..., is_native_spin=True)marksnframes, the flat node countN, and edge countEas dynamic, and makesspinshare the same dynamicNsymbol asatype.test_graph_lower_exportable_torch_exportfor native-spin DPA4 already builds its default sample withnframes=2, exports it with dynamicnf/N/E, and checks the exported program against eager execution. That path is finite and agrees with eager execution.The coverage hole appears one layer later: AOTInductor packaging/execution. The end-to-end
test_deep_eval_graph_spin_parityfreezes a graph.pt2, loads it throughDeepPot, and runs the AOTI package, but its_SPIN_EVAL_COORDS, cell, and spins are all hard-coded with leading dimension1. That test is also skipped in CI because the AOTI compile is slow. I checked currentmaster; it is still single-frame.So the first layer that is both consistent with this report and not covered by the existing multi-frame tests is:
torch.export ExportedProgram (passes multi-frame)→aoti_compile_and_package/ CUDA AOTInductor →AOTICompiledModel(only tested with one frame)My current diagnosis is therefore a CUDA AOTInductor dynamic-shape miscompile/runtime bug in the frozen graph artifact, rather than a missing frame offset in
DeepEvalorNeighborGraph. The non-monotonic shape dependence (2/3/8/16 bad while 4 happened to work) also fits generated-kernel shape sensitivity better than a deterministic frame-offset error.One additional clue is that energy itself is corrupted, together with force/virial/
force_mag. That makes a bug confined to the final force/virial CSR scatter unlikely; the corruption must occur in the energy/descriptor path or an earlier compiled intermediate as well.I would not claim the exact generated kernel yet without a reproducer. The clean isolation test is to run the same 1/2/3/4/8-frame graph inputs through three stages: (1) eager
forward_common_lower_graph, (2) the pre-AOTI exported module, and (3) the packaged.pt2. If (1) and (2) remain finite while (3) fails, that pins the bug to Inductor codegen/runtime and gives us a small target for inspecting the generated kernel/configuration. This should also become a regression test: batched.pt2results must match concatenated single-frame calls for native-spin DPA4.If possible, could you attach the affected
.pt2(or a minimal checkpoint/config that reproduces it)? The current report is enough to localize the layer, but the model would let us identify the exact AOTI kernel/operation instead of guessing from static inspection.For now, evaluating one frame per call is a valid workaround.
Agent: ChatGPT
Agent version: not exposed in this runtime
Model: GPT-5.6 Sol
OutisLi commentedon Aug 17, 2026
I could not reproduce this on current
masterusing the existing local DeepMD/PyTorch environment and CUDA hardware.Environment used
ed691aab147d9d7686d296e30f46902c08e9fb68(currentupstream/masterwhen tested)2.13.0+cu130DP_INFER_BATCH_SIZE=4096DeepPot(..., auto_batch_size=False), so every requested frame count stayed in one AOTI callThe installed
libdeepmd_op_pt.sowas older than the checked-out Python/CUDA source and initially failed during export because itsedge_force_virialschema still contained anedge_spin_gradientargument. I rebuilt only the existing build-treedeepmd_op_pttarget from current source and explicitly loaded that build-tree library. The verified schema was:The stale-library failure is unrelated to this issue and was not used as evidence below.
Reproduction procedure
I built a jittered native-spin DPA4 model so that the model has non-trivial spin dependence and
force_magis not vacuously zero. The model used fp32 descriptor and fitting parameters with a default-scale architecture:I exported a graph-form CUDA AOTInductor
.pt2throughdeepmd.pt_expt.utils.serialization.deserialize_to_file(..., lower_kind="graph"), loaded it through the publicDeepPotAPI, and compared each batched call against concatenated single-frame calls.The test system contained 54 atoms per frame, matching the report. Every frame used distinct coordinates, cells, and spins. I tested
nframes = 1, 2, 3, 4, 8.All outputs were finite for every frame count:
These are ordinary finite fp32 reduction-order differences, not the reported NaNs or
1e7-1e9corruption. No frame was wholly or partially corrupted.Related paths checked
The previously fixed frame-offset bugs in #5660/#5851 and #5852 do not match this current path: the graph builder applies frame offsets, and
spinis flattened onto the sameN = nframes * natomsnode axis asatype. #5965 concerns ragged training/padding loss and is also separate.There is also a provenance inconsistency worth resolving. At both report commit
f573ca045and current master, the standard PT SeZM exporter used bydp --pt freezereportslower_input_kind="edge_vec", whereas the issue says the artifact entered_eval_model_graph_spin, which requireslower_input_kind="graph". Please paste the artifact'smodel/extra/metadata.jsonor clarify whether it was actually frozen through the pt_expt graph exporter or a locally modified path.Current assessment
I cannot confirm a DeepMD frame-offset or graph-construction bug with the available information. The report's non-monotonic shape dependence remains consistent with an environment-specific CUDA AOTInductor wrong-code/runtime problem. Since the same fp32 architecture and frame/atom shapes are stable on the RTX PRO 6000 Blackwell test host, a reporter-side GPU/driver/hardware issue is also plausible. This is a hypothesis, not a confirmed hardware diagnosis; the exact production model may still trigger generated code that this synthetic model does not.
The remaining major differences are:
2.11.0+cu126; local test:2.13.0+cu130AOTInductor
.pt2packages are tied to their build/runtime toolchain and target device, so moving the reported.pt2to a different host is not a reliable reproduction method. To reproduce by rebuilding the artifact in a controlled environment, please provide:The hardware/environment discriminator is to rebuild
.pt2packages independently from the same checkpoint and config on the two environments, then run the same input frames. The codegen discriminator in the reporter's own environment remains comparing the same graph inputs through eager, the pre-AOT exported module, and the locally packaged.pt2.