Skip to content

fix(dpa4): align pt_expt training and native-spin fine-tuning - #5963

Merged
wanghan-iapcm merged 3 commits into
deepmodeling:masterfrom
OutisLi:pr/dpa4
Aug 12, 2026
Merged

fix(dpa4): align pt_expt training and native-spin fine-tuning#5963
wanghan-iapcm merged 3 commits into
deepmodeling:masterfrom
OutisLi:pr/dpa4

Conversation

@OutisLi

@OutisLi OutisLi commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • align the pt_expt DPA4 parameter set and precision behavior with the PT reference
  • keep compiled training and evaluation graphs mode-specific
  • make native-spin DPA4 fine-tuning from a spin-free checkpoint function-preserving and trainable

Details

The pt_expt wrapper treated the bare NumPy weights in FrameExpand and FrameContract as buffers, so they never reached the optimizer. It also used NumPy-only operations in ReducedEquivariantRMSNorm, coupled inference AMP to the training switch, and reused a train-mode compiled graph during evaluation. This change promotes the missing trainable arrays, makes the norm tensor-safe, separates training and inference precision policies, and caches compiled lowers independently for train and eval mode.

Native-spin fine-tuning had two related state-transfer problems. Per-type spin gates derived from use_spin were persisted in checkpoints, allowing an all-zero gate from a spin-free pretraining to override the fine-tune configuration. In addition, dormant randomly initialized spin routes became active at full amplitude when a magnetic type was introduced. Configuration-derived gates are now non-persistent and archived copies are ignored when loading older checkpoints; output-bias calibration receives the native moment; and native-spin models no longer fabricate a virtual-atom scale.

DPA4 spin routes now initialize at the zero function. prepare_finetune resets dormant routes only when activating a spin-free checkpoint, preserves already-trained matching routes, and rejects reassignment to a different magnetic-type set. The environment-spin gate moves after the quadratic form so it retains a nonzero gradient at zero. Versioned migration preserves the function of existing checkpoints, and the reset capability is forwarded through atomic-model compositions such as ZBL bridging.

This PR contains no DPA4C-specific or LMDB changes.

Checks

  • SeZM descriptor, Triton-dispatch, model, compile, native-spin, and LoRA tests: 119 passed and 27 skipped in the full run; the two Warp-dependent tests that the sandbox could not compile were rerun outside it and passed with all 24 subtests
  • DPA4 dpmodel, PT/pt_expt gradient parity, pt_expt descriptor/model, native-spin, fine-tune, and compiled dynamic-shape tests: 130 passed
  • full source/tests/pt_expt/test_training.py: 50 passed
  • pre-commit hooks and git diff --check

Summary by CodeRabbit

  • New Features

    • Native-spin models now accept per-atom spin data during prediction and compiled training.
    • Added independent training and inference controls for AMP and TF32 precision.
    • Compilation now caches separate training and evaluation graphs.
    • Native-spin fine-tuning validates pretrained model compatibility and magnetic element settings.
  • Bug Fixes

    • Improved migration and loading of older DPA4 and SeZM checkpoints.
    • Corrected restoration of spin configuration and dormant parameters.
    • Improved compatibility with legacy exported model state.
    • Fixed spin embedding initialization and gradient behavior.

@github-actions github-actions Bot added the Python label Aug 9, 2026
@OutisLi
OutisLi requested a review from wanghan-iapcm August 9, 2026 14:00
@OutisLi
OutisLi marked this pull request as ready for review August 9, 2026 14:00
Copilot AI lite review requested due to automatic review settings August 9, 2026 14:00

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread deepmd/pt/train/training.py Fixed
Comment thread deepmd/dpmodel/model/base_model.py Fixed
@coderabbitai

coderabbitai Bot commented Aug 9, 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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ea742ed8-0925-415e-baba-6cb0c9ee2f92

📥 Commits

Reviewing files that changed from the base of the PR and between fd19534 and 3be0356.

📒 Files selected for processing (1)
  • source/tests/pt_expt/test_training.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • source/tests/pt_expt/test_training.py

📝 Walkthrough

Walkthrough

The pull request updates native-spin forwarding, descriptor migration, configuration-derived checkpoint state, fine-tuning validation, AMP controls, matmul precision policies, and mode-specific compiled execution.

Changes

Native spin and descriptor migration

Layer / File(s) Summary
Spin forwarding and configuration
deepmd/dpmodel/atomic_model/..., deepmd/dpmodel/descriptor/..., deepmd/dpmodel/model/..., deepmd/utils/spin.py
Graph inference forwards optional spin inputs. Spin masks become configuration-derived. Virtual-scale configuration becomes optional.
Descriptor spin behavior and migration
deepmd/dpmodel/descriptor/..., deepmd/pt/model/descriptor/..., source/tests/...descriptor...
DPA4 and SeZM separate geometric and spin contributions, initialize spin routes to zero, and migrate version-1.1 state to version 1.2.
Derived state and fine-tuning
deepmd/pt_expt/common.py, deepmd/utils/finetune.py, source/tests/common/..., source/tests/pt_expt/...
Configuration-derived masks are rebuilt during initialization. Archived masks are removed during loading. Native-spin fine-tuning validates pretrained and target magnetic element sets.

Training runtime

Layer / File(s) Summary
Matmul precision policy
deepmd/pt_expt/train/utils.py, deepmd/pt_expt/train/wrapper.py, deepmd/pt_expt/train/validation.py, deepmd/utils/argcheck.py
PyTorch Exportable training and evaluation use separate matmul precision controls. The policy is applied by model wrappers and validators.
Mode-specific compiled execution
deepmd/pt_expt/train/training.py, source/tests/pt_expt/test_training.py, source/tests/pt_expt/model/test_dpa2_graph_lower.py
Compiled dense and graph forwards preserve model mode, support spin inputs, maintain mode-specific caches, use evaluation no-grad contexts, and report compilation timing after the first invocation.

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

Sequence Diagram(s)

sequenceDiagram
  participant Training
  participant ModelWrapper
  participant CompiledGraph
  participant Validator
  Training->>ModelWrapper: configure matmul precision
  ModelWrapper->>CompiledGraph: select training or evaluation cache
  CompiledGraph-->>ModelWrapper: run compiled forward
  Training->>Validator: pass precision policy
  Validator->>CompiledGraph: evaluate with evaluation precision
Loading

Possibly related issues

Possibly related PRs

Suggested reviewers: wanghan-iapcm

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 65.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the pull request's main changes to pt_expt training and native-spin fine-tuning for DPA4.
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: 4

🤖 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/dpmodel/descriptor/make_base_descriptor.py`:
- Around line 170-181: Update the documentation for reset_spin_parameters to
state that dormant spin parameters may exist in spin-free checkpoints with
arbitrary archived values, and that activation discards this checkpoint state by
resetting them before use. Preserve the description of function-preserving spin
activation and the zero-function behavior.

In `@deepmd/pt/model/descriptor/sezm.py`:
- Around line 2214-2230: Update the use_amp_infer helper docstring in
deepmd/kernels/utils.py to document that inference AMP is controlled
independently by DP_AMP_INFER and applies even when the descriptor’s use_amp is
false, matching the selection logic and
test_train_and_eval_amp_switches_are_independent.

In `@deepmd/pt/model/model/sezm_native_spin_model.py`:
- Around line 83-99: Update prepare_finetune in SeZMNativeSpinModel to call
self.atomic_model.reset_spin_parameters() rather than accessing
self.atomic_model.descriptor directly, so composed atomic models such as ZBL
bridges reset their learned child correctly. Add coverage for
spin-free-to-native fine-tuning with bridging_method="ZBL".

In `@deepmd/utils/argcheck.py`:
- Around line 5958-5959: Update the tf32_infer argument declaration in the
validating configuration, near the existing supported_backends("pt") setting, to
also include the "pt_expt" backend. Keep the existing PyTorch backend support
unchanged so generated documentation exposes this option for both trainers.
🪄 Autofix

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 Plus

Run ID: 953f6efc-a0fd-4c10-906b-3e90d35eec8d

📥 Commits

Reviewing files that changed from the base of the PR and between bc902da and 08300c4.

📒 Files selected for processing (38)
  • deepmd/dpmodel/atomic_model/base_atomic_model.py
  • deepmd/dpmodel/atomic_model/dp_atomic_model.py
  • deepmd/dpmodel/atomic_model/linear_atomic_model.py
  • deepmd/dpmodel/descriptor/dpa4.py
  • deepmd/dpmodel/descriptor/dpa4_nn/embedding.py
  • deepmd/dpmodel/descriptor/dpa4_nn/norm.py
  • deepmd/dpmodel/descriptor/make_base_descriptor.py
  • deepmd/dpmodel/model/base_model.py
  • deepmd/dpmodel/model/model.py
  • deepmd/dpmodel/model/native_spin_model.py
  • deepmd/dpmodel/model/spin_model.py
  • deepmd/dpmodel/utils/stat.py
  • deepmd/pt/model/descriptor/sezm.py
  • deepmd/pt/model/descriptor/sezm_nn/embedding.py
  • deepmd/pt/model/model/__init__.py
  • deepmd/pt/model/model/sezm_native_spin_model.py
  • deepmd/pt/train/training.py
  • deepmd/pt_expt/common.py
  • deepmd/pt_expt/descriptor/dpa4.py
  • deepmd/pt_expt/infer/deep_eval.py
  • deepmd/pt_expt/model/get_model.py
  • deepmd/pt_expt/model/native_spin_model.py
  • deepmd/pt_expt/train/training.py
  • deepmd/pt_expt/train/utils.py
  • deepmd/pt_expt/train/validation.py
  • deepmd/pt_expt/train/wrapper.py
  • deepmd/utils/argcheck.py
  • deepmd/utils/spin.py
  • source/tests/common/dpmodel/test_descrpt_dpa4.py
  • source/tests/common/dpmodel/test_zbl_bridging.py
  • source/tests/pt/model/test_descriptor_sezm.py
  • source/tests/pt/model/test_dpa4_ptexpt_grad_parity.py
  • source/tests/pt/model/test_sezm_model.py
  • source/tests/pt_expt/descriptor/test_dpa4.py
  • source/tests/pt_expt/model/test_dpa4_native_spin.py
  • source/tests/pt_expt/model/test_get_model_dpa4.py
  • source/tests/pt_expt/test_finetune.py
  • source/tests/pt_expt/test_training.py
💤 Files with no reviewable changes (1)
  • deepmd/dpmodel/model/model.py

Comment thread deepmd/dpmodel/descriptor/make_base_descriptor.py Outdated
Comment thread deepmd/pt/model/descriptor/sezm.py
Comment thread deepmd/pt/model/model/sezm_native_spin_model.py Outdated
Comment thread deepmd/utils/argcheck.py
@codecov

codecov Bot commented Aug 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.62810% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.38%. Comparing base (bc902da) to head (3be0356).

Files with missing lines Patch % Lines
deepmd/pt_expt/train/utils.py 71.42% 8 Missing ⚠️
deepmd/pt_expt/model/get_model.py 33.33% 2 Missing ⚠️
deepmd/pt_expt/train/training.py 96.61% 2 Missing ⚠️
deepmd/utils/spin.py 95.83% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #5963      +/-   ##
==========================================
- Coverage   79.60%   79.38%   -0.23%     
==========================================
  Files        1085     1085              
  Lines      126405   126558     +153     
  Branches     4598     4592       -6     
==========================================
- Hits       100631   100464     -167     
- Misses      24120    24441     +321     
+ Partials     1654     1653       -1     

☔ 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.

@github-actions github-actions Bot added the Docs label Aug 9, 2026
@iProzd iProzd added the P0 Blocks the DPA4/DPA4C release. label Aug 10, 2026
@njzjz njzjz added this to the v3.2.0 milestone Aug 10, 2026

@wanghan-iapcm wanghan-iapcm left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A lot of this is solid and I want to name it before the objection. I checked the 1.1 -> 1.2 squaring migration algebraically rather than taking the docstring's word: env_agg is linear in r_tilde, and D = sum_c A[c,i] A[c,j] contracts over the coordinate axis, so it splits into D_geometry + D_spin with no cross terms; the old code's a-scaled spin channels contributed exactly a**2 * D_spin, so a -> a**2 after the move is exact, and sign loss does not matter because the old contribution was already even in a. The AMP/TF32 train-vs-infer split is the right fix and it closes the concern njzjz raised on #5503. Extending the pt/pt_expt grad-parity test to the SO(3) and so2_norm branches is the right way to catch the FrameExpand/FrameContract promotion gap, and it genuinely fails without the fix, because those branches are off by default and no previous parametrization ever constructed those modules.

My objection is to the new reset_spin_parameters / prepare_finetune capability, and it is a design question rather than a list of defects -- which is why I am leading with it. Details inline, but the short version:

The reset is a no-op for every checkpoint this PR can produce. The same commit zero-initializes all four spin routes at construction -- spin_scale from np.ones to np.zeros, mag_layer2.w to zeros, and both adam_spin_*_weight from rng.normal(0, init_std, ...) to zeros. A dormant source built by this code holds zeros, saves zeros and loads zeros, so zeroing it again achieves nothing. The reset only does real work on a pre-1.2 checkpoint, where dormant routes carried spin_scale = 1.0 and random adam_spin_*. That is a legacy-checkpoint hazard, not a fine-tuning one.

And the legacy path already has a hook. _migrate_variables() runs on exactly those checkpoints and already rewrites spin_scale. I checked _variables() in dpa4_nn/embedding.py: spin_scale, mag_layer1.matrix, mag_layer2.matrix, adam_spin_vec_weight and adam_spin_nbr_weight are all in the serialized dict, so a migration can reach every route it would need to zero, and it can discriminate the dormant case because the source descriptor knows its own spin_flags -- which it must, since a legacy checkpoint with genuinely trained routes has to keep them.

I take the docstring's point that a bridging composition owns several children rather than one descriptor, so the model level is the natural place to ask. But that argues the plumbing is needed somewhere, not that it is needed here: the discriminating information (legacy version plus dormant flags) is available at migration time.

Why I think this is worth answering before patching anything: three of the four defects I found are in this plumbing, not in the fix underneath it. If the legacy sanitization moves into the migration that already exists, the new capability and all three go away together, rather than being fixed one predicate at a time. If there is a case I am missing that genuinely needs a model-level reset on a 1.2 checkpoint, say so and I will drop the objection -- but then it should have a test, because none of the current ones exercise a source whose spin routes are non-zero.

Two things independent of that question: a test that cannot fail on unfixed code, and the open CodeQL alert. Both inline.

Green otherwise -- 53 checks pass.

Comment thread deepmd/pt_expt/model/native_spin_model.py Outdated
Comment thread deepmd/pt_expt/model/native_spin_model.py Outdated
Comment thread deepmd/pt_expt/model/native_spin_model.py Outdated
Comment thread deepmd/pt/model/atomic_model/linear_atomic_model.py Outdated
Comment thread source/tests/pt_expt/descriptor/test_dpa4.py Outdated
Comment thread deepmd/pt/train/training.py Outdated

@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.

Two blocking fine-tuning compatibility issues remain in the new native-spin handoff. The zero-initialization and migration work look sound, but the handoff currently rejects a plain spin-free source and compares magnetic types by unstable positional masks.

Codex quota is about to reset, so I am using the remaining token budget to complete a concentrated review pass over the outstanding PRs.

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

Comment thread deepmd/pt_expt/model/native_spin_model.py Outdated
Comment thread deepmd/pt_expt/model/native_spin_model.py Outdated

@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

🧹 Nitpick comments (3)
source/tests/common/dpmodel/test_descrpt_dpa4.py (1)

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

Consider covering the spin-free use_spin=None retag.

A version-1.1 payload built without use_spin reaches neither migration branch and is still retagged to 1.2. That path is currently untested. One assertion pins the intended no-rewrite retag.

♻️ Proposed extra case
     def test_pre_spin_versions_keep_their_own_tag(self) -> None:

Add after the existing test:

    def test_spin_free_config_is_retagged_without_rewrites(self) -> None:
        """1.1 and 1.2 share their math when no spin route exists."""
        dd = make_descriptor()
        data = dd.serialize()
        data["`@version`"] = 1.1
        migrated = DescrptDPA4.deserialize(data)
        assert migrated.version == 1.2
        coord, atype, nlist = make_inputs()
        nf = atype.shape[0]
        np.testing.assert_array_equal(
            np.asarray(dd.call(coord.reshape(nf, -1), atype, nlist)[0]),
            np.asarray(migrated.call(coord.reshape(nf, -1), atype, nlist)[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 `@source/tests/common/dpmodel/test_descrpt_dpa4.py` around lines 319 - 328, Add
a regression test after test_pre_spin_versions_keep_their_own_tag named
test_spin_free_config_is_retagged_without_rewrites, covering a version-1.1
serialized descriptor with use_spin=None. Assert deserialization retags it to
version 1.2 and that its descriptor output matches the original using
make_inputs and the existing call path.
deepmd/dpmodel/atomic_model/base_atomic_model.py (1)

937-937: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider failing loudly if a spin input reaches the dense route.

Line 937 forwards spin only on the graph branch. The dense branch at lines 962-970 drops it silently. uses_graph_lower() returns False after disable_graph_lower(), so a native-spin descriptor with the escape hatch pulled would calibrate the output bias from spin-free predictions without any signal.

A guard converts that silent miscalibration into an explicit error.

🛡️ Proposed guard
             else:
+                if spin is not None:
+                    raise NotImplementedError(
+                        "native-spin output-bias calibration requires the graph "
+                        "lower; the dense route carries no per-atom moment"
+                    )
                 (
                     extended_coord,
🤖 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/dpmodel/atomic_model/base_atomic_model.py` at line 937, Update the
dense-route handling in the surrounding atomic-model flow to detect when a
non-None spin input would be dropped, and raise an explicit error instead of
continuing with spin-free calibration. Preserve the existing spin forwarding on
the graph route and allow the dense route only when spin is absent.
source/tests/common/test_finetune_utils.py (1)

308-318: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer pytest.raises over the try/except/else pattern.

pytest.raises with match expresses the same intent in fewer lines and reports a clearer failure.

♻️ Proposed refactor
 def test_finetune_rule_builder_rejects_non_native_spin_pretraining():
-    try:
+    with pytest.raises(ValueError, match="requires a native-spin pretrained model"):
         finetune.FinetuneRuleBuilder(
             _model_config(["Fe", "C"]),
             _native_spin_model_config(["Fe", "C"], [True, False]),
             change_model_params=False,
         ).build()
-    except ValueError as exc:
-        assert "requires a native-spin pretrained model" in str(exc)
-    else:
-        raise AssertionError("expected ValueError")
 
 
 def test_finetune_rule_builder_rejects_changed_magnetic_element_set():
-    try:
+    with pytest.raises(ValueError, match="active magnetic element set"):
         finetune.FinetuneRuleBuilder(
             _native_spin_model_config(["Fe", "C"], ["Fe"]),
             _native_spin_model_config(["Fe", "C"], ["C"]),
             change_model_params=False,
         ).build()
-    except ValueError as exc:
-        assert "active magnetic element set" in str(exc)
-    else:
-        raise AssertionError("expected ValueError")

Add import pytest if the module does not already import it.

Also applies to: 321-331

