Skip to content

Propose fix perceptual loss sqrt nan - #8414

Closed
cvbourne wants to merge 8 commits into
Project-MONAI:devfrom
cvbourne:propose-fix-perceptual-loss-sqrt-nan
Closed

Propose fix perceptual loss sqrt nan#8414
cvbourne wants to merge 8 commits into
Project-MONAI:devfrom
cvbourne:propose-fix-perceptual-loss-sqrt-nan

Conversation

@cvbourne

@cvbourne cvbourne commented Apr 7, 2025

Copy link
Copy Markdown

Fixes # 8412

Description

This PR fixes a numerical stability issue in the PerceptualLoss implementation where the normalize_tensor function can produce NaN gradients when the input values are very small.

  • Moved epsilon inside the square root calculation instead of after it
  • Increased default from 1e-10 to 1e-8 for better stability
  • Added test

Types of changes

  • Non-breaking change (fix or new feature that would not break existing functionality).
  • New tests added to cover the changes.
  • Integration tests passed locally by running ./runtests.sh -f -u --net --coverage.
  • Quick tests passed locally by running ./runtests.sh --quick --unittests --disttests.

@cvbourne cvbourne mentioned this pull request Apr 7, 2025
@KumoLiu
KumoLiu requested a review from marksgraham April 8, 2025 15:14
@KumoLiu

KumoLiu commented Apr 8, 2025

Copy link
Copy Markdown
Contributor

Thanks for the update, the changes looks fine to me.
Could you please help fix the failed checks then I could trigger the blossom tests? Thanks.

@KumoLiu
KumoLiu requested review from ericspod and virginiafdez April 8, 2025 15:16
Comment thread monai/losses/perceptual.py Outdated

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.

This file should go into an appropriate subdirectory in the tests directory. We've changed the directory structure there recently so probably tests/losses.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Roger.

# Create tensor
x = torch.zeros(2, 3, 10, 10, requires_grad=True)

optimizer = optim.Adam([x], lr=0.01)

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.

I don't think the optimizer is needed for this test?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Not needed, will remove.

x = torch.zeros(2, 3, 10, 10, requires_grad=True)

optimizer = optim.Adam([x], lr=0.01)
x_scaled = x * scale

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.

Since x is all 0, x_scaled is always going to be 0 unless you're expected float imprecision to create values here. If so, I would add a comment to mention this.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Will add 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.

I don't understand the point of this test with regards to the next one; instead of a zeros tensor, couldn't it be a random one which will be then multiplied by a really small number?

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

The changes look good to me. I'd modify one of the tests, but the rest is fine.

x = torch.zeros(2, 3, 10, 10, requires_grad=True)

optimizer = optim.Adam([x], lr=0.01)
x_scaled = x * scale

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.

I don't understand the point of this test with regards to the next one; instead of a zeros tensor, couldn't it be a random one which will be then multiplied by a really small number?

virginiafdez
virginiafdez previously approved these changes Apr 25, 2025

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

Besides my comment about the point of one of the tests, I think this PR can be merged, as long as the errors happening on the automatic tests are fixed.

@KumoLiu

KumoLiu commented May 9, 2025

Copy link
Copy Markdown
Contributor

Hi @cvbourne, could you please help resolve the DCO issue and also help take a look at the failed pipeline? Thanks.

@ericspod ericspod moved this to Backlog in MONAI v1.6 Feb 24, 2026
@coderabbitai

coderabbitai Bot commented Mar 1, 2026

Copy link
Copy Markdown
Contributor

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: 10a3748c-1a6a-46c3-abd6-9321c601a52d

📥 Commits

Reviewing files that changed from the base of the PR and between 1f60f13 and bf2c919.

📒 Files selected for processing (1)
  • tests/test_perceptual_loss_stability.py
 _________________________________________________
< Untangling your code like a pair of headphones. >
 -------------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: 0e8580e2-f108-48df-aae4-9ff5e8b0f67e

📥 Commits

Reviewing files that changed from the base of the PR and between 6d2c886 and c6c7cee.

📒 Files selected for processing (1)
  • monai/losses/perceptual.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • monai/losses/perceptual.py

Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

normalize_tensor now uses 1e-8 by default. It adds epsilon inside the squared-sum square-root calculation and removes the second denominator addition. Tests cover gradient stability for small-valued tensors and zero tensors.

Estimated code review effort: 2 (Simple) | ~12 minutes

Merge Risk: ⚪ Minimal · up to c6c7c

Tensor normalization now remains stable for zero and very small inputs, preventing NaN gradients in perceptual-loss usage. The targeted gradient-stability coverage supports merge readiness.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description explains the numerical stability fix, lists the main implementation changes, identifies the added test, and includes the applicable change-type checklist items. It omits several unchec…
Title check ✅ Passed The title clearly identifies the perceptual loss square-root NaN fix and matches the main change. Its wording is slightly informal but remains concise and specific.
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.
  • Fix all pre-merge checks with AI
✨ 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: 1

♻️ Duplicate comments (1)
tests/test_perceptual_loss_stability.py (1)

36-39: ⚠️ Potential issue | 🟡 Minor

Zero tensor negates scale parameter.

torch.zeros * scale is always zeros. To test small values, use random tensor:

Proposed fix
-        x = torch.zeros(2, 3, 10, 10, requires_grad=True)
-
-        optimizer = optim.Adam([x], lr=0.01)
-        x_scaled = x * scale
+        x = torch.randn(2, 3, 10, 10, requires_grad=True)
+        x_scaled = x * scale

