Skip to content

Three center integration and numerical stabilities of trinity - #366

Open
floatingCatty wants to merge 2 commits into
deepmodeling:mainfrom
floatingCatty:main
Open

Three center integration and numerical stabilities of trinity#366
floatingCatty wants to merge 2 commits into
deepmodeling:mainfrom
floatingCatty:main

Conversation

@floatingCatty

@floatingCatty floatingCatty commented Jul 15, 2026

Copy link
Copy Markdown
Member

Scope

Briefly describe what this PR changes and why.

DeePTB Impact Area

Check every area that may be affected, even indirectly.

  • AtomicData / dataset loading
  • AtomicDataDict keys or tensor semantics
  • OrbitalMapper / basis-to-index mapping
  • Hamiltonian / overlap construction
  • Model build / checkpoint loading
  • NNENV / NNSK / DFTBSK / MIX behavior
  • Config schema / defaults / examples
  • Training / testing / loss behavior
  • CLI / postprocess / export
  • Documentation only

Risk And Compatibility

  • Risk level: low / medium / high
  • Public API changed: yes / no
  • CLI behavior changed: yes / no
  • Config schema changed: yes / no
  • Checkpoint compatibility affected: yes / no
  • Dataset format compatibility affected: yes / no

If any answer is "yes", explain the intended compatibility behavior.

Tests

List the tests you ran and the behavior they cover.

AI Assistance

  • This PR used AI assistance for code, tests, docs, or review.
  • AI-generated changes were manually checked for DeePTB domain correctness.
  • AI review findings were handled or explicitly waived below.

Notes:

Merge Decision

For maintainers. Fill this before merging.

  • Recommendation: merge / merge after fixes / hold
  • Remaining risks:
  • Known limitations:
  • Follow-up issue needed: yes / no

Summary by CodeRabbit

  • New Features

    • Added optional spectral balancing for improved stability in Trinity models.
    • Added EMA evaluation/checkpoint weights, gradient clipping, per-group learning-rate scaling, and warmup-cosine scheduling.
    • Added the Huber Hamiltonian loss option.
    • Added optional TF32 configuration for supported GPUs.
  • Bug Fixes

    • Improved device and dtype handling for cutoff calculations, model buffers, and loss computations.
    • Improved checkpoint compatibility and restart behavior.
  • Documentation

    • Expanded the production numerics roadmap with configuration guidance, benchmarks, and compatibility notes.

@github-actions

Copy link
Copy Markdown

This advisory review plan was generated from changed file names using trusted base-branch code.
此审查计划由受信任的 base 分支代码根据变更文件名生成,仅作为维护者辅助。

DeePTB PR Review Plan / DeePTB PR 审查计划

Risk / 风险等级: High (高) · Changed files / 变更文件: 14

Why / 风险来源

  • Config schema changed. / 配置 schema 有变更。
  • Training behavior changed. / 训练行为 有变更。
  • Loss behavior changed. / loss 行为 有变更。
  • Embedding and prediction changed. / embedding 与 prediction 有变更。
  • Documentation changed. / 文档有变更。

Recommended Review / 建议审查重点

  • Run the Maintainer Review Prompt. / 运行维护者审查 prompt。
  • Run the Test Gap Review Prompt. / 运行测试缺口审查 prompt。
  • Focus human review on config/API/data/checkpoint compatibility. / 人工重点看配置、API、数据和 checkpoint 兼容性。
Detailed risk areas
  • Config schema (High): dptb/utils/argcheck.py
  • Training behavior (High): dptb/nnops/trainer.py
  • Loss behavior (High): dptb/nnops/loss.py
  • Embedding and prediction (Medium): dptb/nn/embedding/trinity.py
  • Plugins (Medium): dptb/plugins/saver.py
  • Tests (Medium): dptb/tests/test_threecenter.py, dptb/tests/test_trainer_hardening.py
  • Documentation (Low): docs/adr/0005-trinity-sota-enhancements.md
  • Maintenance governance (Low): docs/adr/0005-trinity-sota-enhancements.md
  • Unclassified by current risk_map.md: dptb/nn/cutoff.py, dptb/nn/norm.py, dptb/nn/tensor_product.py, dptb/nn/threecenter.py, dptb/nnops/ema.py, dptb/utils/tools.py
