Skip to content

fix(disp): div 0 error when using disp_avg under multitask - #5809

Open
OutisLi wants to merge 4 commits into
deepmodeling:masterfrom
OutisLi:pr/disp
Open

fix(disp): div 0 error when using disp_avg under multitask#5809
OutisLi wants to merge 4 commits into
deepmodeling:masterfrom
OutisLi:pr/disp

Conversation

@OutisLi

@OutisLi OutisLi commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • Bug Fixes
    • Fixed multi-task loss reporting for intervals with no sampled steps so metrics show the full expected set of loss fields with NaN values instead of missing/empty results.
    • Improved disp_avg logging to keep displayed indices and columns consistent, including NaN entries when averages can’t be computed.
  • Tests
    • Added a multi-task test to ensure disp_avg correctly handles unsampled intervals and that lcurve.out shows NaN in the expected row/columns.

Copilot AI review requested due to automatic review settings July 16, 2026 01:40
@dosubot dosubot Bot added the bug label Jul 16, 2026
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Multitask training-loss logging now handles unsampled intervals by emitting NaN values for affected loss keys. Display averaging initializes missing task accumulators, computes per-task validation results, and a regression test verifies the resulting learning-curve output.

Changes

Multitask loss logging

Layer / File(s) Summary
Loss averaging and regression validation
deepmd/pt/train/training.py, source/tests/pt/test_multitask.py
Per-task losses use task-specific step counts, missing accumulators are initialized for display averaging, unsampled tasks render NaN values, and the test validates lcurve.out steps, columns, and NaN output.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: 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 bug fix: preventing divide-by-zero in multitask training when disp_avg is enabled.
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: 2

🤖 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/train/training.py`:
- Around line 1619-1632: Update the multitask display-step handling in run()
around the disp_avg branch: perform a dummy forward pass for each unsampled
model key to populate self.train_loss_accu with its loss keys before generating
train_results, excluding l2_ fields as in the proposed flow. Move the validation
and rank-zero console logging loop outside the disp_avg conditional so
log_loss_valid and progress messages execute for both averaged and non-averaged
modes.

In `@source/tests/pt/test_multitask.py`:
- Around line 273-277: Update the lcurve.out assertions in the multitask test to
read and tokenize the header row, then assert every data row has exactly the
header’s column count. Preserve the existing displayed_steps and “nan” checks
while ensuring the unsampled task does not produce a shorter row.
🪄 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: 5ac6ebae-2ba6-49ed-b897-c162751bd612

📥 Commits

Reviewing files that changed from the base of the PR and between 1c5ff68 and 620847a.

📒 Files selected for processing (2)
  • deepmd/pt/train/training.py
  • source/tests/pt/test_multitask.py

Comment thread deepmd/pt/train/training.py
Comment thread source/tests/pt/test_multitask.py
@OutisLi
OutisLi requested a review from njzjz July 16, 2026 02:08

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 encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

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

Requesting changes because the filtered-batch path can still leave an unsampled task without the metric schema required by learning-curve output.

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

Comment thread deepmd/pt/train/training.py
@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.47619% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.84%. Comparing base (2f419f4) to head (e2a29b2).

Files with missing lines Patch % Lines
deepmd/pt/train/training.py 90.47% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #5809      +/-   ##
==========================================
- Coverage   79.06%   78.84%   -0.23%     
==========================================
  Files        1067     1067              
  Lines      123805   123815      +10     
  Branches     4510     4515       +5     
==========================================
- Hits        97891    97624     -267     
- Misses      24304    24581     +277     
  Partials     1610     1610              

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

@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: 1

🤖 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/train/training.py`:
- Around line 1715-1716: Update the unsampled-task initialization around the `if
not task_input` early return so filtered `{}` batches cannot leave
`train_results[_key]` without its metric keys. Populate the expected metric
schema independently of a consumable training batch, or continue fetching until
`get_data()` returns a valid batch, while preserving the existing `disp_avg` and
validation display behavior.
🪄 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: 28d64a76-12e4-4629-83a0-3c8a68c14e06

📥 Commits

Reviewing files that changed from the base of the PR and between 620847a and a73ebb0.

📒 Files selected for processing (2)
  • deepmd/pt/train/training.py
  • source/tests/pt/test_multitask.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • source/tests/pt/test_multitask.py

Comment on lines +1715 to +1716
if not task_input:
return

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

Handle filtered batches when initializing unsampled metrics.

When disp_avg is enabled and a task was not sampled in the interval, get_data() can return {} if min_pair_dist rejects the fetched batch. Returning early leaves train_results[_key] without metric keys. With validation configured, print_on_training() then indexes those missing training metrics and raises a KeyError at the first display; without validation, the header is incomplete and later rows become misaligned.

Please populate the metric schema without relying on one consumable training batch, or loop until a valid batch is returned.