This also addresses the unused name parameter warning (ARG002) since parameterized tests require the name argument for test identification.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_perceptual_loss_stability.py` around lines 36 - 39, The test
currently creates x = torch.zeros(...) then computes x_scaled = x * scale which
is always zero; replace the zero tensor with a small random tensor (e.g.,
torch.randn(...) * small_factor or torch.empty(...).normal_(mean=0,
std=small_value)) so scaling actually affects values, and ensure the
parameterized test's name parameter is used (or include it in the test id) to
avoid the unused-name warning; update references to x, x_scaled, optimizer in
the test_perceptual_loss_stability test accordingly.
🧹 Nitpick comments (2)
tests/test_perceptual_loss_stability.py (1)

17-17: Remove unused import.

torch.optim import can be removed when optimizer is deleted.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_perceptual_loss_stability.py` at line 17, Remove the now-unused
import "import torch.optim as optim" from
tests/test_perceptual_loss_stability.py because the optimizer was removed;
search for the symbol "optim" and delete the import statement so the file no
longer contains an unused dependency.
monai/losses/perceptual.py (1)

277-279: Missing docstring.

Per coding guidelines, add a docstring describing parameters (x, eps) and return value.

Proposed docstring
 def normalize_tensor(x: torch.Tensor, eps: float = 1e-8) -> torch.Tensor:
+    """Normalize tensor across channel dimension.
+
+    Args:
+        x: Input tensor with shape (B, C, ...).
+        eps: Small constant for numerical stability.
+
+    Returns:
+        Channel-normalized tensor with same shape as input.
+    """
     norm_factor = torch.sqrt(torch.sum(x**2, dim=1, keepdim=True) + eps)
-    return x / (norm_factor + eps)
+    return x / norm_factor
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@monai/losses/perceptual.py` around lines 277 - 279, Add a proper docstring to
the normalize_tensor function that documents parameters and return value:
describe param x (torch.Tensor) as the input tensor with channel dimension at
dim=1, param eps (float) as the small numerical epsilon used to stabilize the
norm computation, and describe the return as a torch.Tensor containing the input
scaled by the L2 norm per-channel (with shape preserved). Also mention behavior
with shapes and that eps prevents division by zero and the dtype of the returned
tensor matches the input.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@monai/losses/perceptual.py`:
- Around line 277-279: The normalize_tensor function currently applies eps twice
(inside the sqrt and again added to norm_factor), so change normalize_tensor to
only add eps inside the sqrt: compute norm_factor = torch.sqrt(torch.sum(x**2,
dim=1, keepdim=True) + eps) and return x / norm_factor (remove the extra + eps
in the denominator) to avoid double-counting the epsilon while keeping numerical
stability.

---

Duplicate comments:
In `@tests/test_perceptual_loss_stability.py`:
- Around line 36-39: The test currently creates x = torch.zeros(...) then
computes x_scaled = x * scale which is always zero; replace the zero tensor with
a small random tensor (e.g., torch.randn(...) * small_factor or
torch.empty(...).normal_(mean=0, std=small_value)) so scaling actually affects
values, and ensure the parameterized test's name parameter is used (or include
it in the test id) to avoid the unused-name warning; update references to x,
x_scaled, optimizer in the test_perceptual_loss_stability test accordingly.

---

Nitpick comments:
In `@monai/losses/perceptual.py`:
- Around line 277-279: Add a proper docstring to the normalize_tensor function
that documents parameters and return value: describe param x (torch.Tensor) as
the input tensor with channel dimension at dim=1, param eps (float) as the small
numerical epsilon used to stabilize the norm computation, and describe the
return as a torch.Tensor containing the input scaled by the L2 norm per-channel
(with shape preserved). Also mention behavior with shapes and that eps prevents
division by zero and the dtype of the returned tensor matches the input.

In `@tests/test_perceptual_loss_stability.py`:
- Line 17: Remove the now-unused import "import torch.optim as optim" from
tests/test_perceptual_loss_stability.py because the optimizer was removed;
search for the symbol "optim" and delete the import statement so the file no
longer contains an unused dependency.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 1e3d29b and 6d2c886.

📒 Files selected for processing (2)
  • monai/losses/perceptual.py
  • tests/test_perceptual_loss_stability.py

Comment thread monai/losses/perceptual.py Outdated
@ericspod

Copy link
Copy Markdown
Member

Hi @cvbourne if you can come back to this one and address the issues raised by us and Coderabbit, we'd still like to merge this one. Thanks!

@ericspod ericspod moved this to 🏗 In progress in MONAI Core Backlog Jun 23, 2026
@ericspod ericspod moved this from Backlog to In Progress in MONAI v1.6 Jun 23, 2026
@ericspod ericspod moved this from 🏗 In progress to 👀 In review in MONAI Core Backlog Jun 23, 2026
Signed-off-by: Eric Kerfoot <17726042+ericspod@users.noreply.github.com>
Signed-off-by: Eric Kerfoot <17726042+ericspod@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@ericspod

ericspod commented Sep 7, 2026

Copy link
Copy Markdown
Member

I think this PR has been superseded now so closing.

@ericspod ericspod closed this Sep 7, 2026
@github-project-automation github-project-automation Bot moved this from In Progress to Done in MONAI v1.6 Sep 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: 👀 In Review
Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants