Skip to content
9 changes: 9 additions & 0 deletions deepmd/dpmodel/infer/deep_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
from deepmd.infer.deep_eval import DeepEval as DeepEvalWrapper
from deepmd.infer.deep_eval import (
DeepEvalBackend,
_standardize_fparam_aparam,
)
from deepmd.infer.deep_polar import (
DeepPolar,
Expand Down Expand Up @@ -240,6 +241,14 @@ def eval(
natoms, numb_test = self._get_natoms_and_nframes(
coords, atom_types, len(atom_types.shape) > 1
)
fparam, aparam = _standardize_fparam_aparam(
fparam,
aparam,
numb_test,
natoms,
self.get_dim_fparam(),
self.get_dim_aparam(),
)
request_defs = self._get_request_defs(atomic)
out = self._eval_func(self._eval_model, numb_test, natoms)(
coords, cells, atom_types, fparam, aparam, request_defs
Expand Down
101 changes: 80 additions & 21 deletions deepmd/infer/deep_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,75 @@
import ase.neighborlist


def _standardize_fparam_aparam(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deepmd/pt_expt/infer/deep_eval.py has the same defect and is not wired to this helper: raw fparam/aparam go into _eval_func / AutoBatchSize, and _eval_model / _eval_model_lower do bare fparam.reshape(nframes, dim_fparam) and aparam.reshape(nframes, natoms, dim_aparam) with no shorthand handling. It is also absent from the io test loop, so nothing would catch it.

Its own eval docstring documents only the full nframes x ... forms, which quietly narrows the base DeepEvalBackend.eval contract - that has listed all three shorthand forms since #3213 and is the contract this PR is enforcing everywhere else.

Given pt_expt is the actively developed backend and carries no back-compat constraints, adding the same one-line call here seems worth doing in this PR rather than leaving a fifth adapter behind.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9867233. pt_expt now calls the shared helper before auto batching, its eval docstring documents all supported shorthand forms, and a direct adapter regression verifies the normalized arguments reaching _eval_model.

Coding agent: Codex
Codex version: codex-cli 0.144.6
Model: gpt-5.6-sol
Reasoning effort: xhigh

fparam: np.ndarray | list | None,
aparam: np.ndarray | list | None,
nframes: int,
natoms: int,
dim_fparam: int,
dim_aparam: int,
) -> tuple[np.ndarray | None, np.ndarray | None]:
"""Normalize documented parameter shorthand to frame-major arrays.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring explains the rationale well but skips the numpydoc Parameters/Returns sections that the neighbouring functions in this file use - eval, eval_descriptor, eval_fitting_last_layer, eval_embedding, and even the private single-argument _check_mixed_types.

With six parameters and a two-element tuple return, and given this is now the single definition of the shorthand contract for four backends, it is worth documenting each argument and the returned shapes explicitly - in particular that aparam comes back 3-D (nframes, natoms, dim_aparam) while _standard_input re-flattens it to 2-D for the public path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9867233. The shared helper now has complete numpydoc Parameters and Returns sections, including the canonical 2-D fparam and 3-D aparam return shapes and the public wrapper flattening note.

Coding agent: Codex
Codex version: codex-cli 0.144.6
Model: gpt-5.6-sol
Reasoning effort: xhigh


This normalization must happen before automatic batching. In particular,
an ``(natoms, dim_aparam)`` shared atomic parameter has an atom axis first;
a batcher would otherwise mistake that axis for frames and slice it.

Parameters
----------
fparam : np.ndarray or list or None
Frame parameters in full ``(nframes, dim_fparam)`` form or shared
``(dim_fparam,)`` shorthand.
aparam : np.ndarray or list or None
Atomic parameters in full ``(nframes, natoms, dim_aparam)`` form,
shared-per-atom ``(natoms, dim_aparam)`` shorthand, or shared-for-all
``(dim_aparam,)`` shorthand.
nframes : int
Number of input frames.
natoms : int
Number of atoms in each frame.
dim_fparam : int
Number of frame-parameter components.
dim_aparam : int
Number of atomic-parameter components.

Returns
-------
normalized_fparam : np.ndarray or None
Frame parameters with shape ``(nframes, dim_fparam)`` when provided.
normalized_aparam : np.ndarray or None
Atomic parameters with shape ``(nframes, natoms, dim_aparam)`` when
provided. The public wrapper may subsequently flatten the last two
axes to preserve its historical backend ABI.
"""
if fparam is not None:
fparam = np.asarray(fparam)
if fparam.size == nframes * dim_fparam:
fparam = fparam.reshape(nframes, dim_fparam)
elif fparam.size == dim_fparam:
fparam = np.tile(fparam.reshape(1, dim_fparam), (nframes, 1))
else:
raise RuntimeError(
"got wrong size of frame param, should be either "
f"{nframes} x {dim_fparam} or {dim_fparam}"
)
if aparam is not None:
aparam = np.asarray(aparam)
if aparam.size == nframes * natoms * dim_aparam:
aparam = aparam.reshape(nframes, natoms, dim_aparam)
elif aparam.size == natoms * dim_aparam:
aparam = np.tile(aparam.reshape(1, natoms, dim_aparam), (nframes, 1, 1))
elif aparam.size == dim_aparam:
aparam = np.tile(aparam.reshape(1, 1, dim_aparam), (nframes, natoms, 1))
else:
raise RuntimeError(
"got wrong size of atomic param, should be either "
f"{nframes} x {natoms} x {dim_aparam} or "
f"{natoms} x {dim_aparam} or {dim_aparam}"
)
return fparam, aparam


class DeepEvalBackend(ABC):
"""Low-level Deep Evaluator interface.

Expand Down Expand Up @@ -948,28 +1017,18 @@ def _standard_input(
coords = coords.reshape(nframes, natoms, 3)
if cells is not None:
cells = cells.reshape(nframes, 3, 3)
if fparam is not None:
fdim = self.get_dim_fparam()
if fparam.size == nframes * fdim:
fparam = np.reshape(fparam, [nframes, fdim])
elif fparam.size == fdim:
fparam = np.tile(fparam.reshape([-1]), [nframes, 1])
else:
raise RuntimeError(
f"got wrong size of frame param, should be either {nframes} x {fdim} or {fdim}"
)
fparam, aparam = _standardize_fparam_aparam(
fparam,
aparam,
nframes,
natoms,
self.get_dim_fparam(),
self.get_dim_aparam(),
)
if aparam is not None:
fdim = self.get_dim_aparam()
if aparam.size == nframes * natoms * fdim:
aparam = np.reshape(aparam, [nframes, natoms * fdim])
elif aparam.size == natoms * fdim:
aparam = np.tile(aparam.reshape([-1]), [nframes, 1])
elif aparam.size == fdim:
aparam = np.tile(aparam.reshape([-1]), [nframes, natoms])
else:
raise RuntimeError(
f"got wrong size of frame param, should be either {nframes} x {natoms} x {fdim} or {natoms} x {fdim} or {fdim}"
)
# Preserve the historical flattened backend ABI used by the public
# wrapper; backend adapters normalize it back to frame-major 3-D.
aparam = aparam.reshape(nframes, natoms * self.get_dim_aparam())
return coords, cells, atom_types, fparam, aparam, nframes, natoms

def get_sel_type(self) -> list[int]:
Expand Down
9 changes: 9 additions & 0 deletions deepmd/jax/infer/deep_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from deepmd.infer.deep_eval import DeepEval as DeepEvalWrapper
from deepmd.infer.deep_eval import (
DeepEvalBackend,
_standardize_fparam_aparam,
)
from deepmd.infer.deep_polar import (
DeepPolar,
Expand Down Expand Up @@ -278,6 +279,14 @@ def eval(
natoms, numb_test = self._get_natoms_and_nframes(
coords, atom_types, len(atom_types.shape) > 1
)
fparam, aparam = _standardize_fparam_aparam(
fparam,
aparam,
numb_test,
natoms,
self.get_dim_fparam(),
self.get_dim_aparam(),
)
request_defs = self._get_request_defs(atomic)
out = self._eval_func(self._eval_model, numb_test, natoms)(
coords, cells, atom_types, fparam, aparam, charge_spin, request_defs
Expand Down
9 changes: 9 additions & 0 deletions deepmd/pd/infer/deep_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from deepmd.infer.deep_eval import DeepEval as DeepEvalWrapper
from deepmd.infer.deep_eval import (
DeepEvalBackend,
_standardize_fparam_aparam,
)
from deepmd.infer.deep_polar import (
DeepGlobalPolar,
Expand Down Expand Up @@ -376,6 +377,14 @@ def eval(
natoms, numb_test = self._get_natoms_and_nframes(
coords, atom_types, len(atom_types.shape) > 1
)
fparam, aparam = _standardize_fparam_aparam(
fparam,
aparam,
numb_test,
natoms,
self.get_dim_fparam(),
self.get_dim_aparam(),
)
request_defs = self._get_request_defs(atomic)
if "spin" not in kwargs or kwargs["spin"] is None:
out = self._eval_func(self._eval_model, numb_test, natoms)(
Expand Down
20 changes: 20 additions & 0 deletions deepmd/pt/infer/deep_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from deepmd.infer.deep_eval import DeepEval as DeepEvalWrapper
from deepmd.infer.deep_eval import (
DeepEvalBackend,
_standardize_fparam_aparam,
)
from deepmd.infer.deep_polar import (
DeepGlobalPolar,
Expand Down Expand Up @@ -544,6 +545,14 @@ def eval(
natoms, numb_test = self._get_natoms_and_nframes(
coords, atom_types, len(atom_types.shape) > 1
)
fparam, aparam = _standardize_fparam_aparam(
fparam,
aparam,
numb_test,
natoms,
self.get_dim_fparam(),
self.get_dim_aparam(),
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
request_defs = self._get_request_defs(atomic)
if "spin" not in kwargs or kwargs["spin"] is None:
out = self._eval_func(self._eval_model, numb_test, natoms)(
Expand Down Expand Up @@ -1312,6 +1321,17 @@ def eval_embedding(
natoms, numb_test = self._get_natoms_and_nframes(
coords, atom_types, len(atom_types.shape) > 1
)
# Normalize shared parameter shorthand before auto batching. Otherwise
# a one-dimensional fparam/aparam is passed unchanged to every split,
# and _eval_embedding cannot reshape it to the split frame count.
fparam, aparam = _standardize_fparam_aparam(
fparam,
aparam,
numb_test,
natoms,
self.get_dim_fparam(),
self.get_dim_aparam(),
)
return self._eval_func(self._eval_embedding, numb_test, natoms)(
coords, cells, atom_types, fparam, aparam, charge_spin, dtype
)
Expand Down
46 changes: 43 additions & 3 deletions deepmd/pt_expt/infer/deep_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from deepmd.infer.deep_eval import DeepEval as DeepEvalWrapper
from deepmd.infer.deep_eval import (
DeepEvalBackend,
_standardize_fparam_aparam,
)
from deepmd.infer.deep_polar import (
DeepPolar,
Expand Down Expand Up @@ -848,10 +849,15 @@ def eval(
Calculate the atomic energy and virial
fparam
The frame parameter.
The array should be of size nframes x dim_fparam.
The array can be of size :
- nframes x dim_fparam.
- dim_fparam. Then all frames are assumed to be provided with the same fparam.
aparam
The atomic parameter.
The array should be of size nframes x natoms x dim_aparam.
The atomic parameter
The array can be of size :
- nframes x natoms x dim_aparam.
- natoms x dim_aparam. Then all frames are assumed to be provided with the same aparam.
- dim_aparam. Then all frames and atoms are provided with the same aparam.
charge_spin
The charge and spin values for each frame.
The array should be reshape-compatible with nframes x 2, where the first
Expand All @@ -875,6 +881,14 @@ def eval(
natoms, numb_test = self._get_natoms_and_nframes(
coords, atom_types, len(atom_types.shape) > 1
)
fparam, aparam = _standardize_fparam_aparam(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wires eval, but not the other two entry points on this backend, and the PR body says otherwise.

The summary claims the change covers "normal, spin, embedding, descriptor, and fitting-last-layer routes". For pt_expt the descriptor and fitting-last-layer routes are not covered: _standardize_fparam_aparam appears exactly twice in this file -- the import and this call. Meanwhile eval_descriptor and eval_fitting_last_layer both hand their raw fparam/aparam to _prepare_nlist_inputs, which does the hard reshape at L1674-L1678:

        if aparam is not None:
            aparam_t = torch.tensor(
                aparam.reshape(nframes, natoms, self.get_dim_aparam()),

So a backend-direct eval_descriptor(coords_2frames, cells, atypes, aparam=np.zeros((natoms, dim_aparam))) raises ValueError: cannot reshape array of size ... into shape (2, natoms, dim), while the base-class method it overrides documents that exact form (deepmd/infer/deep_eval.py). pt handles it after this PR and pd inherits it by delegating to self.eval, so pt_expt is now the only backend that rejects documented input on these two routes.

I want to be clear this is milder than the bug the PR fixes: these routes do not go through AutoBatchSize, so the failure is a loud rejection rather than silent corruption, and the public eval_descriptor path is unaffected because _standard_input pre-flattens. It is a contract inconsistency, not a data-corruption bug. But it is the same one-line call in the same file, and leaving it means the PR body overstates what landed -- which is the part I would most like fixed either way.

Normalizing inside _prepare_nlist_inputs would cover both entry points at once and match how pd gets it for free. Their docstrings would want the shorthand lines too -- they currently say only "Frame parameters, optional.", the same narrowing you just corrected for eval.

On coverage while you are here: the new pd and pt_expt adapter tests both set _eval_func = lambda inner, numb_test, numb_atoms: inner, which takes AutoBatchSize out of the picture entirely. They prove the helper is called, which is useful, but they cannot catch a regression in the axis-slicing behaviour in execute_all that made this a bug in the first place -- only test_io.py and test_embedding.py do that, via a real auto_batch_size=natoms. Pointing one of the two adapter tests at a real batcher would close that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 866ce34. _standardize_fparam_aparam is now wired into pt_expt's eval_descriptor and eval_fitting_last_layer (in addition to eval), so both routes normalize shared-per-atom shorthand before _prepare_nlist_inputs reshapes the arrays. Added adapter regressions for both routes that call the backend methods directly and assert the normalized frame-major arrays reach _prepare_nlist_inputs; all three pt_expt adapter tests pass and ruff is clean. The PR body's "descriptor and fitting-last-layer routes" claim now matches the implementation.

Coding agent: opencode
opencode version: 1.18.9
Model: ustc/deepseek-v4-flash
Reasoning effort: max

fparam,
aparam,
numb_test,
natoms,
self.get_dim_fparam(),
self.get_dim_aparam(),
)
request_defs = self._get_request_defs(atomic)
spins = kwargs.get("spin")
if self._is_spin and spins is None:
Expand Down Expand Up @@ -2203,6 +2217,19 @@ def eval_descriptor(
"eval_descriptor is not supported for this model type "
f"({type(self._dpmodel).__name__})."
)
nframes = coords.shape[0]
natoms = len(atom_types) if len(atom_types.shape) == 1 else atom_types.shape[1]
# Canonicalize shared parameter shorthands before _prepare_nlist_inputs
# reshapes them; otherwise a shared per-atom array can be mistaken for
# a batch of frames.
fparam, aparam = _standardize_fparam_aparam(
fparam,
aparam,
nframes,
natoms,
self.get_dim_fparam(),
self.get_dim_aparam(),
)
(
ext_coord_t,
ext_atype_t,
Expand Down Expand Up @@ -2274,6 +2301,19 @@ def eval_fitting_last_layer(
"eval_fitting_last_layer is not supported for this model type "
f"({type(self._dpmodel).__name__})."
)
nframes = coords.shape[0]
natoms = len(atom_types) if len(atom_types.shape) == 1 else atom_types.shape[1]
# Canonicalize shared parameter shorthands before _prepare_nlist_inputs
# reshapes them; otherwise a shared per-atom array can be mistaken for
# a batch of frames.
fparam, aparam = _standardize_fparam_aparam(
fparam,
aparam,
nframes,
natoms,
self.get_dim_fparam(),
self.get_dim_aparam(),
)
(
ext_coord_t,
ext_atype_t,
Expand Down
20 changes: 20 additions & 0 deletions deepmd/tf/infer/deep_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
)
from deepmd.infer.deep_eval import (
DeepEvalBackend,
_standardize_fparam_aparam,
)
from deepmd.infer.deep_polar import (
DeepGlobalPolar,
Expand Down Expand Up @@ -761,6 +762,14 @@ def eval(
coords,
atom_types,
)
fparam, aparam = _standardize_fparam_aparam(
fparam,
aparam,
numb_test,
natoms,
self.get_dim_fparam(),
self.get_dim_aparam(),
)
output = self._eval_func(self._eval_inner, numb_test, natoms)(
coords,
cells,
Expand Down Expand Up @@ -1084,6 +1093,17 @@ def eval_descriptor(
coords,
atom_types,
)
# Canonicalize shared parameter shorthands before AutoBatchSize slices
# the frame axis; otherwise a shared per-atom array can be mistaken for
# a batch of frames.
fparam, aparam = _standardize_fparam_aparam(
fparam,
aparam,
numb_test,
natoms,
self.get_dim_fparam(),
self.get_dim_aparam(),
)
descriptor = self._eval_func(self._eval_descriptor_inner, numb_test, natoms)(
coords,
cells,
Expand Down
9 changes: 9 additions & 0 deletions deepmd/tf2/infer/deep_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from deepmd.infer.deep_eval import DeepEval as DeepEvalWrapper
from deepmd.infer.deep_eval import (
DeepEvalBackend,
_standardize_fparam_aparam,
)
from deepmd.infer.deep_polar import (
DeepPolar,
Expand Down Expand Up @@ -292,6 +293,14 @@ def eval(
natoms, numb_test = self._get_natoms_and_nframes(
coords, atom_types, len(atom_types.shape) > 1
)
fparam, aparam = _standardize_fparam_aparam(
fparam,
aparam,
numb_test,
natoms,
self.get_dim_fparam(),
self.get_dim_aparam(),
)
request_defs = self._get_request_defs(atomic)
out = self._eval_func(self._eval_model, numb_test, natoms)(
coords, cells, atom_types, fparam, aparam, request_defs
Expand Down
Loading