🛠️ Proposed fix
-                            if not task_input:
-                                return
+                            while not task_input:
+                                task_input, task_label, _ = self.get_data(
+                                    is_train=True, task_key=_task_key
+                                )
🤖 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/train/training.py` around lines 1715 - 1716, Update the
unsampled-task initialization around the `if not task_input` early return so
filtered `{}` batches cannot leave `train_results[_key]` without its metric
keys. Populate the expected metric schema independently of a consumable training
batch, or continue fetching until `get_data()` returns a valid batch, while
preserving the existing `disp_avg` and validation display behavior.

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

Request changes: the unsampled-task schema is still not guaranteed when a training batch is filtered out by min_pair_dist.

initialize_task_loss_accumulator() returns immediately on an empty task_input. That leaves train_results[_task_key] empty. At the first display, print_on_training() iterates the validation metric keys and indexes the corresponding missing training keys, causing a KeyError; without validation, the generated lcurve.out header is incomplete and later rows can become misaligned.

Please populate the placeholder metric schema without depending on a single consumable batch (or keep fetching until one is usable), and add a regression test covering an unsampled task whose initialization batch is fully filtered.

All CI checks are otherwise passing.

— OpenClaw 2026.6.11

if self.train_loss_accu[_task_key]:
return
self.optimizer.zero_grad(set_to_none=True)
task_input, task_label, _ = self.get_data(

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.

[P1] Do not consume a real training batch merely to discover the metric schema

initialize_task_loss_accumulator() calls get_data(is_train=True, task_key=_task_key) for an unsampled task, and then runs only a forward pass. However, get_data() unconditionally advances that task's iterator with next(iterator) (line 2310); this batch is neither cached nor used in backward() / optimizer.step().

For example, with two tasks, disp_avg=true, and disp_freq=1, if step 1 trains model_1, the display path consumes model_2's first batch. If step 2 selects model_2, optimization starts from its second batch. Thus changing a logging-only setting (disp_freq, or whether a task happens to be sampled before the first display) changes the batches used for optimization and breaks reproducibility for a fixed seed.

Please obtain the loss-key schema without advancing the training iterator, or buffer the probe batch so that the next optimization step for this task consumes it. Add a regression test that records fid (or an equivalent batch identifier) and verifies that schema initialization does not alter the subsequent training-batch sequence.

— OpenClaw 2026.6.11

@njzjz njzjz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks — the second half of this is a clear bug fix, but the accumulator seeding worries me.

The logging hoist is right

On master the for _key in self.model_keys: loop that calls log_loss_valid and emits the _trn/_val lines sits inside the else: (non-disp_avg) branch, so a multi-task run with disp_avg: true produced no per-task log lines at all — train_results was filled and then dropped on the floor. Hoisting it to its own loop over model_keys fixes that, and threading check_total_rmse_nan=False for a task with step_count_per_task == 0 is the right way to keep the deliberate NaN from tripping the NaN guard. dict.fromkeys(task_losses, float("nan")) for the zero-step case is a clean replacement for the silent empty dict.

initialize_task_loss_accumulator runs a real training step to learn column names

self.optimizer.zero_grad(set_to_none=True)
task_input, task_label, _ = self.get_data(is_train=True, task_key=_task_key)
if not task_input:
    return
_, _, task_more_loss = self.wrapper(**task_input, cur_lr=pref_lr, label=task_label, task_key=_task_key)

I understand why: lcurve.out writes its header once, so every task's column set has to be known at the first display step, and more_loss keys only exist after a forward. But this pays for that with four side effects on a path that is supposed to be pure reporting:

  1. It consumes a training batch. get_data(is_train=True, task_key=...) advances that task's training iterator. So the task the display is about to report as "not sampled this interval" is sampled — the batch is drawn, fed forward, and thrown away. That shifts the data stream and epoch bookkeeping for that task by one batch per display step until it is first sampled naturally. Your own test has to mock dp_random.choice with an exact [0, 1] sequence, which is a hint at how sensitive this is.
  2. No torch.no_grad(). The enclosing block runs after self.wrapper.eval(), so this forward builds a full autograd graph in eval mode and discards it — wasted memory and time on every display step until the task is seeded, and for a large task that is not cheap.
  3. self.optimizer.zero_grad(set_to_none=True) mutates optimizer state from inside the display path. The non-disp_avg branch does the same thing on master, so there is precedent, but that branch is at least computing numbers it then reports; here the loss is discarded.
  4. DDP. The if not task_input: return early exit is evaluated per rank. If it can ever be true on some ranks and not others, the ranks disagree on whether to run a forward and the collectives desynchronize. Worth confirming get_data cannot return a falsy task_input on a subset of ranks.

Two directions that avoid all four:

  • Keep the keys, not the counters. The reset at the end of the display block already zeroes values and preserves keys, so a task only needs seeding if it has never been sampled since training started. If the header could be deferred until every task has been seen once, or written with a per-task placeholder set derived from the loss configuration (which terms are enabled: energy / force / virial / …) rather than from an executed more_loss, no forward is needed at all.
  • If a forward really is unavoidable, at minimum wrap it in torch.no_grad(), and draw from the validation loader rather than the training one so the training stream is untouched.

Smaller points

  • initialize_task_loss_accumulator is redefined on every display step; it does not close over anything that changes except pref_lr, so it could be a method or moved above the loop.
  • The test asserts self.assertIn("nan", data_lines[1]) — that checks the literal token appears somewhere in the row. Asserting that the NaN falls in that task's columns (via header_columns.index(...)) would pin the actual contract; as written it would still pass if the NaN landed in the wrong task's column.
  • Worth a line in the test docstring or a comment saying which task is unsampled and why, so the [0, 1] mock sequence is not load-bearing but unexplained.

Copilot AI review requested due to automatic review settings July 28, 2026 07:26

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.

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.

4 participants