🤖 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/common/test_finetune_utils.py` around lines 308 - 318, Refactor
the exception assertions in
test_finetune_rule_builder_rejects_non_native_spin_pretraining and the analogous
test around lines 321–331 to use pytest.raises with a matching error message
instead of try/except/else; add the pytest import if needed and preserve the
existing ValueError expectations.
🤖 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_expt/train/training.py`:
- Around line 16-18: Run Ruff formatting on the import in training.py and
collapse the single-name contextlib import to Ruff’s preferred one-line form,
without changing its behavior.

In `@source/tests/pt/model/test_sezm_model.py`:
- Around line 1713-1744: Update
test_migrated_spin_routes_are_trainable_after_activation to load the version-1.1
legacy_state directly into the model built with use_spin=[True, False], ensuring
the magnetic-target migration and zeroing path is exercised; avoid routing
through the spin-free migrated model, or explicitly document that the two-stage
load is the supported behavior.

---

Nitpick comments:
In `@deepmd/dpmodel/atomic_model/base_atomic_model.py`:
- Line 937: Update the dense-route handling in the surrounding atomic-model flow
to detect when a non-None spin input would be dropped, and raise an explicit
error instead of continuing with spin-free calibration. Preserve the existing
spin forwarding on the graph route and allow the dense route only when spin is
absent.

In `@source/tests/common/dpmodel/test_descrpt_dpa4.py`:
- Around line 319-328: Add a regression test after
test_pre_spin_versions_keep_their_own_tag named
test_spin_free_config_is_retagged_without_rewrites, covering a version-1.1
serialized descriptor with use_spin=None. Assert deserialization retags it to
version 1.2 and that its descriptor output matches the original using
make_inputs and the existing call path.

In `@source/tests/common/test_finetune_utils.py`:
- Around line 308-318: Refactor the exception assertions in
test_finetune_rule_builder_rejects_non_native_spin_pretraining and the analogous
test around lines 321–331 to use pytest.raises with a matching error message
instead of try/except/else; add the pytest import if needed and preserve the
existing ValueError expectations.
🪄 Autofix

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 Plus

Run ID: 84c5a71e-005b-4168-8784-94aae20fe5c6

📥 Commits

Reviewing files that changed from the base of the PR and between 79f3d46 and d32b1b6.

📒 Files selected for processing (15)
  • deepmd/dpmodel/atomic_model/base_atomic_model.py
  • deepmd/dpmodel/descriptor/dpa4.py
  • deepmd/dpmodel/descriptor/dpa4_nn/embedding.py
  • deepmd/pt/model/descriptor/sezm.py
  • deepmd/pt/model/descriptor/sezm_nn/embedding.py
  • deepmd/pt_expt/descriptor/dpa4.py
  • deepmd/pt_expt/train/training.py
  • deepmd/utils/finetune.py
  • source/tests/common/dpmodel/test_descrpt_dpa4.py
  • source/tests/common/test_finetune_utils.py
  • source/tests/pt/model/test_descriptor_sezm.py
  • source/tests/pt/model/test_sezm_model.py
  • source/tests/pt_expt/descriptor/test_dpa4.py
  • source/tests/pt_expt/model/test_dpa4_native_spin.py
  • source/tests/pt_expt/test_finetune.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • source/tests/pt/model/test_descriptor_sezm.py
  • source/tests/pt_expt/descriptor/test_dpa4.py

Comment thread deepmd/pt_expt/train/training.py
Comment thread source/tests/pt/model/test_sezm_model.py

@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.

Three independent review passes on the current head converge on two blocking execution paths: plain spin-free checkpoints are still rejected before the new initialization machinery can run, and native-spin models cannot be called through the enabled compilation wrapper. I left both findings inline.

Codex quota is about to reset, so I am using the remaining token budget to complete a concentrated review pass over the outstanding PRs.

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

Comment thread deepmd/utils/finetune.py
Comment thread deepmd/pt_expt/train/training.py
- carry native spin through pt_expt compiled graph training
- honor compiled_infer across regular, full, and EMA validation
- reject dense bias calibration that would discard spin

@wanghan-iapcm wanghan-iapcm left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

All six findings from my earlier review are addressed at 3be0356, each with a test that fails on the pre-fix code:

  • The reset_spin_parameters / prepare_finetune hierarchy is gone; legacy cleanup now happens in the 1.1 -> 1.2 descriptor migration, so the unsafe forwarding loop in the PT linear atomic model went with it.
  • The native-spin compatibility check moved to FinetuneRuleBuilder and compares magnetic element sets rather than positional boolean lists, so type-map reordering and added inactive elements no longer abort.
  • Conversion from a spin-free source is still unsupported, but now fails early with an actionable message instead of a TypeError.
  • The AMP test drives _forward_blocks with a CUDA device double, so the train/eval policy is observable on CPU; use_amp=False, eval, DP_AMP_INFER=1 fails on the old gate.
  • The CodeQL uninitialized-local alert is resolved structurally.

I also checked the replacement mechanism, since it is new since my review. The migration is exact: the quadratic form sums over the coordinate axis only, so D splits into D_geometry + D_spin with no cross terms, an amplitude a contributed a2 * D_spin, and the new linear gate reproduces it with s = a2. Both dpmodel and pt carry the migration and the version bump.

Two things to keep in mind rather than block on: the migration test asserts the stored gate value, not a numeric round-trip of the forward output; and the CUDA jobs report skipping, so the autocast region itself was never executed in CI - only the selection logic was.

@wanghan-iapcm
wanghan-iapcm added this pull request to the merge queue Aug 12, 2026
Merged via the queue into deepmodeling:master with commit 7141514 Aug 12, 2026
58 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in DPA4 / DPA4C Release Aug 12, 2026
wanghan-iapcm pushed a commit to wanghan-iapcm/deepmd-kit that referenced this pull request Aug 14, 2026
…ndary

use_amp is a runtime/training policy, not model state: revert the
dpa4/sezm serialize additions (a use_amp record leaks a torch runtime
option into cross-backend records -- the jax deserializer rejects
use_amp=true -- and deepmodeling#5963 established that checkpoints must not carry
the AMP switch).

The real pt_expt bug is in model assembly: make_model handed the raw
dpmodel atomic class to the dpmodel CM, so the constructed atomic model
was converted through the auto-wrap serialize()/deserialize() round-trip
and every runtime-only option on the live descriptor was reset to its
constructor default. Hand the CM the auto-wrapped atomic class instead:
the atomic model is constructed directly as a torch module and the live
(already wrapped) descriptor/fitting are kept as-is -- no round-trip.

Regression tests exercise the public construction path (get_model with
descriptor.use_amp=false) and pin that the portable record does not
carry use_amp.
atulcthakur pushed a commit to atulcthakur/deepmd-kit-fork that referenced this pull request Aug 17, 2026
…through pt_expt assembly (deepmodeling#5960)

Users reported that compiled DPA4 training runs ~2x slower on `pt_expt`
than on `pt`. This PR is the result of chasing that: a performance bug
in how the SO3 contractions were lowered, and a correctness bug where a
configured `use_amp: false` was lost while `pt_expt` assembled the
model.

## Changes

**1. Weight broadcast across the node axis (`so3.py`, `lora.py`,
`grid_net.py`)**

`matmul(x[..., None, :], weight[None, ...])` makes the node count `N`
the matmul BATCH, so matmul broadcasts the weight to `(N, D, F, Cin,
Cout)` and autograd then reduces that whole expanded gradient
(`ExpandBackward0`) back to the parameter shape. At the water example's
sizes a 165 K-element weight expanded to 191 M elements (~0.8 GB) per
call, and the reduce was the single costliest kernel of a training step
(45.6 ms, 3x per step).

The fix batches the contraction over the small `(D, F)` axes so `N`
stays the GEMM ROW dimension and the weight is used in place.
Micro-benchmark, fwd+bwd at the real shapes: **16.48 ms -> 1.09 ms
(15x)**.

Two lookalike sites in `projection.py` are deliberately NOT changed:
their operands are `requires_grad=False` buffers, so no backward reduce
exists. Verified rather than assumed.

**2. The same lowering for the frame mixers (`_degree_batched_matmul`)**

Review found the `FrameContract` / `FrameExpand` mixers still on the
broadcast spelling. They now share one helper, `_degree_batched_matmul`,
written identically on the dpmodel side
(`dpmodel/descriptor/dpa4_nn/grid_net.py`) and the pt side
(`pt/model/descriptor/sezm_nn/grid_net.py`). Because it does no reshape,
an empty node axis (`N == 0`) flows through unchanged instead of hitting
a reshape error — pinned by a test.

**3. `use_amp` was lost during `pt_expt` model assembly (correctness)**

`use_amp` is a training-runtime policy, not model state, so it stays OUT
of the portable serialization record (this is the deepmodeling#5963 position, and
the jax deserializer actively rejects records carrying `use_amp: true`).
The bug was elsewhere: `pt_expt` assembled its model by converting an
already-populated dpmodel instance, and that conversion round-trips the
component through `deserialize(serialize())`. Anything that is
deliberately not in the portable record — `use_amp` among it — was
therefore dropped, and training silently ran under bfloat16 autocast
even when the input configured `use_amp: false`.

The fix is at the assembly boundary, not in the record: `pt_expt` now
constructs the wrapped class directly (`auto_wrapped_class(...)` in
`make_model.py`, `get_model.py`, and the bridging composition path), so
a live constructor-supplied component keeps its runtime state. The rule
is stated once, in the `auto_wrapped_class` docstring; the call sites
reference it.

An earlier revision of this PR instead added `use_amp` to `serialize()`.
That was reverted in review — it put a runtime knob into the portable
record and would have broken the jax contract.

**Also removed in review: an `enable_tf32` / `DP_TF32_INFER`
implementation for `pt_expt`.** It contributes nothing to the speedup
measured below (the benchmark card has no TF32 silicon), and deepmodeling#5958 owns
the `pt_expt` training-runtime alignment — including the documented
position that `pt_expt` always runs at `"highest"` matmul precision.
`pt_expt` therefore keeps master's warn-and-ignore behavior for
`enable_tf32`.

## Benchmark

DPA4 water example (`examples/water/dpa4`), one Tesla T4, torch 2.11,
fp32 (`use_amp: false`), batch size 6. Steady-state seconds per training
step, obtained by differencing the wall time of a 33-step and a 3-step
run of the same config, which cancels every one-time cost (import, data
load, statistics, `torch.compile` / make_fx lowering). All five arms
were measured in one session on the same machine; run-to-run variation
is about 2-3%.

**Provenance: measured at `ae720432b`**, the head at which this PR was
opened — i.e. BEFORE the review changes (change 2, the frame-mixer
lowering, and change 3's move from `serialize()` to the assembly
boundary). Change 1, which is where the entire speedup comes from, is
unmodified since. The numbers have not been re-measured on the current
head; a re-run is pending and I will post it rather than silently reuse
these.

| training mode | `pt` (reference) | `pt_expt` at master | `pt_expt` at
`ae720432b` | speedup vs master |
|---|---|---|---|---|
| eager | 0.891 s/step | 1.555 s/step | **0.921 s/step** | **1.69x** |
| compiled | 0.545 s/step | 1.611 s/step | **0.535 s/step** | **3.01x**
|

This reproduces the reported issue at master — `pt_expt` compiled was
3.0x slower than `pt` compiled, and even slower than its own eager path,
because the broadcast-weight contraction lowers to worse code under
inductor than under eager cuBLAS. After the fix `pt_expt` is at parity
with `pt`: eager within 3.4%, compiled within measurement noise.

## Known limitations

- **Backward numerics are covered for the frame mixers, not for the SO3
/ LoRA contractions.** `test_dpa4_frame_mixers.py` compares
`_degree_batched_matmul`'s weight gradient against the pt module's at
rtol/atol 1e-12. The rewritten SO3 and LoRA contractions are pinned on
the forward against an explicit `einsum` reference (rtol/atol 1e-12,
numpy and torch namespaces); their backward is still exercised only by
tracing, not compared by value.
- **The `pt` / `pt_expt` TF32 policy gap remains open.** On Ampere+
cards `pt` runs training matmuls under TF32 (`enable_tf32`, default
`True`) while `pt_expt` ignores the key with a warning; the two backends
are not speed-comparable there. Deferred to the deepmodeling#5958 training-runtime
series.
- **The residual compiled gap vs `pt` is not stable across sessions.**
An earlier session measured `pt_expt` compiled 10.8% slower than `pt`
compiled; the benchmark above measured it 1.7% faster. Both are within a
couple of run-to-run standard deviations, so I treat compiled as at
parity and the earlier gap as unconfirmed.
- The history contains churn at the GridBranch router (`7518a417c` ->
`01c58e665` -> `75459610a` -> `504bb2430` -> `157444204`): a matmul
spelling introduced, reverted, reintroduced, and finally restored to
master's line. The site is byte-identical to master in the final diff.
The degenerate GEMM that profiling found there existed only on this
branch, so it is not a fix — I have left the commits rather than
rewriting pushed history, and would squash them on request.
- Unrelated but found while benchmarking: **torch >= 2.11 ships no Volta
(CC 7.0) kernels**, and compiled training requires >= 2.11 via
`check_compile_torch_version`. Compiled DPA4 training is therefore
impossible on V100 with official wheels; T4 (CC 7.5) is the oldest card
that works.

## Tests

- `source/tests/common/dpmodel/test_dpa4_frame_mixers.py` —
`_degree_batched_matmul` vs the pt module: forward parity, the `N == 0`
contract, and weight-gradient parity.
- `source/tests/common/dpmodel/test_dpa4_lora.py` — new
`test_lora_so3_call_matches_einsum_contract`: `LoRASO3.call` against the
explicit `einsum("ndfi,difo->ndfo")` reference with a nonzero adapter,
on both the numpy and torch namespaces, for `n_focus` 1 and 2.
- `source/tests/pt_expt/model/test_get_model_dpa4.py` — `use_amp`
survives model assembly (both branches), for the plain and the
bridged/composed construction paths.
- `source/tests/common/dpmodel/test_descrpt_dpa4.py` — `use_amp` is
absent from the portable serialization record and defaults on
deserialize.
- Existing `test_grid_branch[1]`/`[2]` cover the changed SO3 contraction
against the pt implementation at rtol 1e-12.
- Run locally: 434 passed / 10 skipped across the dpa4 dpmodel, pt_expt
and cross-backend parity suites, plus the pt_expt model suite.
CUDA-gated precision-context cases were run on a T4 (29/29).

## Test status caveat — resolved

An earlier revision of this description flagged two locally failing
`pt_expt` AOTI-freeze tests
(`test_zbl_bridging.py::test_native_spin_with_bridging_graph_freeze_and_deep_eval`,
`test_dpa4_zbl_parallel.py::TestBridgedSpinGraphSelfComm::test_freeze_embeds_with_comm_artifact`)
as unadjudicated. They are now adjudicated as **pre-existing and
environmental, not caused by this branch**: a clean `upstream/master`
worktree on the same machine fails both with the identical
`InductorError: assert isinstance(index, CppCSEVariable) and
index.is_vec` (torch 2.11 CPU-SIMD codegen bug on an `atomic_add`
scatter buffer), and both tests pass on this branch with the known
workaround `torch._inductor.config.cpp.simdlen = 1` (2 passed). The same
bug is already documented in `source/tests/infer/gen_dpa4.py` /
`gen_dpa2.py`.

---------

Co-authored-by: Han Wang <wang_han@iapcm.ac.cn>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Docs enhancement P0 Blocks the DPA4/DPA4C release. Python

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

7 participants