Human review focus
  • Config schema: defaults, backward compatibility, docs/examples alignment
  • Training behavior: task detection, validation behavior, and reference datasets
  • Loss behavior: numerical targets, masks, reductions, and task-specific semantics
  • Embedding and prediction: tensor shape, irreps, batch behavior, and model compatibility
  • Plugins: checkpointing, logging, and training lifecycle
  • Tests: marker choice, regression coverage, skipped environments, and assertions
  • Documentation: stale commands, stale paths, config consistency, and Sphinx warnings
  • Maintenance governance: review guidance consistency and low-noise maintainer workflow
  • Confirm compatibility for public API, CLI/config schema, checkpoints, and datasets.
Local commands and hold conditions

Suggested local commands:

  • python scripts/ci/check_repository_hygiene.py
  • uv run python -m sphinx -b html docs docs/_build/pr-review-plan-html
  • uv run pytest ./dptb/tests -m regression
  • uv run pytest ./dptb/tests

Hold conditions:

  • CI failed and the failure is not explained.
  • The PR scope is unclear or mixes unrelated changes.
  • AI review found a plausible correctness issue that was not fixed or explicitly waived.
  • High-risk behavior changed without a regression test or documented waiver.
  • Config, checkpoint, dataset, tensor key/order, or numerical compatibility is unclear.

Advisory only. / 仅作为审查辅助。

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds opt-in spectral balancing to Trinity, trainer hardening with EMA, gradient clipping, grouped learning rates, and warmup-cosine scheduling. It also improves device-safe tensor handling, adds a Huber Hamiltonian loss, updates checkpoint behavior, and introduces regression and workflow tests.

Changes

Stability and training enhancements

Layer / File(s) Summary
Spectral balance path
dptb/nn/norm.py, dptb/nn/embedding/trinity.py, dptb/utils/argcheck.py, dptb/tests/test_threecenter.py, docs/adr/...
Adds per-l normalization and identity-initialized PerLGain modules behind spectral_balance, with constructor wiring, compatibility coverage, and ADR documentation.
Device-safe module state
dptb/nn/cutoff.py, dptb/nn/tensor_product.py, dptb/nn/threecenter.py, dptb/tests/test_threecenter.py
Moves cutoff tensors and module buffers to the active device, including non-persistent masks and er_max, with dtype and buffer tests.
Trainer controls and EMA checkpoints
dptb/nnops/ema.py, dptb/nnops/trainer.py, dptb/plugins/saver.py, dptb/utils/tools.py, dptb/utils/argcheck.py, dptb/tests/test_trainer_hardening.py, docs/adr/...
Adds grouped learning rates, warmup-cosine scheduling, gradient clipping, EMA updates, EMA validation, restart fallback, and raw/EMA checkpoint state handling.
Hamiltonian loss variants
dptb/nnops/loss.py, dptb/utils/argcheck.py, dptb/tests/test_trainer_hardening.py
Centralizes elementwise loss computation, adds the registered hamil_huber loss with straight-through energy reporting, and aligns loss tensors and weights with runtime devices.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TrainingLoop
  participant Optimizer
  participant EMA
  participant Validation
  participant Checkpoint
  TrainingLoop->>Optimizer: compute gradients and optimizer.step()
  TrainingLoop->>EMA: update shadow parameters
  Validation->>EMA: average_parameters()
  EMA-->>Validation: EMA weights
  Checkpoint->>EMA: average_parameters()
  EMA-->>Checkpoint: EMA model state
  Checkpoint-->>TrainingLoop: raw and EMA checkpoint state
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is just the template and lacks filled scope, impact, risk, tests, and merge decision details. Fill in the PR template with a real scope summary, affected areas, risk/compatibility notes, tests run, and merge decision details.
Docstring Coverage ⚠️ Warning Docstring coverage is 11.48% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is related to the changes, referencing Trinity three-center work and numerical stability improvements.
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
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch main

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: 12

🧹 Nitpick comments (1)
dptb/nn/embedding/trinity.py (1)

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

Use node_irreps_in for the node feature LayerNorm.

sln_n normalizes node features, but it is currently initialized with the edge irreps (self.irreps_in) instead of the node irreps (self.node_irreps_in). Since Layer currently passes the exact same irreps for both arguments, this doesn't crash at runtime, but it introduces a latent bug that will surface if node and edge irreps ever diverge.

♻️ Proposed refactor
-        self.sln_n = SeperableLayerNorm(
-            irreps=self.irreps_in,
+        self.sln_n = SeperableLayerNorm(
+            irreps=self.node_irreps_in,
             eps=5e-3,
             affine=True,
             normalization='component',
             std_balance_degrees=True,
             per_l=spectral_balance,
🤖 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 `@dptb/nn/embedding/trinity.py` around lines 1013 - 1020, Update the
SeperableLayerNorm initialization for sln_n to use self.node_irreps_in instead
of self.irreps_in, while leaving the edge-feature normalization configuration
unchanged.
🤖 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 `@docs/adr/0005-trinity-sota-enhancements.md`:
- Around line 188-210: Revise the compatibility statement introducing the
train_options flags so it excludes allow_tf32 from the claim that all defaults
preserve previous behavior. Keep the allow_tf32 entry explicit that its default
is false and therefore changes the prior TF32 behavior.

In `@dptb/nn/threecenter.py`:
- Around line 177-181: Update ThreeCenterFactorized so compute_P, to_feature,
to_reduced, and forward derive dtype and device from a moved tensor such as the
er_max buffer or an existing parameter, rather than cached
self.dtype/self.device attributes; update the affected test in
dptb/tests/test_threecenter.py lines 487-506 only as needed to verify behavior
after module device or dtype moves. In dptb/nn/threecenter.py lines 177-181,
remove or stop relying on stale plain attributes while preserving the registered
er_max buffer.

In `@dptb/nnops/ema.py`:
- Around line 41-45: Update EMA.copy_to to validate that an explicitly supplied
parameters collection has the same count as the EMA shadow parameters before
copying; reject mismatches rather than allowing zip to silently truncate, while
preserving the existing filtering and copy behavior for matching collections.

In `@dptb/nnops/loss.py`:
- Around line 410-425: Add the missing _elem_loss method to EigHamLoss before
forward, matching the loss computation used by HamilLossAbs: combine loss1 and
the square root of loss2 with the 0.5 weighting, returning a torch.Tensor so all
existing _elem_loss calls in forward work.
- Around line 768-770: Update the loss module initialization to register
onsite_weight and hopping_weight via PyTorch’s register_buffer instead of plain
tensor attributes, preserving their existing initialization values and device
while ensuring model.to(device) moves both buffers with the module.

In `@dptb/nnops/trainer.py`:
- Around line 224-233: Update the checkpoint restore logic around trainer.ema
and raw_model_state_dict so raw training weights are loaded whenever the
checkpoint contains them, regardless of whether trainer.ema is enabled. After an
EMA state restore fails in the existing ValueError handler, reinitialize the EMA
shadow from the restored raw model weights, preserving deployment-weight
restoration for successful EMA loads.
- Around line 214-222: Update the checkpoint restoration loop in Trainer so
optimizer and scheduler state are restored as a single atomic operation: stage
or validate both states before applying either, and if either load_state_dict
call raises ValueError, RuntimeError, or KeyError, discard both restored states
and retain fresh optimizer and scheduler instances. Preserve the existing
warning behavior while ensuring no partially restored pairing remains.

In `@dptb/tests/test_threecenter.py`:
- Line 553: In the test setup around sln0, split the chained assignment
statement into separate statements so the code complies with Ruff E702, while
preserving the existing values of y and off.

In `@dptb/tests/test_trainer_hardening.py`:
- Around line 283-296: Correct the restart assertion in the test around
Saver._save and Trainer.restart so it expects the next unexecuted iteration
without validating an extra increment. Use the actual iteration-plugin save path
if needed, or assert that the restarted trainer.iter equals the value stored by
_save; keep the EMA restoration checks unchanged.
- Around line 100-114: Update test_ema_state_dict_roundtrip_and_mismatch to use
a model exposing a different number of parameter tensors for bad, rather than
another Linear layer with two parameters. Keep the existing ValueError assertion
so the test exercises the EMA parameter-count mismatch path.

In `@dptb/utils/argcheck.py`:
- Line 360: The warmup_cos scheduler requires per-iteration learning-rate
updates but can currently be used with the trainer’s per-epoch default. In
dptb/utils/argcheck.py lines 360-360, reject warmup_cos unless
update_lr_per_iter=True or enable that setting automatically; in
dptb/utils/tools.py lines 231-248, make the scheduler/trainer stepping contract
explicit and add coverage for the default configuration.

In `@dptb/utils/tools.py`:
- Around line 168-172: The group_key function must preserve the layer index for
embedding layer names. Update its embedding branch to return a key containing
the first three components, such as embedding.layers.0, while retaining the
existing fallback behavior for other names.

---

Nitpick comments:
In `@dptb/nn/embedding/trinity.py`:
- Around line 1013-1020: Update the SeperableLayerNorm initialization for sln_n
to use self.node_irreps_in instead of self.irreps_in, while leaving the
edge-feature normalization configuration unchanged.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: aeaa15dd-e7bd-4ce0-b161-91306f39cf04

📥 Commits

Reviewing files that changed from the base of the PR and between 1dcc7f6 and 7110ac1.

📒 Files selected for processing (14)
  • docs/adr/0005-trinity-sota-enhancements.md
  • dptb/nn/cutoff.py
  • dptb/nn/embedding/trinity.py
  • dptb/nn/norm.py
  • dptb/nn/tensor_product.py
  • dptb/nn/threecenter.py
  • dptb/nnops/ema.py
  • dptb/nnops/loss.py
  • dptb/nnops/trainer.py
  • dptb/plugins/saver.py
  • dptb/tests/test_threecenter.py
  • dptb/tests/test_trainer_hardening.py
  • dptb/utils/argcheck.py
  • dptb/utils/tools.py

Comment on lines +188 to +210
Shipped as `train_options` flags, all defaulting to the previous behaviour so existing configs and
checkpoints are unaffected:

- `per_group_lr: true` (**P2**) — `build_wrms_param_groups` (`dptb/utils/tools.py`) splits the
optimizer into per-block groups with lr scaled by each block's weight RMS (trust ratio at init,
clamped to [0.02, 1.0], referenced to the median group RMS). On a full-mode Trinity this puts the
small-init AtomicResNet heads (`edge_prediction_h2`, `edge_prediction_s`, |w|_rms≈0.05) at lr scale
≈0.077 while the O(1) embedding stack stays at 1.0 — a **13× measured spread**, exactly the
imbalance the audit flagged. Scales compose correctly with any scheduler (each group's base_lr is
scaled independently). Verified in `test_wrms_param_groups_*`.
- `grad_clip_norm: <float>` (**P3**) — global-norm gradient clipping each step
(`clip_grad_norm_`); off at 0.0. Tames the loss spikes typical of batch_size=1 Hamiltonian fitting.
- `ema_decay: <float>` (**P3**) — `ExponentialMovingAverage` (`dptb/nnops/ema.py`) of the weights,
with the standard `(1+t)/(10+t)` warmup on the effective decay. **Validation scores and Saver
checkpoints use the averaged weights** (`model_state_dict` = EMA), while the raw training weights
are stored under `raw_model_state_dict` for exact restart. This removes the single-iteration noise
from best-checkpoint selection (the failure mode where stage-2 `best.pth` was honestly worse than
`latest`). Verified end-to-end in `test_trainer_engages_hardening_and_checkpoints_ema`.
- `lr_scheduler.type: warmup_cos` (**P3**) — linear warmup then cosine to `eta_min`
(`get_lr_scheduler`), meant to be stepped per-iteration (`update_lr_per_iter: true`). Replaces the
RoP-to-floor collapse. Verified in `test_warmup_cosine_shape`.
- `allow_tf32: false` (**P3**, default) — TF32 matmuls are now explicitly disabled unless requested;
Hamiltonian targets need the precision.

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Clarify that the TF32 default changes prior behavior.

“All defaulting to the previous behaviour” conflicts with Lines 209-210, which say TF32 is now explicitly disabled. Exclude allow_tf32 from that compatibility claim.

🤖 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 `@docs/adr/0005-trinity-sota-enhancements.md` around lines 188 - 210, Revise
the compatibility statement introducing the train_options flags so it excludes
allow_tf32 from the claim that all defaults preserve previous behavior. Keep the
allow_tf32 entry explicit that its default is false and therefore changes the
prior TF32 behavior.

Comment thread dptb/nn/threecenter.py
Comment on lines +177 to +181
self.bessel = BesselBasis(r_max=torch.tensor(er_max, dtype=dtype, device=device), num_basis=n_radial_basis, trainable=True)
# register as a buffer (with device) so model.to(device) moves it; a plain tensor attribute
# stays on CPU and breaks the polynomial_cutoff below when the model runs on GPU. Non-persistent:
# it is fully determined by the `er_max` config, so it stays out of the checkpoint (no layout change).
self.register_buffer("er_max", torch.tensor(er_max, dtype=dtype, device=device), persistent=False)

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:

sed -n '1,260p' dptb/nn/threecenter.py

Repository: deepmodeling/DeePTB

Length of output: 14855


🏁 Script executed:

sed -n '430,560p' dptb/tests/test_threecenter.py

Repository: deepmodeling/DeePTB

Length of output: 7509


🏁 Script executed:

rg -n "class BesselBasis|def forward|self\._cg|reduced_values\(|compute_P\(" dptb/nn/threecenter.py dptb/nn/radial_basis.py

Repository: deepmodeling/DeePTB

Length of output: 1219


🏁 Script executed:

sed -n '1,260p' dptb/nn/radial_basis.py

Repository: deepmodeling/DeePTB

Length of output: 4273


🏁 Script executed:

sed -n '260,520p' dptb/nn/threecenter.py

Repository: deepmodeling/DeePTB

Length of output: 7686


🏁 Script executed:

rg -n "self\.device\s*=|self\.dtype\s*=|def to\(" dptb/nn/threecenter.py dptb/nn/*.py

Repository: deepmodeling/DeePTB

Length of output: 2149


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path
text = Path("dptb/nn/threecenter.py").read_text()
for needle in ["self.dtype, self.device =", "self._cg =", "dtype=self.dtype, device=self.device", "self.device)", "self.dtype)"]:
    print(needle, text.count(needle))
PY

Repository: deepmodeling/DeePTB

Length of output: 268


ThreeCenterFactorized still caches dtype/device as plain attributes. nn.Module.to(...) moves the buffers and parameters, but it will not update self.device/self.dtype, so later allocations in compute_P, to_feature, to_reduced, and forward can still use the stale device after a move. Derive them from an existing tensor instead.

📍 Affects 2 files
  • dptb/nn/threecenter.py#L177-L181 (this comment)
  • dptb/tests/test_threecenter.py#L487-L506
🤖 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 `@dptb/nn/threecenter.py` around lines 177 - 181, Update ThreeCenterFactorized
so compute_P, to_feature, to_reduced, and forward derive dtype and device from a
moved tensor such as the er_max buffer or an existing parameter, rather than
cached self.dtype/self.device attributes; update the affected test in
dptb/tests/test_threecenter.py lines 487-506 only as needed to verify behavior
after module device or dtype moves. In dptb/nn/threecenter.py lines 177-181,
remove or stop relying on stale plain attributes while preserving the registered
er_max buffer.

Comment thread dptb/nnops/ema.py
Comment on lines +41 to +45
def copy_to(self, parameters=None):
parameters = self._params if parameters is None else [p for p in parameters if p.requires_grad]
with torch.no_grad():
for s, p in zip(self.shadow, parameters):
p.copy_(s)

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject partial EMA copies when parameter counts differ.

An explicit parameters collection with a different length is silently truncated by zip, leaving only part of the target model updated.

Proposed fix
 def copy_to(self, parameters=None):
     parameters = self._params if parameters is None else [p for p in parameters if p.requires_grad]
+    if len(parameters) != len(self.shadow):
+        raise ValueError(
+            f"EMA tracks {len(self.shadow)} parameters, but received {len(parameters)}"
+        )
     with torch.no_grad():
         for s, p in zip(self.shadow, parameters):
             p.copy_(s)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def copy_to(self, parameters=None):
parameters = self._params if parameters is None else [p for p in parameters if p.requires_grad]
with torch.no_grad():
for s, p in zip(self.shadow, parameters):
p.copy_(s)
def copy_to(self, parameters=None):
parameters = self._params if parameters is None else [p for p in parameters if p.requires_grad]
if len(parameters) != len(self.shadow):
raise ValueError(
f"EMA tracks {len(self.shadow)} parameters, but received {len(parameters)}"
)
with torch.no_grad():
for s, p in zip(self.shadow, parameters):
p.copy_(s)
🧰 Tools
🪛 Ruff (0.15.21)

[warning] 44-44: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)

🤖 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 `@dptb/nnops/ema.py` around lines 41 - 45, Update EMA.copy_to to validate that
an explicitly supplied parameters collection has the same count as the EMA
shadow parameters before copying; reject mismatches rather than allowing zip to
silently truncate, while preserving the existing filtering and copy behavior for
matching collections.

Source: Linters/SAST tools

Comment thread dptb/nnops/loss.py
Comment on lines 410 to +425
pre = data[AtomicDataDict.NODE_FEATURES_KEY][self.idp.mask_to_nrme[data[AtomicDataDict.ATOM_TYPE_KEY].flatten()]]
tgt = ref_data[AtomicDataDict.NODE_FEATURES_KEY][self.idp.mask_to_nrme[ref_data[AtomicDataDict.ATOM_TYPE_KEY].flatten()]]
onsite_loss = 0.5*(self.loss1(pre, tgt) + torch.sqrt(self.loss2(pre, tgt)))
onsite_loss = self._elem_loss(pre, tgt)

pre = data[AtomicDataDict.EDGE_FEATURES_KEY][self.idp.mask_to_erme[data[AtomicDataDict.EDGE_TYPE_KEY].flatten()]]
tgt = ref_data[AtomicDataDict.EDGE_FEATURES_KEY][self.idp.mask_to_erme[ref_data[AtomicDataDict.EDGE_TYPE_KEY].flatten()]]
hopping_loss = 0.5*(self.loss1(pre, tgt) + torch.sqrt(self.loss2(pre, tgt)))
hopping_loss = self._elem_loss(pre, tgt)

if self.overlap:
pre = data[AtomicDataDict.EDGE_OVERLAP_KEY][self.idp.mask_to_erme[data[AtomicDataDict.EDGE_TYPE_KEY].flatten()]]
tgt = ref_data[AtomicDataDict.EDGE_OVERLAP_KEY][self.idp.mask_to_erme[ref_data[AtomicDataDict.EDGE_TYPE_KEY].flatten()]]
overlap_loss = 0.5*(self.loss1(pre, tgt) + torch.sqrt(self.loss2(pre, tgt)))
overlap_loss = self._elem_loss(pre, tgt)

pre = data[AtomicDataDict.NODE_OVERLAP_KEY][self.idp.mask_to_nrme[data[AtomicDataDict.ATOM_TYPE_KEY].flatten()]]
tgt = ref_data[AtomicDataDict.NODE_OVERLAP_KEY][self.idp.mask_to_nrme[ref_data[AtomicDataDict.ATOM_TYPE_KEY].flatten()]]
overlap_loss += 0.5*(self.loss1(pre, tgt) + torch.sqrt(self.loss2(pre, tgt)))
overlap_loss += self._elem_loss(pre, tgt)

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.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Missing _elem_loss method in EigHamLoss.

The forward method has been refactored to call self._elem_loss(pre, tgt), but this method was never added to EigHamLoss (unlike HamilLossAbs). This will cause an AttributeError during training.

🐛 Proposed fix

Define the missing _elem_loss method inside the EigHamLoss class before forward:

    def _elem_loss(self, pre: torch.Tensor, tgt: torch.Tensor) -> torch.Tensor:
        return 0.5 * (self.loss1(pre, tgt) + torch.sqrt(self.loss2(pre, tgt)))
🤖 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 `@dptb/nnops/loss.py` around lines 410 - 425, Add the missing _elem_loss method
to EigHamLoss before forward, matching the loss computation used by
HamilLossAbs: combine loss1 and the square root of loss2 with the 0.5 weighting,
returning a torch.Tensor so all existing _elem_loss calls in forward work.

Comment thread dptb/nnops/loss.py
Comment on lines +768 to 770
self.onsite_weight = torch.ones(idp.num_types, device=self.device)
self.hopping_weight = torch.ones(len(idp.bond_types), device=self.device)
if isinstance(onsite_weight, float) or isinstance(onsite_weight, int):

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 | ⚡ Quick win

Register stateful tensors as buffers.

onsite_weight and hopping_weight are initialized as standard attributes rather than registered buffers. If the loss module is moved to a different device (e.g., via model.to(device)) after initialization, these tensors will remain on the original device, leading to a device mismatch crash during the forward pass.

🔧 Proposed fix

Use register_buffer so PyTorch properly manages their device placement:

-        self.onsite_weight = torch.ones(idp.num_types, device=self.device)
-        self.hopping_weight = torch.ones(len(idp.bond_types), device=self.device)
+        self.register_buffer("onsite_weight", torch.ones(idp.num_types, device=self.device))
+        self.register_buffer("hopping_weight", torch.ones(len(idp.bond_types), device=self.device))

(Note: PyTorch's __setattr__ behavior ensures that subsequent in-place modifications and reassignments to these attributes in this __init__ will safely update the registered buffer.)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
self.onsite_weight = torch.ones(idp.num_types, device=self.device)
self.hopping_weight = torch.ones(len(idp.bond_types), device=self.device)
if isinstance(onsite_weight, float) or isinstance(onsite_weight, int):
self.register_buffer("onsite_weight", torch.ones(idp.num_types, device=self.device))
self.register_buffer("hopping_weight", torch.ones(len(idp.bond_types), device=self.device))
if isinstance(onsite_weight, float) or isinstance(onsite_weight, int):
🤖 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 `@dptb/nnops/loss.py` around lines 768 - 770, Update the loss module
initialization to register onsite_weight and hopping_weight via PyTorch’s
register_buffer instead of plain tensor attributes, preserving their existing
initialization values and device while ensuring model.to(device) moves both
buffers with the module.


# unaffine per-l norm sends each l to ~unit RMS independently
sln0 = SeperableLayerNorm(irreps, eps=1e-6, affine=False, per_l=True, dtype=torch.float64)
y = sln0(x); off = 0

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Split the statements to satisfy Ruff E702.

Proposed fix
-    y = sln0(x); off = 0
+    y = sln0(x)
+    off = 0
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
y = sln0(x); off = 0
y = sln0(x)
off = 0
🧰 Tools
🪛 Ruff (0.15.21)

[error] 553-553: Multiple statements on one line (semicolon)

(E702)

🤖 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 `@dptb/tests/test_threecenter.py` at line 553, In the test setup around sln0,
split the chained assignment statement into separate statements so the code
complies with Ruff E702, while preserving the existing values of y and off.

Source: Linters/SAST tools

Comment on lines +100 to +114
def test_ema_state_dict_roundtrip_and_mismatch():
torch.manual_seed(0)
m = torch.nn.Linear(3, 3)
ema = ExponentialMovingAverage(m.parameters(), decay=0.9)
ema.update()
sd = ema.state_dict()
ema2 = ExponentialMovingAverage(m.parameters(), decay=0.9)
ema2.load_state_dict(sd)
for a, b in zip(ema.shadow, ema2.shadow):
assert torch.allclose(a, b)
# loading into a model with a different #params must error, not silently corrupt
other = torch.nn.Linear(5, 5)
bad = ExponentialMovingAverage(other.parameters(), decay=0.9)
with pytest.raises(ValueError):
bad.load_state_dict(sd)

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Exercise an actual EMA parameter-count mismatch.

Linear(3, 3) and Linear(5, 5) both expose two parameters, so this reaches the shape check rather than the asserted count check. Use a model with a different number of parameter tensors.

Proposed fix
-    other = torch.nn.Linear(5, 5)
+    other = torch.nn.Sequential(
+        torch.nn.Linear(3, 3),
+        torch.nn.Linear(3, 3),
+    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_ema_state_dict_roundtrip_and_mismatch():
torch.manual_seed(0)
m = torch.nn.Linear(3, 3)
ema = ExponentialMovingAverage(m.parameters(), decay=0.9)
ema.update()
sd = ema.state_dict()
ema2 = ExponentialMovingAverage(m.parameters(), decay=0.9)
ema2.load_state_dict(sd)
for a, b in zip(ema.shadow, ema2.shadow):
assert torch.allclose(a, b)
# loading into a model with a different #params must error, not silently corrupt
other = torch.nn.Linear(5, 5)
bad = ExponentialMovingAverage(other.parameters(), decay=0.9)
with pytest.raises(ValueError):
bad.load_state_dict(sd)
def test_ema_state_dict_roundtrip_and_mismatch():
torch.manual_seed(0)
m = torch.nn.Linear(3, 3)
ema = ExponentialMovingAverage(m.parameters(), decay=0.9)
ema.update()
sd = ema.state_dict()
ema2 = ExponentialMovingAverage(m.parameters(), decay=0.9)
ema2.load_state_dict(sd)
for a, b in zip(ema.shadow, ema2.shadow):
assert torch.allclose(a, b)
# loading into a model with a different `#params` must error, not silently corrupt
other = torch.nn.Sequential(
torch.nn.Linear(3, 3),
torch.nn.Linear(3, 3),
)
bad = ExponentialMovingAverage(other.parameters(), decay=0.9)
with pytest.raises(ValueError):
bad.load_state_dict(sd)
🧰 Tools
🪛 Ruff (0.15.21)

[warning] 108-108: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)

🤖 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 `@dptb/tests/test_trainer_hardening.py` around lines 100 - 114, Update
test_ema_state_dict_roundtrip_and_mismatch to use a model exposing a different
number of parameter tensors for bad, rather than another Linear layer with two
parameters. Keep the existing ValueError assertion so the test exercises the EMA
parameter-count mismatch path.

Comment on lines +283 to +296
ckpt_dir = tmp_path / "ckpt"; ckpt_dir.mkdir()
saver = Saver(); saver.register(trainer, str(ckpt_dir))
saver._save("trinity.iter", trainer.model, trainer.model.model_options,
trainer.common_options, trainer.train_options)
ckpt = str(ckpt_dir / "trinity.iter.pth")
iter_at_save = trainer.iter
ema_shadow_at_save = [s.clone() for s in trainer.ema.shadow]

# ---- restart: exact resume ---- (train.py always passes common_options)
r = Trainer.restart(checkpoint=ckpt, train_datasets=make_ds(), common_options=dict(common))
assert r.iter == iter_at_save + 1 # resumes at the next iteration
assert r.ema is not None
for a, b in zip(r.ema.shadow, ema_shadow_at_save):
assert torch.allclose(a, b), "EMA shadow not restored on restart"

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not validate an extra iteration increment as an exact restart.

After two completed iterations, trainer.iter is already the next iteration. _save() stores that value, while restart adds another one; the assertion therefore codifies a skipped counter value. Exercise the real iteration-plugin save path or require the restarted value to equal the next unexecuted iteration.

🧰 Tools
🪛 Ruff (0.15.21)

[error] 283-283: Multiple statements on one line (semicolon)

(E702)


[error] 284-284: Multiple statements on one line (semicolon)

(E702)


[warning] 295-295: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)

🤖 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 `@dptb/tests/test_trainer_hardening.py` around lines 283 - 296, Correct the
restart assertion in the test around Saver._save and Trainer.restart so it
expects the next unexecuted iteration without validating an extra increment. Use
the actual iteration-plugin save path if needed, or assert that the restarted
trainer.iter equals the value stored by _save; keep the EMA restoration checks
unchanged.

Comment thread dptb/utils/argcheck.py
Argument("linear", dict, LinearLR()),
Argument("rop", dict, ReduceOnPlateau(), doc="rop: reduce on plateau"),
Argument("cos", dict, CosineAnnealingLR(), doc="cos: cosine annealing"),
Argument("warmup_cos", dict, WarmupCosineLR(), doc="warmup_cos: linear warmup then cosine annealing (step per-iteration)"),

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

warmup_cos can silently run on the wrong time scale. The scheduler counts iterations, while the independently configured trainer defaults to stepping schedulers per epoch.

  • dptb/utils/argcheck.py#L360-L360: reject warmup_cos unless update_lr_per_iter=True, or make the selection enable it automatically.
  • dptb/utils/tools.py#L231-L248: make the stepping requirement explicit in the scheduler/trainer contract and cover the default configuration with a test.
📍 Affects 2 files
  • dptb/utils/argcheck.py#L360-L360 (this comment)
  • dptb/utils/tools.py#L231-L248
🤖 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 `@dptb/utils/argcheck.py` at line 360, The warmup_cos scheduler requires
per-iteration learning-rate updates but can currently be used with the trainer’s
per-epoch default. In dptb/utils/argcheck.py lines 360-360, reject warmup_cos
unless update_lr_per_iter=True or enable that setting automatically; in
dptb/utils/tools.py lines 231-248, make the scheduler/trainer stepping contract
explicit and add coverage for the default configuration.

Comment thread dptb/utils/tools.py
Comment on lines +168 to +172
def group_key(name):
parts = name.split(".")
if len(parts) >= 3 and parts[0] == "embedding":
return ".".join(parts[:2])
return parts[0]

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Include the layer index in WRMS group keys.

For names such as embedding.layers.0.*, parts[:2] produces embedding.layers, combining every message-passing layer despite the documented per-layer grouping.

Proposed fix
     def group_key(name):
         parts = name.split(".")
-        if len(parts) >= 3 and parts[0] == "embedding":
+        if len(parts) >= 3 and parts[:2] == ["embedding", "layers"]:
+            return ".".join(parts[:3])
+        if len(parts) >= 2 and parts[0] == "embedding":
             return ".".join(parts[:2])
         return parts[0]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def group_key(name):
parts = name.split(".")
if len(parts) >= 3 and parts[0] == "embedding":
return ".".join(parts[:2])
return parts[0]
def group_key(name):
parts = name.split(".")
if len(parts) >= 3 and parts[:2] == ["embedding", "layers"]:
return ".".join(parts[:3])
if len(parts) >= 2 and parts[0] == "embedding":
return ".".join(parts[:2])
return parts[0]
🤖 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 `@dptb/utils/tools.py` around lines 168 - 172, The group_key function must
preserve the layer index for embedding layer names. Update its embedding branch
to return a key containing the first three components, such as
embedding.layers.0, while retaining the existing fallback behavior for other
names.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant