Skip to content

feat(dpa-adapt): add grouped embedding for system-level property prediction#5741

Open
zirenjin wants to merge 275 commits into
deepmodeling:masterfrom
zirenjin:feat/grouped-embedding
Open

feat(dpa-adapt): add grouped embedding for system-level property prediction#5741
zirenjin wants to merge 275 commits into
deepmodeling:masterfrom
zirenjin:feat/grouped-embedding

Conversation

@zirenjin

@zirenjin zirenjin commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds grouped embedding support to DPA-ADAPT, enabling frame-level atomistic representations to be pooled into group-level embeddings for system-level property prediction.

The main use case is downstream adaptation where one training label corresponds to a collection of structures rather than a single frame, such as polymers, mixtures, reaction assemblies, or catalyst configurations.

Main Changes

  • Add grouped property model support with differentiable frame-to-group pooling.
  • Add group_id, weight, and pool_mask data fields for grouping frames and controlling atom-level pooling.
  • Add grouped data conversion utilities and CLI support.
  • Add grouped-aware dataloading and batch sampling so all frames from the same group stay together.
  • Support distributed training by ensuring each group is assigned to a single rank.
  • Add tests covering grouped data conversion, grouped sampling, grouped loss/model behavior, and DDP-safe batching.

Notes

This feature is intended for DPA-ADAPT tasks where the prediction target is defined at the system/sample level, while the input information is provided by multiple atomistic structures.

Summary by CodeRabbit

  • New Features
    • Added grouped-property training support with new group-level model and loss, including group-aware fitting and grouped-data batching.
    • Introduced grouped data tooling (writers, offline grouped dataset loading), plus a new mark-groups CLI command and grouped polymer dataset generation.
  • Bug Fixes
    • Hardened grouped pooling/masking for padded/virtual atoms and improved validation for per-group label and fparam consistency; distributed sampling now keeps groups intact.
  • Documentation
    • Added a guide for “Assembly Groups for Multi-Component Properties”.
  • Tests
    • Added extensive unit and end-to-end coverage for grouped training, batching, and serialization.

zirenjin and others added 30 commits June 2, 2026 13:54
The old _load_descriptor_model, _validate_type_map, _remap_atom_types,
_extract_features_cached, and _extract_features method bodies were left
in place alongside the new thin delegators, causing CodeQL 'variable
defined multiple times' warnings.  Removed the old bodies; kept
_extract_features_cached on DPAFineTuner directly so that test patches
on DPAFineTuner._extract_features are honoured through the cache wrapper.
… method

- Replace try/except ImportError in _unwrap_multioutput with direct import
  (sklearn is always available when dpa_tools is loaded)
- Remove _FrozenSklearnPipeline.extract_features_cached (dead code;
  the caching wrapper lives on DPAFineTuner so test patches work)
The workflow still referenced the deleted deepmd_property_tools/ directory.
Updated paths trigger to deepmd/dpa_tools/** and test command to
source/tests/dpa_tools/. Added torch to lightweight dependencies.
numpy 2.3+ requires Python>=3.11, but the property_tools_tests workflow
runs on Python 3.10. Pin numpy>=1.21,<2.2 to keep the lightweight
dependency install working on older Python.
refactor: unify dpa_tools CLI/API and merge deepmd_property_tools
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…flow-cleanup

Fix CI dependency installation
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
dpa_tools quickstart demo bundle, convert() glob multi-match, docs cleanup
zirenjin added 6 commits July 6, 2026 10:38
…dding

# Conflicts:
#	README.md
#	doc/dpa_adapt/index.rst
#	doc/dpa_adapt/input_formats.md
#	doc/dpa_adapt/overview.md
#	dpa_adapt/__init__.py
#	dpa_adapt/cli.py
#	dpa_adapt/data/__init__.py
#	dpa_adapt/data/convert.py
#	dpa_adapt/data/dataset.py
#	dpa_adapt/data/desc_cache.py
#	dpa_adapt/data/loader.py
#	dpa_adapt/data/smiles.py
#	dpa_adapt/data/type_map.py
#	dpa_adapt/finetuner.py
#	dpa_adapt/mft.py
#	dpa_adapt/trainer.py
#	source/tests/dpa_adapt/test_cli_smoke.py
#	source/tests/dpa_adapt/test_conditions.py
#	source/tests/dpa_adapt/test_convert.py
#	source/tests/dpa_adapt/test_type_map.py
test_pooling.py imported POOLING_PRIMITIVES, parse_pooling and
_pool_descriptor from dpa_adapt.finetuner, but those symbols never
existed -- the module only had a fixed _VALID_POOLING allowlist. The
failed import broke pytest collection, which under pytest-split failed
every Test Python split.

Add the module-level pooling API the tests expect:
- POOLING_PRIMITIVES = ("mean","sum","std","max","min") canonical order
- parse_pooling(): split on "+" / accept a sequence, dedup, canonical
  order, reject unknown and empty specs
- _pool_descriptor(): reduce over the atom axis and concat in primitive
  order, byte-identical to the previous per-string pooling

Route frozen-sklearn feature pooling through _pool_descriptor and replace
DPAFineTuner's allowlist check with parse_pooling plus an intensive-pooling
strategy guard (composite pooling requires strategy='frozen_sklearn').
Three grouped tests (added in 2cd5d60) exercised a grouped adaptation
API that was never implemented, so they surfaced once the pooling
collection error was cleared:

- DPATrainer auto-detects grouped systems (set.*/{group_id,weight,
  pool_mask}.npy) and emits the group_property fitting_net + loss, and
  auto-sizes numb_fparam from set.*/fparam.npy; a `grouped` override is
  accepted.
- DPAFineTuner accepts target= (alias for property_name); fit() accepts
  train=/valid= aliases.
- frozen_sklearn fit()/predict() route grouped input through
  GroupedDataset (weighted per-group pooling + shared labels), so
  target_key/labels are optional for grouped data.
Comment thread source/tests/dpa_adapt/test_grouped_end_to_end.py Fixed
Comment thread source/tests/dpa_adapt/test_grouped_end_to_end.py Fixed
Comment thread source/tests/dpa_adapt/test_grouped_end_to_end.py Fixed
Comment thread source/tests/dpa_adapt/test_grouped_end_to_end.py Fixed
Comment thread source/tests/dpa_adapt/test_grouped_end_to_end.py Fixed
Comment thread dpa_adapt/data/grouped_dataset.py Fixed
Comment thread dpa_adapt/data/grouped_convert.py Fixed
Comment thread dpa_adapt/data/assemblies.py Fixed
Comment thread dpa_adapt/data/aggregation.py Fixed
Comment thread deepmd/pt/model/model/group_property_model.py Fixed
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds grouped-property training in the PyTorch backend and grouped-data tooling in DPA-ADAPT. It introduces new grouped loss, model, batching, conversion, finetuning, and dataset-writing paths, plus documentation and tests.

Changes

PyTorch backend grouped-property training

Layer / File(s) Summary
GroupPropertyLoss contract and implementation
deepmd/pt/loss/__init__.py, deepmd/pt/loss/group_property.py, deepmd/pt/model/model/__init__.py
Adds GroupPropertyLoss, grouped-label handling, grouped loss computation, and model selection for group_property.
GroupProperty model and fitting net
deepmd/pt/model/task/group_property.py, deepmd/pt/model/task/__init__.py, deepmd/utils/argcheck.py, deepmd/pt/model/model/group_property_model.py
Adds the grouped fitting net and model, public exports, grouped argument schemas, grouped pooling, fparam handling, and serialization.
Training loop and grouped batching
deepmd/pt/train/training.py, deepmd/pt/utils/grouped.py, deepmd/pt/utils/dataloader.py
Adds grouped loss selection, checkpoint remapping, grouped tensor utilities, and group-preserving samplers in DpLoaderSet.
PT tests
source/tests/pt/test_group_property.py, source/tests/pt/test_group_property_hardening.py
Adds tests for grouped loss behavior, batching, pooling, serialization, and descriptor-padding hardening.

DPA-ADAPT grouped data toolkit

Layer / File(s) Summary
Assembly writer and grouped schema
dpa_adapt/grouped/_core.py, dpa_adapt/grouped/_convert.py, dpa_adapt/grouped/__init__.py, dpa_adapt/__init__.py, doc/dpa_adapt/assembly_scenarios.md
Adds grouped DeepMD writer classes, manifest generation, marker generation, lazy exports, CLI-facing grouped docs, and grouped usage documentation.
Offline grouped dataset and aggregation
dpa_adapt/grouped/_aggregation.py, dpa_adapt/grouped/_offline.py
Adds grouped dataset aggregation and the weighted group reducer used by finetuning.
Polymer builder
dpa_adapt/grouped/_polymer.py
Adds PolymerBuilder for grouped polymer dataset construction, scaling, and output writing.
Finetuner and trainer grouped integration
dpa_adapt/finetuner.py, dpa_adapt/trainer.py
Adds canonical pooling parsing, grouped frozen-sklearn fit/predict, and grouped-mode trainer inference.
CLI wiring
dpa_adapt/cli.py
Adds data mark-groups CLI routing and argument parsing.
DPA-ADAPT tests
source/tests/dpa_adapt/*.py
Adds tests covering grouped assembly writing, marker generation, offline aggregation, grouped finetuning, hardening, polymer builder, and pooling.

Estimated code review effort: 5 (Critical) | ~120 minutes
Suggested labels: new feature, Python, Docs
Suggested reviewers: iProzd, njzjz

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding grouped embedding support for system-level property prediction in dpa-adapt.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 13

🧹 Nitpick comments (4)
deepmd/pt/model/model/group_property_model.py (1)

298-316: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Vectorize the per-group fparam consistency check.

This loop is O(n_groups) Python iterations, each re-scanning all nframes rows via inverse == group_index. The scatter+gather+allclose pattern already used in GroupPropertyLoss._group_labels (deepmd/pt/loss/group_property.py) can validate per-group consistency without a Python loop, which matters for training with large numbers of groups per batch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deepmd/pt/model/model/group_property_model.py` around lines 298 - 316, The
per-group fparam consistency check in GroupPropertyModel is still using a Python
loop with repeated inverse == group_index scans, which is too slow for large
batches. Refactor the fparam handling in the GroupPropertyModel path to use the
same vectorized scatter/gather-style validation pattern as
GroupPropertyLoss._group_labels, so consistency is checked without iterating
over group_order in Python. Keep the existing reshape/to/cat flow, but replace
the looped first/allclose check with a batched group-wise comparison that
preserves the same ValueError behavior for inconsistent group rows.
deepmd/pt/model/task/group_property.py (1)

29-38: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse ActivationFn here. _activation() only accepts tanh/relu/gelu/linear/none, while the shared PT utility already handles relu6, softplus, sigmoid, silu, gelu_tf, and custom SiLU variants. Using the common helper keeps this task aligned with the rest of the PT stack and avoids a second activation whitelist.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deepmd/pt/model/task/group_property.py` around lines 29 - 38, The _activation
helper in group_property.py is using a separate hardcoded whitelist, so update
it to reuse the shared ActivationFn utility instead of manually mapping only
tanh/relu/gelu/linear/none. Locate the _activation function and replace the
custom branching with the common PT activation helper so it supports the broader
set of activations already handled elsewhere, while preserving the same return
type and fallback error behavior for unsupported names.
source/tests/pt/test_group_property.py (2)

163-163: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Quadratic list-flattening pattern (sum(..., [])).

Ruff flags all three occurrences as RUF017 (quadratic list summation). Per coding guidelines, ruff check . must pass before committing or CI will fail.

♻️ Proposed fix
+import itertools
+
     rank_frames = [set(sum(rank0, [])), set(sum(rank1, []))]
+    rank_frames = [set(itertools.chain.from_iterable(rank0)), set(itertools.chain.from_iterable(rank1))]

Apply the same itertools.chain.from_iterable(...) substitution at lines 188 and 212.

Also applies to: 188-188, 212-212

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@source/tests/pt/test_group_property.py` at line 163, The test helpers in
test_group_property.py use quadratic list flattening via sum(..., []) in the
rank frame construction, which triggers Ruff RUF017. Update the affected
rank_frames and related flattening sites to use itertools.chain.from_iterable
for flattening instead, and apply the same substitution to the other two
matching occurrences in this test module so ruff check . passes cleanly.

Sources: Coding guidelines, Linters/SAST tools


192-192: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Unused loop variable rank (B007).

rank is never referenced in the loop body; ruff flags this and, per coding guidelines, ruff check . must pass before committing.

♻️ Proposed fix
-    for rank, frames in enumerate(frames_by_rank):
+    for _rank, frames in enumerate(frames_by_rank):
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@source/tests/pt/test_group_property.py` at line 192, The loop in
test_group_property.py introduces an unused variable `rank`, which triggers ruff
B007. Update the `for` loop that iterates over `frames_by_rank` to remove the
unused `rank` binding, since only `frames` is needed in the loop body. Keep the
rest of the test logic unchanged and ensure the `frames_by_rank` iteration still
works with the new loop variable pattern.

Sources: Coding guidelines, Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@deepmd/pt/model/model/group_property_model.py`:
- Around line 329-336: `GroupPropertyModel` currently has `serialize()` but no
matching `deserialize()` classmethod, so serialized checkpoints cannot be
restored; add the inverse loader alongside `serialize()` and align it with the
patterns used by other `BaseModel` subclasses in this module. Reconstruct the
model from the serialized dict by restoring the descriptor, fitting network, and
type map, and make sure the new `deserialize()` is discoverable on
`GroupPropertyModel` so saved state can round-trip cleanly.
- Around line 182-201: The `charge_spin` argument is being accepted by
`GroupPropertyModel` but is not passed through to the descriptor call, so
charge-spin conditioning is silently ignored. Update the model’s forward/path
that invokes `self.descriptor(...)` to forward `charge_spin` whenever
`has_chg_spin_ebd()`, `has_default_chg_spin()`, or `get_default_chg_spin()`
indicate support, and keep the behavior consistent with the descriptor’s
expected inputs. Use the `GroupPropertyModel` and `self.descriptor` call site to
locate the fix.
- Around line 248-266: The fallback pool mask in group_property_model.py
currently uses all ones, which treats padded/virtual atoms as real atoms when
pool_mask is omitted. Update the logic in the frame-embedding path around
normalize_pool_mask_tensor and frame_embedding so the default mask excludes
virtual atoms by deriving it from atype >= 0 (or make pool_mask mandatory),
while keeping the existing zero-mask validation and weighted sum behavior
intact.

In `@deepmd/pt/utils/dataloader.py`:
- Around line 194-209: The distributed grouped batching path in
distributed_grouped_frame_batches() can create empty rank-local samplers when
the number of groups is smaller than the world size, which leads to
cycle_iterator() never making progress on some ranks. Add an early validation in
the dataloader path around the GroupDistributedBatchSampler construction to
reject this configuration, or adjust the grouping logic so each rank gets at
least one batch before DDP starts. Use the distributed branch in
deepmd/pt/utils/dataloader.py and the GroupDistributedBatchSampler /
cycle_iterator flow to locate the fix.
- Around line 354-369: `GroupDistributedBatchSampler.__iter__()` currently
increments a local `_epoch`, which can cause different ranks to generate
different shuffle seeds and drift out of sync; make the epoch advancement
synchronized across ranks so `_make_batches()` uses the same `base_seed +
_epoch` everywhere. Also add a guard before calling `cycle_iterator()`/batch
generation for the case where `len(unique_groups) < num_replicas`, and handle
empty-rank cases explicitly so ranks with no assigned groups do not loop
forever.

In `@dpa_adapt/finetuner.py`:
- Around line 1681-1695: The grouped evaluate path in finetuner.py is still
loading frame-level labels via _load_labels and reshaping them to match grouped
predictions, which can mismatch because predict() on GroupedDataset returns one
row per group. Update evaluate() to mirror the grouped training flow by using
the existing _grouped branch and GroupedDataset.get_labels() when getattr(self,
"_grouped", False) is true, so the labels align with the predictions before any
metric computation.

In `@dpa_adapt/grouped/_convert.py`:
- Around line 205-245: The loop in _convert.py is unpacking natoms from per_set
but never uses it, which triggers Ruff B007. Update the iteration over per_set
in the group-id writing block to stop binding the unused natoms value, or
replace it with a throwaway variable, and keep the rest of the logic in the same
GroupMarkerResult path unchanged.
- Around line 229-234: The pool mask generation in mark_groups/_convert.py is
still gated on real_types containing negatives, so overwrite=True can leave an
old pool_mask.npy behind when the current data no longer needs a mask. Update
the pool mask handling near the real_types check to also regenerate or remove
the file deterministically when overwrite is requested, even if no negative
entries are present. Use the existing _should_write(...), POOL_MASK_KEY, and
result.wrote_pool_mask flow in the same block so overwrite=True always
reconciles the on-disk pool_mask.npy with the current derivation.

In `@dpa_adapt/grouped/_core.py`:
- Around line 316-322: The manifest and per-group write logic for fparam are
inconsistent: _has_fparam() marks tensor_fields.fparam as present for the whole
dataset, but _write_group_system only writes fparam.npy when a given
group.fparam is truthy. Update the grouped writer in _core.py so the behavior is
consistent across all groups, either by validating that every group in the
Assembly has fparam when tensor_fields.fparam is set, or by applying
fparam_schema defaults before writing. Use _has_fparam, _write_group_system, and
the tensor_fields construction to locate and align the manifest and file-writing
paths.
- Around line 280-292: In `write`, make overwrite fully reset the output by
removing any existing `systems/*` contents before recreating the directory
structure so stale grouped systems are not rediscovered later. Also update
`_write_group_system()` to treat `manifest.tensor_fields.fparam` as
authoritative and always write `fparam.npy` when the manifest expects it, even
if an individual group has no local `fparam`, so the later load/validation path
stays consistent.

In `@dpa_adapt/grouped/_offline.py`:
- Around line 131-163: The system-discovery logic in _candidate_paths and
_paths_from_directory only checks immediate subdirectories, which can miss
nested grouped systems that mark_groups already finds recursively. Update
_paths_from_directory to mirror the recursive traversal used by _walk_systems in
dpa_adapt/grouped/_convert.py so GroupedDataset and has_grouped_markers discover
all marked systems at any depth. Keep the existing filtering for valid system
directories, but recurse through child directories instead of stopping at
path.iterdir() level. Add the needed os-based walk/import in the module if you
follow the same approach as _walk_systems.
- Around line 82-89: The grouped offline descriptor extraction in
load_or_extract/extract_features is ignoring the per-system pool mask, so padded
or virtual atoms still affect the averaged embedding. Update the offline sklearn
path in grouped/_offline.py to thread pool_mask.npy through the descriptor
loading/extraction flow, and make extract_features apply that mask before
averaging so only real atoms contribute to the final descriptors. Keep the
change localized around load_or_extract(), extract_features(), and the grouped
system handling that builds descriptors from self.systems.

In `@dpa_adapt/trainer.py`:
- Around line 406-412: Grouped fparam inference is happening too late in fit(),
so _validate_fparam(...) sees self.fparam_dim as 0 and skips validation for
grouped fparam.npy inputs. Move the grouped detection and fparam_dim inference
in Trainer.fit (using _systems_are_grouped(train_sys) and
_detect_fparam_dim(train_sys)) before the preflight validation, or rerun
_validate_fparam after those values are set, so grouped inputs are validated
with the correct shape/frame expectations.

---

Nitpick comments:
In `@deepmd/pt/model/model/group_property_model.py`:
- Around line 298-316: The per-group fparam consistency check in
GroupPropertyModel is still using a Python loop with repeated inverse ==
group_index scans, which is too slow for large batches. Refactor the fparam
handling in the GroupPropertyModel path to use the same vectorized
scatter/gather-style validation pattern as GroupPropertyLoss._group_labels, so
consistency is checked without iterating over group_order in Python. Keep the
existing reshape/to/cat flow, but replace the looped first/allclose check with a
batched group-wise comparison that preserves the same ValueError behavior for
inconsistent group rows.

In `@deepmd/pt/model/task/group_property.py`:
- Around line 29-38: The _activation helper in group_property.py is using a
separate hardcoded whitelist, so update it to reuse the shared ActivationFn
utility instead of manually mapping only tanh/relu/gelu/linear/none. Locate the
_activation function and replace the custom branching with the common PT
activation helper so it supports the broader set of activations already handled
elsewhere, while preserving the same return type and fallback error behavior for
unsupported names.

In `@source/tests/pt/test_group_property.py`:
- Line 163: The test helpers in test_group_property.py use quadratic list
flattening via sum(..., []) in the rank frame construction, which triggers Ruff
RUF017. Update the affected rank_frames and related flattening sites to use
itertools.chain.from_iterable for flattening instead, and apply the same
substitution to the other two matching occurrences in this test module so ruff
check . passes cleanly.
- Line 192: The loop in test_group_property.py introduces an unused variable
`rank`, which triggers ruff B007. Update the `for` loop that iterates over
`frames_by_rank` to remove the unused `rank` binding, since only `frames` is
needed in the loop body. Keep the rest of the test logic unchanged and ensure
the `frames_by_rank` iteration still works with the new loop variable pattern.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b314c46e-0047-4939-81cc-82941207452c

📥 Commits

Reviewing files that changed from the base of the PR and between 3d73c67 and 82c1d73.

📒 Files selected for processing (36)
  • deepmd/pt/loss/__init__.py
  • deepmd/pt/loss/group_property.py
  • deepmd/pt/model/model/__init__.py
  • deepmd/pt/model/model/group_property_model.py
  • deepmd/pt/model/task/__init__.py
  • deepmd/pt/model/task/group_property.py
  • deepmd/pt/train/training.py
  • deepmd/pt/utils/dataloader.py
  • deepmd/pt/utils/grouped.py
  • deepmd/utils/argcheck.py
  • doc/dpa_adapt/assembly_scenarios.md
  • dpa_adapt/__init__.py
  • dpa_adapt/cli.py
  • dpa_adapt/data/aggregation.py
  • dpa_adapt/data/assemblies.py
  • dpa_adapt/data/grouped_convert.py
  • dpa_adapt/data/grouped_dataset.py
  • dpa_adapt/data/polymer.py
  • dpa_adapt/finetuner.py
  • dpa_adapt/grouped/__init__.py
  • dpa_adapt/grouped/_aggregation.py
  • dpa_adapt/grouped/_convert.py
  • dpa_adapt/grouped/_core.py
  • dpa_adapt/grouped/_offline.py
  • dpa_adapt/grouped/_polymer.py
  • dpa_adapt/trainer.py
  • source/tests/dpa_adapt/test_assemblies.py
  • source/tests/dpa_adapt/test_grouped_convert.py
  • source/tests/dpa_adapt/test_grouped_dataset.py
  • source/tests/dpa_adapt/test_grouped_end_to_end.py
  • source/tests/dpa_adapt/test_grouped_finetuner.py
  • source/tests/dpa_adapt/test_grouped_hardening.py
  • source/tests/dpa_adapt/test_polymer_builder.py
  • source/tests/dpa_adapt/test_pooling.py
  • source/tests/pt/test_group_property.py
  • source/tests/pt/test_group_property_hardening.py

Comment thread deepmd/pt/model/model/group_property_model.py
Comment thread deepmd/pt/model/model/group_property_model.py Outdated
Comment thread deepmd/pt/model/model/group_property_model.py
Comment thread deepmd/pt/utils/dataloader.py
Comment on lines +354 to +369
def _make_batches(self) -> list[list[int]]:
base_seed = 0 if self.seed is None else self.seed
rng = np.random.default_rng(base_seed + self._epoch)
return distributed_grouped_frame_batches(
self.group_ids,
self.max_frames,
num_replicas=self.num_replicas,
rank=self.rank,
shuffle=self.shuffle,
rng=rng,
)

def __iter__(self):
self._batches = self._make_batches()
self._epoch += 1
yield from self._batches

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the target file and related symbols first.
ast-grep outline deepmd/pt/utils/dataloader.py --view expanded || true

printf '\n=== Search symbols ===\n'
rg -n "GroupDistributedBatchSampler|distributed_grouped_frame_batches|cycle_iterator|set_epoch|_epoch|group_ids|max_frames" deepmd -S

printf '\n=== Candidate tests ===\n'
rg -n "GroupDistributedBatchSampler|distributed_grouped_frame_batches|cycle_iterator|group.*replica|num_replicas" tests deepmd/tests -S || true

printf '\n=== File size ===\n'
wc -l deepmd/pt/utils/dataloader.py

printf '\n=== Relevant slice around lines 320-420 ===\n'
sed -n '320,420p' deepmd/pt/utils/dataloader.py

Repository: deepmodeling/deepmd-kit

Length of output: 18952


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for tests / usages in the repo.
rg -n "GroupDistributedBatchSampler|distributed_grouped_frame_batches|cycle_iterator|set_epoch|_epoch|group_ids|max_frames" . -S --glob '!**/.git/**'

# Print the relevant section of the target file with line numbers.
cat -n deepmd/pt/utils/dataloader.py | sed -n '320,420p'

Repository: deepmodeling/deepmd-kit

Length of output: 30779


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '=== grouped.py outline ===\n'
ast-grep outline deepmd/pt/utils/grouped.py --view expanded || true

printf '\n=== grouped.py relevant slice ===\n'
wc -l deepmd/pt/utils/grouped.py
cat -n deepmd/pt/utils/grouped.py | sed -n '145,210p'

printf '\n=== training.py cycle_iterator slice ===\n'
cat -n deepmd/pt/train/training.py | sed -n '255,380p'

printf '\n=== group property tests slice ===\n'
cat -n source/tests/pt/test_group_property.py | sed -n '130,230p'

Repository: deepmodeling/deepmd-kit

Length of output: 13832


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '=== group_property_hardening slice ===\n'
cat -n source/tests/pt/test_group_property_hardening.py | sed -n '220,280p'

printf '\n=== lmdb dataloader set_epoch test slice ===\n'
cat -n source/tests/pt/test_lmdb_dataloader.py | sed -n '490,530p'

Repository: deepmodeling/deepmd-kit

Length of output: 3498


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '=== grouped.py full relevant region ===\n'
cat -n deepmd/pt/utils/grouped.py | sed -n '100,210p'

printf '\n=== search for set_epoch support on batch samplers ===\n'
rg -n "set_epoch" deepmd/pt source/tests/pt -S

Repository: deepmodeling/deepmd-kit

Length of output: 4912


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '=== DpLoaderSet _build_dataloaders group branch ===\n'
cat -n deepmd/pt/utils/dataloader.py | sed -n '177,214p'

printf '\n=== any guards for too-few groups / empty batch samplers ===\n'
rg -n "num_replicas|group_ids|empty|zero batches|len\\(.*\\) == 0|no batches" deepmd/pt source/tests/pt -S

Repository: deepmodeling/deepmd-kit

Length of output: 30294


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import numpy as np

def grouped_frame_batches(group_ids, max_frames, shuffle=True, rng=None):
    groups = {}
    for frame_idx, group_id in enumerate(np.asarray(group_ids).astype(np.int64, copy=False)):
        groups.setdefault(int(group_id), []).append(frame_idx)
    group_items = list(groups.values())
    if shuffle:
        rng = rng or np.random.default_rng()
        order = rng.permutation(len(group_items))
        group_items = [group_items[int(ii)] for ii in order]
    batches = []
    current = []
    limit = max(int(max_frames), 1)
    for indices in group_items:
        if current and len(current) + len(indices) > limit:
            batches.append(current)
            current = []
        current.extend(indices)
        if len(current) >= limit:
            batches.append(current)
            current = []
    if current:
        batches.append(current)
    return batches

def distributed_grouped_frame_batches(group_ids, max_frames, num_replicas, rank, shuffle=True, rng=None):
    if shuffle and rng is None:
        rng = np.random.default_rng(0)
    group_items = grouped_frame_batches(group_ids, max_frames, shuffle=shuffle, rng=rng)  # wrong helper? no
PY

Repository: deepmodeling/deepmd-kit

Length of output: 161


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import numpy as np

def group_frame_indices(group_ids):
    groups = {}
    for frame_idx, group_id in enumerate(np.asarray(group_ids).astype(np.int64, copy=False)):
        groups.setdefault(int(group_id), []).append(frame_idx)
    return list(groups.values())

def shuffle_group_items(group_items, shuffle, rng):
    if not shuffle:
        return list(group_items)
    rng = rng or np.random.default_rng()
    order = rng.permutation(len(group_items))
    return [group_items[int(ii)] for ii in order]

def pack_group_items(group_items, max_frames):
    batches = []
    current = []
    limit = max(int(max_frames), 1)
    for indices in group_items:
        if current and len(current) + len(indices) > limit:
            batches.append(current)
            current = []
        current.extend(indices)
        if len(current) >= limit:
            batches.append(current)
            current = []
    if current:
        batches.append(current)
    return batches

def distributed_grouped_frame_batches(group_ids, max_frames, num_replicas, rank, shuffle=True, rng=None):
    num_replicas = int(num_replicas)
    rank = int(rank)
    if shuffle and rng is None:
        rng = np.random.default_rng(0)
    group_items = shuffle_group_items(group_frame_indices(group_ids), shuffle=shuffle, rng=rng)
    rank_items = group_items[rank::num_replicas]
    return pack_group_items(rank_items, max_frames)

group_ids = np.array([0,0,1,1,2,3,3,4], dtype=np.int64)
for epoch0, epoch1 in [(0,0), (0,1), (1,0), (1,1)]:
    out = []
    for rank, epoch in [(0, epoch0), (1, epoch1)]:
        rng = np.random.default_rng(11 + epoch)
        out.append(distributed_grouped_frame_batches(group_ids, 3, 2, rank, True, rng))
    frames = [set(sum(batches, [])) for batches in out]
    print(f"epochs rank0={epoch0}, rank1={epoch1}")
    print("  rank0", out[0], "frames", sorted(frames[0]))
    print("  rank1", out[1], "frames", sorted(frames[1]))
    print("  disjoint", frames[0].isdisjoint(frames[1]), "cover", frames[0] | frames[1] == set(range(len(group_ids))))
    print()

# Empty-rank case: fewer groups than replicas
group_ids = np.array([0,0,1,1], dtype=np.int64)  # 2 groups, 4 replicas
for rank in range(4):
    batches = distributed_grouped_frame_batches(group_ids, 3, 4, rank, False, np.random.default_rng(0))
    print("rank", rank, "batches", batches, "len", len(batches))
PY

Repository: deepmodeling/deepmd-kit

Length of output: 837


Sync GroupDistributedBatchSampler epochs across ranks
__iter__() advances a local _epoch, but each rank restarts cycle_iterator() independently. Once ranks wrap at different times, base_seed + _epoch diverges, the shuffled group order can differ before group_items[rank::num_replicas], and groups can be duplicated or dropped across ranks.

Also guard len(unique_groups) < num_replicas: some ranks get an empty batch list, and cycle_iterator() will spin forever there.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deepmd/pt/utils/dataloader.py` around lines 354 - 369,
`GroupDistributedBatchSampler.__iter__()` currently increments a local `_epoch`,
which can cause different ranks to generate different shuffle seeds and drift
out of sync; make the epoch advancement synchronized across ranks so
`_make_batches()` uses the same `base_seed + _epoch` everywhere. Also add a
guard before calling `cycle_iterator()`/batch generation for the case where
`len(unique_groups) < num_replicas`, and handle empty-rank cases explicitly so
ranks with no assigned groups do not loop forever.

Comment thread dpa_adapt/grouped/_core.py
Comment thread dpa_adapt/grouped/_core.py
Comment thread dpa_adapt/grouped/_offline.py
Comment thread dpa_adapt/grouped/_offline.py
Comment thread dpa_adapt/trainer.py
@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.17580% with 89 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.51%. Comparing base (8de6596) to head (e148529).
⚠️ Report is 14 commits behind head on master.

Files with missing lines Patch % Lines
deepmd/pt/model/model/group_property_model.py 74.37% 51 Missing ⚠️
deepmd/pt/model/task/group_property.py 85.22% 13 Missing ⚠️
deepmd/pt/loss/group_property.py 83.58% 11 Missing ⚠️
deepmd/pt/utils/grouped.py 90.47% 8 Missing ⚠️
deepmd/pt/utils/dataloader.py 92.30% 6 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #5741      +/-   ##
==========================================
- Coverage   81.29%   79.51%   -1.79%     
==========================================
  Files         990     1018      +28     
  Lines      111018   116074    +5056     
  Branches     4234     4274      +40     
==========================================
+ Hits        90250    92292    +2042     
- Misses      19240    22240    +3000     
- Partials     1528     1542      +14     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

CodeRabbit (correctness / data integrity):
- group_property_model: forward charge_spin into the descriptor; derive the
  default pool_mask from atype>=0 so padded/virtual atoms are excluded from
  frame embeddings; add deserialize() to match serialize().
- dataloader: partition grouped batches across ranks with an epoch-independent
  seed and reshuffle only each rank's own batch order, so ranks that wrap
  epochs at different times can no longer duplicate/drop groups; grouped.py
  rejects setups with fewer groups than ranks.
- grouped/_core: clear stale system dirs on overwrite; write fparam.npy for
  every group (schema order, defaults filled) when the manifest advertises a
  uniform fparam field.
- grouped/_convert: regenerate/remove stale pool_mask.npy on overwrite; drop
  unused loop var (Ruff B007).
- grouped/_offline: discover systems recursively (match mark_groups); thread
  the pool mask into offline descriptor pooling.
- finetuner: exclude virtual atoms from offline pooling; grouped evaluate()
  uses group-level labels.
- trainer: infer grouped numb_fparam before the fparam preflight so grouped
  fparam.npy files are validated.

CodeQL / pre-commit:
- remove deprecated dpa_adapt.data grouped shims and unused imports; break the
  finetuner<->_offline import cycle; drop unnecessary del and redundant
  annotation quotes; guard possibly-uninitialized test vars; isort/ruff/
  ruff-format/mdformat.
Comment thread source/tests/dpa_adapt/test_grouped_end_to_end.py Fixed
Comment thread source/tests/dpa_adapt/test_grouped_end_to_end.py Fixed
Comment thread source/tests/dpa_adapt/test_grouped_end_to_end.py Fixed
Comment thread source/tests/dpa_adapt/test_grouped_end_to_end.py Fixed
Comment thread source/tests/dpa_adapt/test_grouped_end_to_end.py Fixed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
dpa_adapt/grouped/_core.py (2)

222-229: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Restrict overrides to ComponentSpec data fields.

hasattr(component, key) also accepts methods like validate, so a typo or bad override can replace behavior instead of being rejected.

Proposed fix
+from dataclasses import (
+    dataclass,
+    field,
+    fields,
+)
...
     def add_component(
         self, component: ComponentSpec, **overrides: Any
     ) -> ComponentSpec:
+        valid_fields = {item.name for item in fields(ComponentSpec)}
         for key, value in overrides.items():
-            if not hasattr(component, key):
+            if key not in valid_fields:
                 raise TypeError(f"Unknown ComponentSpec field: {key}")
             setattr(component, key, value)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dpa_adapt/grouped/_core.py` around lines 222 - 229, The add_component method
is allowing overrides for any attribute that exists on ComponentSpec, including
methods like validate, which can overwrite behavior instead of rejecting bad
keys. Update add_component to validate overrides only against ComponentSpec’s
data fields (for example, its declared dataclass fields) and keep the TypeError
for any unknown or non-field key so only real component data can be overridden.

350-358: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Honor fparam_schema even when all values come from defaults.

_has_fparam() ignores self.fparam_schema, so calling set_fparam_schema() with defaults but no per-group fparam leaves tensor_fields.fparam as None and skips fparam.npy entirely.

Proposed fix
     def _has_fparam(self) -> bool:
-        return any(group.fparam for group in self.groups)
+        return bool(self.fparam_schema) or any(group.fparam for group in self.groups)

Also applies to: 383-405

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dpa_adapt/grouped/_core.py` around lines 350 - 358, The grouped adaptation
path is dropping fparam data whenever only defaults are provided because
`_has_fparam()` does not account for `self.fparam_schema`. Update the logic in
the grouped core flow (the `tensor_fields` setup and the related `fparam_schema`
handling in the same class, including the later save/load path) so
`set_fparam_schema()` alone is enough to enable `fparam` processing and
`fparam.npy` generation. Use the existing `fparam_schema` and `_has_fparam()`
symbols in `dpa_adapt/grouped/_core.py` to ensure `tensor_fields["fparam"]` is
populated whenever the schema is present, even if no per-group `fparam` values
exist.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@deepmd/pt/model/model/group_property_model.py`:
- Around line 242-251: The neighbor-list construction in group_property_model’s
input handling is hard-coding mixed neighbor lists, which breaks descriptors
that need type-distinguished neighbors. Update the
extend_input_and_build_neighbor_list call inside the relevant model method to
use the descriptor’s mixed_types() result from the descriptor/model contract, or
add an explicit initialization-time check in the group_property_model path to
reject unsupported non-mixed descriptors. Keep the fix centered on the
mixed_types() behavior so the selected descriptor and neighbor-list mode stay
consistent.

In `@dpa_adapt/grouped/_core.py`:
- Around line 326-343: Sanitized group system paths can collide when different
group.key values map to the same _safe_name(), causing _write_group_system and
the manifest entries built in the self.groups loop to target the same directory.
Update the grouped/_core.py logic around _safe_name, systems_root,
_write_group_system, and _group_manifest so each group gets a unique system path
even after sanitization, for example by incorporating group_idx or another
stable disambiguator into the directory name before writing and recording the
manifest.

---

Outside diff comments:
In `@dpa_adapt/grouped/_core.py`:
- Around line 222-229: The add_component method is allowing overrides for any
attribute that exists on ComponentSpec, including methods like validate, which
can overwrite behavior instead of rejecting bad keys. Update add_component to
validate overrides only against ComponentSpec’s data fields (for example, its
declared dataclass fields) and keep the TypeError for any unknown or non-field
key so only real component data can be overridden.
- Around line 350-358: The grouped adaptation path is dropping fparam data
whenever only defaults are provided because `_has_fparam()` does not account for
`self.fparam_schema`. Update the logic in the grouped core flow (the
`tensor_fields` setup and the related `fparam_schema` handling in the same
class, including the later save/load path) so `set_fparam_schema()` alone is
enough to enable `fparam` processing and `fparam.npy` generation. Use the
existing `fparam_schema` and `_has_fparam()` symbols in
`dpa_adapt/grouped/_core.py` to ensure `tensor_fields["fparam"]` is populated
whenever the schema is present, even if no per-group `fparam` values exist.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 56fdff0e-02f8-4de0-aadb-a2b1ec2d5869

📥 Commits

Reviewing files that changed from the base of the PR and between 82c1d73 and 8bb0d6b.

📒 Files selected for processing (25)
  • deepmd/pt/loss/__init__.py
  • deepmd/pt/loss/group_property.py
  • deepmd/pt/model/model/group_property_model.py
  • deepmd/pt/model/task/group_property.py
  • deepmd/pt/train/training.py
  • deepmd/pt/utils/dataloader.py
  • deepmd/pt/utils/grouped.py
  • doc/dpa_adapt/assembly_scenarios.md
  • dpa_adapt/finetuner.py
  • dpa_adapt/grouped/_aggregation.py
  • dpa_adapt/grouped/_convert.py
  • dpa_adapt/grouped/_core.py
  • dpa_adapt/grouped/_offline.py
  • dpa_adapt/grouped/_polymer.py
  • dpa_adapt/trainer.py
  • source/tests/dpa_adapt/test_assemblies.py
  • source/tests/dpa_adapt/test_grouped_convert.py
  • source/tests/dpa_adapt/test_grouped_dataset.py
  • source/tests/dpa_adapt/test_grouped_end_to_end.py
  • source/tests/dpa_adapt/test_grouped_finetuner.py
  • source/tests/dpa_adapt/test_grouped_hardening.py
  • source/tests/dpa_adapt/test_polymer_builder.py
  • source/tests/dpa_adapt/test_pooling.py
  • source/tests/pt/test_group_property.py
  • source/tests/pt/test_group_property_hardening.py
✅ Files skipped from review due to trivial changes (1)
  • doc/dpa_adapt/assembly_scenarios.md
🚧 Files skipped from review as they are similar to previous changes (22)
  • deepmd/pt/loss/init.py
  • source/tests/dpa_adapt/test_grouped_finetuner.py
  • source/tests/pt/test_group_property.py
  • deepmd/pt/train/training.py
  • source/tests/dpa_adapt/test_pooling.py
  • source/tests/dpa_adapt/test_grouped_hardening.py
  • source/tests/dpa_adapt/test_grouped_dataset.py
  • source/tests/dpa_adapt/test_assemblies.py
  • source/tests/dpa_adapt/test_polymer_builder.py
  • dpa_adapt/grouped/_offline.py
  • deepmd/pt/utils/dataloader.py
  • source/tests/dpa_adapt/test_grouped_end_to_end.py
  • deepmd/pt/loss/group_property.py
  • dpa_adapt/trainer.py
  • deepmd/pt/utils/grouped.py
  • source/tests/dpa_adapt/test_grouped_convert.py
  • dpa_adapt/grouped/_polymer.py
  • deepmd/pt/model/task/group_property.py
  • source/tests/pt/test_group_property_hardening.py
  • dpa_adapt/finetuner.py
  • dpa_adapt/grouped/_aggregation.py
  • dpa_adapt/grouped/_convert.py

Comment thread deepmd/pt/model/model/group_property_model.py
Comment thread dpa_adapt/grouped/_core.py
zirenjin added 4 commits July 6, 2026 22:03
- isort: reorder imports in group_property_model.py and finetuner.py
- disallow-caps: spell the product name "DeePMD" (not "DeepMD") in the
  grouped module/doc/test comments and docstrings
- test_grouped_end_to_end: use pytest.importorskip so the backend names are
  always bound (resolves py/uninitialized-local-variable; try/except + skip
  isn't modelled as terminating by CodeQL).
- finetuner._fit_sklearn: return without a value in the grouped branch so the
  None-returning method no longer mixes implicit/explicit returns
  (py/mixed-returns).
The grouped property head pools an un-normalized frame/group embedding
concatenated with fparam (no descriptor-style input statistics). At that
input scale tanh saturates, so the head learns only its bias and collapses
predictions to the global label mean. Direct-overfit evidence: on the same
few samples, tanh plateaus at ~30 RMSE (constant output) while GELU reaches
<0.5 and fits distinct labels.

- deepmd/pt/model/task/group_property.py: default activation_function -> "gelu"
- deepmd/utils/argcheck.py: fitting_group_property defaults activation to gelu
  (property head keeps tanh)
- dpa_adapt/trainer.py: grouped config emits activation_function="gelu"
  (a user fitting_net_params override still wins)
- test: assert the generated grouped config uses gelu

@njzjz-bot njzjz-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the large addition. I focused on the grouped-training integration and found several correctness issues that should be fixed before merge:

  1. Grouped frozen-sklearn extraction ignores real_atom_types.npy.
    The grouped writer intentionally writes type.raw as a uniform placeholder and stores the real per-frame atom types, including virtual -1 padding, in real_atom_types.npy. The frozen-sklearn descriptor extraction path still tiles system.data["atom_types"] for every frame, so heterogeneous grouped systems are described as if every atom had the first type. pool_mask only affects final pooling and cannot fix descriptor typing or neighbor-list construction.

  2. Masked offline pooling is not NaN-safe.
    The offline masked mean / sum / std paths multiply descriptors by the mask. If an excluded virtual atom has a non-finite descriptor, 0 * NaN remains NaN, and the later nan_to_num can zero/corrupt the whole pooled feature instead of dropping only the masked atoms. Please use the same torch.where(mask > 0, descriptor, 0)-style masking pattern as the grouped PT model path before reduction.

  3. Descriptor cache keys miss grouped inputs.
    The descriptor cache currently fingerprints coordinates/types/cells, but grouped frozen-sklearn features also depend on pool_mask.npy and, after fixing item 1, real_atom_types.npy. Changing group markers or mixed-type real atom types can silently reuse stale cached descriptors.

  4. Missing/HDF5 group_id handling is inconsistent between sampler and loss.
    The grouped dataloader falls back to an all-zero group id when filesystem group_id.npy cannot be loaded, treating the whole system as one group. The loss, however, falls back to one group per frame when group_id is absent. This can ignore the configured batch size, break DDP when the implicit single group is fewer than the number of ranks, and incorrectly treats HDF5 systems with valid in-data group_id as missing because the loader only scans set.* files.

  5. Grouped-mode detection only checks the first set of the first system.
    dpa_adapt decides whether to switch to group_property from a single set directory. If later systems/sets contain markers but the first one does not, grouped training is not enabled; if the first is grouped but later sets are missing markers, training fails later or becomes inconsistent. Please scan all train/valid set dirs and reject mixed marker presence with a clear error.

  6. group_reduce is implemented but not exposed in config validation.
    The fitting head supports group_reduce="mean"|"sum", but the argcheck schema reuses the property schema without adding this option, so users cannot set sum through normal input JSON even though direct Python tests cover it.

  7. Grouped frozen-sklearn does not support the CLI's comma-separated multi-target API.
    Non-grouped _load_labels() accepts list[str], and the CLI turns --target-key a,b into a list. The grouped route passes that list into GroupedDataset, which resolves a single string key and will fail on a list target.

  8. Several property-fitting options are accepted but ignored.
    The grouped fitting arg schema inherits property options, but GroupPropertyFittingNet swallows many of them via **kwargs (seed, numb_aparam, default_fparam, intensive, distinguish_types, resnet_dt, etc.). trainable list semantics are also collapsed to all(trainable). This makes normal configs look accepted while silently changing behavior.

I would recommend adding integration tests for: trained serialize/deserialize prediction equality, JSON config construction with group_reduce, grouped frozen-sklearn on mixed real_atom_types.npy, cache invalidation when masks/types change, HDF5 or in-data group_id, DDP/missing-group fallback, and grouped multi-target CLI input.

Reviewed by OpenClaw (model: custom-chat-jinzhezeng-group/gpt-5.5)

zirenjin added 7 commits July 9, 2026 02:33
FinetuneRuleBuilder already auto-selects a multitask pretrained checkpoints
only branch (and re-initializes the fitting net) when a single-task target
like GroupPropertyModel finetunes with no --model-branch/finetune_head set,
but this exact path -- direct DPA foundation-model checkpoint to
GroupPropertyModel -- had no test coverage. Add two tests: the auto-pick
+ random-fitting case, and documenting the deterministic first-branch pick
when several branches are available with no explicit selection.
…e group system names

- GroupPropertyModel.forward hard-coded mixed_types=True when building the
  neighbor list instead of calling self.mixed_types(), so descriptors that
  require type-distinguished neighbor lists silently got a mixed one.
- Assembly.write() sanitized group keys into system directory names without
  checking for collisions; two keys that sanitize to the same name (e.g.
  differing only in stripped punctuation) silently overwrote one another and
  the manifest recorded a duplicate path.

Addresses the two still-open threads from the PR deepmodeling#5741 review.
…, group_id fallback

Four correctness issues flagged in review, all specific to heterogeneous
grouped systems (padded/virtual atoms, uniform type.raw placeholder):

- frozen-sklearn extract_features() tiled the single, uniform atom_types
  array for every frame instead of reading the real, per-frame local types
  (with -1 padding) grouped systems store in real_atom_types.npy, so every
  atom in every frame was described as if it had the placeholder type.
  Added _real_atom_types_for_system() + remap_atom_types_preserving_padding()
  (which, unlike remap_atom_types, does not wrap -1 to the last checkpoint
  type via numpy negative-index fancy indexing).

- _pool_descriptor() multiplied the raw descriptor by the pool mask before
  reducing (mean/sum/std); a non-finite value on a masked/virtual atom kept
  0 * NaN == NaN and poisoned the whole frames pooled feature. Sanitize
  masked rows via torch.where(mask > 0, descrpt, 0) before any reduction,
  matching the pattern already used in GroupPropertyModel.forward.

- The descriptor cache fingerprinted coords/types/cells but not
  pool_mask.npy or real_atom_types.npy, so changing group markers or real
  atom types on otherwise-identical padded geometry silently reused a stale
  cached descriptor.

- GroupCompleteBatchSampler/GroupDistributedBatchSampler fell back to one
  giant group (all-zero group_id) when group_id.npy is missing, while
  GroupPropertyLoss falls back to one group per frame (torch.arange).  The
  mismatch let the sampler ignore batch_size and could break DDP by handing
  an implicit single group to more ranks than it has frames for. Also
  rewrote load_group_ids_for_system() to read set.* through the systems own
  DPPath dirs (already backend-resolved by DeepmdData) instead of a raw
  pathlib glob, so HDF5-backed systems are checked the same way as on-disk
  ones instead of an in-data group_id being silently reported as missing.
…st the first

_systems_are_grouped() only looked at the first set.* directory of the first
train system to decide whether to switch the fitting/loss to group_property.
If a later system had markers while the first did not, grouped mode stayed
off and that systems group labels were silently dropped; if the first had
markers but a later one did not, grouped mode turned on and then failed (or
trained inconsistently) once that system was reached. valid_systems was not
checked at all, so a grouped/ungrouped mismatch between train and valid went
unnoticed too.

Scan every set.* directory across both train_systems and valid_systems, and
raise a clear DPADataError on any mix of grouped/ungrouped sets or a
partially-marked set (some but not all of group_id/weight/pool_mask) instead
of guessing.
GroupPropertyFittingNet supports group_reduce="mean"|"sum" (the
frame-embedding -> group-embedding reduction), but fitting_group_property()
reused the property schema verbatim aside from the activation-function
default, with no group_reduce field. dargs strict-mode input.json validation
(the same check dp --pt train runs) rejected it as an unknown key, so
group_reduce="sum" was only reachable by constructing the model in Python
directly, never through normal config files.
…rouped fit

Non-grouped _load_labels() already accepts target_key as a list (the CLI
turns --target-key a,b into ["a", "b"]), stacking each keys column into a
(n_frames, n_keys) label array. GroupedDataset only accepted a single str
and resolved/loaded one property.npy-style file, so passing the same list
through the grouped route (_fit_sklearn_grouped already forwards whatever
target_key it was given) failed.

GroupedDataset.target_keys is now always a list; _read_system_group_rows
reads one label file per key from each set.* dir and stacks them per frame,
same convention as _load_labels (single key keeps its original shape,
multiple keys are column-stacked) -- aggregate_weighted_groups already
supported (n_items, task_dim) labels, so this was the only missing piece.
… options

GroupPropertyFittingNet took **kwargs and accepted the full property-schema
field list (numb_aparam, default_fparam, dim_case_embd, resnet_dt, intensive,
distinguish_types, seed, ...) since it is a standalone MLP that never went
through GeneralFitting, the base class that actually implements those. A
config setting any of them passed validation and construction, then silently
had no effect. trainable as a per-layer list was also collapsed to
all(trainable), losing the per-layer freeze the property schema documents.

- Replace **kwargs with two explicit, named, no-op parameters (type,
  mixed_types) for the two fields the generic model-building path always
  injects; any other unrecognized field now fails construction immediately
  with a normal TypeError instead of vanishing into kwargs.
- fitting_group_property() drops numb_aparam/default_fparam/dim_case_embd/
  resnet_dt/intensive/distinguish_types from the schema entirely, so setting
  one is a dargs strict-mode validation error, not a silently accepted no-op.
- seed is now used: layer init is deterministic when given (scoped via
  fork_rng so it does not perturb the caller's global RNG state), and
  unseeded construction still draws from the global stream as before.
- trainable=[...] now freezes each Linear layer individually instead of
  collapsing to all(trainable), matching the documented per-layer semantics
  and raising if the list length does not match len(neuron)+1.
# coords/atom_types/cells alone are identical across these two
# systems -- only set.000/real_atom_types.npy (per-frame, with -1
# padding) differs. The fingerprint must still change.
s1 = _make_system(tmp_path, "s1", natoms=3, nframes=2)
# systems -- only set.000/real_atom_types.npy (per-frame, with -1
# padding) differs. The fingerprint must still change.
s1 = _make_system(tmp_path, "s1", natoms=3, nframes=2)
s2 = _make_system(tmp_path, "s2", natoms=3, nframes=2)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants