Skip to content

Warn when lazy resampling upcasts non-float input to float32 (#6713) - #9044

Open
vishnukannaujia wants to merge 2 commits into
Project-MONAI:devfrom
vishnukannaujia:fix/6713-lazy-resample-dtype-warning
Open

Warn when lazy resampling upcasts non-float input to float32 (#6713)#9044
vishnukannaujia wants to merge 2 commits into
Project-MONAI:devfrom
vishnukannaujia:fix/6713-lazy-resample-dtype-warning

Conversation

@vishnukannaujia

Copy link
Copy Markdown
Contributor

Fixes #6713

Summary

When integer data (e.g. a uint8 label map) is passed through a lazy Compose, the lazy resampling path silently converts it to float32. The same pipeline with lazy=False preserves the original dtype. This mismatch is surprising and can corrupt integer label data downstream with no indication.

Root cause

Lazy resampling always computes in floating point:

  • torch.nn.functional.grid_sample only supports floating point, and
  • even the "auto" array-slicing fast path in monai/transforms/lazy/utils.py::resample casts to float32 (img.to(torch.float32)) for collate robustness.

As discussed in #6713, preserving the dtype in the fast path was intentionally ruled out by maintainers (float32 is used "even if some specific cases are achievable by simple array slicing ... to ensure the preprocessing collate can work robustly"). The agreed resolution in the thread was to warn the user when this conversion happens (@wyli: "marking this as a feature request" in response to the request for a check/warning).

Change

resample() now emits a UserWarning when the input tensor has a non-floating-point, non-complex dtype (i.e. it will be upcast to float32), pointing users to lazy=False if the dtype must be preserved.

Before → after

import torch
from monai.transforms import DivisiblePadd, Compose
inp = {"x": torch.arange(0, 27, dtype=torch.uint8).reshape(3, 3, 3)}
Compose([DivisiblePadd(k=5, keys=["x"])], lazy=True)(inp)
# before: dtype silently becomes float32, no warning
# after:  dtype still float32 (by design), but a UserWarning is now emitted

Tests

Added to tests/transforms/functional/test_resample.py:

  • test_resample_warns_on_non_float_dtype — warning is emitted for uint8 input (fails before this change, passes after).
  • test_resample_no_warning_for_float_dtype — no warning for float32 input.

Verified locally: targeted + tests/transforms/functional and the pad transform suites pass; ruff/black/isort clean; mypy clean on the changed module.


🤖 Generated with Claude Code

…-MONAI#6713)

Lazy resampling always computes in floating point (grid_sample only
supports floats, and float32 is used even in the array-slicing fast
path for collate robustness). As a result, integer inputs such as
label maps are silently converted to float32 in a lazy Compose,
whereas the same pipeline with lazy=False preserves the dtype.

This surprised users (issue Project-MONAI#6713) and can corrupt integer label data
downstream with no indication. Preserving the dtype in the fast path
was ruled out by maintainers for collate robustness, so this adds the
warning that was agreed as the fix in the issue thread: resample() now
emits a UserWarning when the input is a non-floating, non-complex dtype
and will be upcast to float32.

Adds regression tests in tests/transforms/functional/test_resample.py:
the warning fires for uint8 input and is absent for float32 input.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Vishnu Kannaujia <vishnu.kannaujia@gmail.com>
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 533f3557-9c06-45b4-9fef-025abd9ea995

📥 Commits

Reviewing files that changed from the base of the PR and between 7db60a6 and 7f081fe.

📒 Files selected for processing (2)
  • monai/transforms/lazy/utils.py
  • tests/transforms/functional/test_resample.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • monai/transforms/lazy/utils.py
  • tests/transforms/functional/test_resample.py

📝 Walkthrough

Walkthrough

resample now warns when lazy resampling receives non-floating-point, non-complex input. The warning states that computation converts the input to float32 and does not preserve the original dtype. Tests verify this behavior for uint8 input and verify that float32 input produces no warning.

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

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states that lazy resampling now warns when non-float input is upcast to float32.
Description check ✅ Passed The description explains the issue, root cause, implementation, tests, and validation, although some template checkboxes remain unchecked.
Linked Issues check ✅ Passed The changes address issue #6713 by warning about lazy resampling dtype conversion and directing users to lazy=False when preservation is required.
Out of Scope Changes check ✅ Passed The changes are limited to the resample warning and focused tests related to issue #6713.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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: 3

🧹 Nitpick comments (2)
tests/transforms/functional/test_resample.py (1)

51-54: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Assert that the input dtype remains unchanged.

The PR objective promises that only the resampling result changes dtype. This test checks out.dtype but not img.dtype.

         self.assertIs(out.dtype, torch.float32)
+        self.assertEqual(img.dtype, torch.uint8)

As per path instructions, modified definitions must have unit-test coverage for observable behavior.

🤖 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 `@tests/transforms/functional/test_resample.py` around lines 51 - 54, Extend
the test around resample to assert that the original img remains torch.uint8
after calling resample, while retaining the existing assertion that out is
torch.float32. Ensure this verifies the input tensor is not mutated by the
lazy_resample_mode behavior.

Source: Path instructions

monai/transforms/lazy/utils.py (1)

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

Document the new warning in resample.

Add Warnings:, Returns:, and Raises: sections to the resample docstring. Document the dtype conversion, returned tensor, and NotImplementedError path.

As per path instructions, Python definitions must use accurate Google-style docstrings.

🤖 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 `@monai/transforms/lazy/utils.py` around lines 195 - 200, Update the resample
function docstring with accurate Google-style Warnings, Returns, and Raises
sections: describe floating-point conversion and loss of the original dtype,
document the returned tensor, and specify the conditions that raise
NotImplementedError.

Source: Path instructions

🤖 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 `@monai/transforms/lazy/utils.py`:
- Around line 195-200: Update the warnings.warn call in the lazy resampling
dtype-check block to explicitly use UserWarning and set stacklevel=2, preserving
the existing warning message and condition.

In `@tests/transforms/functional/test_resample.py`:
- Around line 56-58: Update the docstring of
test_resample_no_warning_for_float_dtype to state specifically that float32
inputs do not trigger the lazy resampling dtype warning, without generalizing to
all floating-point dtypes.
- Around line 49-54: Update test_resample_warns_on_non_float_dtype to assert the
specific UserWarning category and match the emitted dtype-conversion message
with assertWarnsRegex(UserWarning, ...), while preserving the existing resample
call and float32 dtype assertion.

---

Nitpick comments:
In `@monai/transforms/lazy/utils.py`:
- Around line 195-200: Update the resample function docstring with accurate
Google-style Warnings, Returns, and Raises sections: describe floating-point
conversion and loss of the original dtype, document the returned tensor, and
specify the conditions that raise NotImplementedError.

In `@tests/transforms/functional/test_resample.py`:
- Around line 51-54: Extend the test around resample to assert that the original
img remains torch.uint8 after calling resample, while retaining the existing
assertion that out is torch.float32. Ensure this verifies the input tensor is
not mutated by the lazy_resample_mode 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 86278d04-b86e-4a1e-9f2d-5e95612b2367

📥 Commits

Reviewing files that changed from the base of the PR and between 8690ae7 and 7db60a6.

📒 Files selected for processing (2)
  • monai/transforms/lazy/utils.py
  • tests/transforms/functional/test_resample.py

Comment on lines +195 to +200
if not (torch.is_floating_point(img) or torch.is_complex(img)):
warnings.warn(
f"Lazy resampling computes in floating point and converts the input of dtype {img.dtype} to "
"float32; the original data type is not preserved. For integer data such as label maps, set "
"`lazy=False` for the affected transforms (or cast back afterwards) if the data type must be preserved."
)

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
git ls-files | grep -F 'monai/transforms/lazy/utils.py' || true

echo "== relevant lines =="
sed -n '160,215p' monai/transforms/lazy/utils.py | cat -n

echo "== surrounding function/docs =="
sed -n '1,130p' monai/transforms/lazy/utils.py | cat -n

echo "== imports in file =="
sed -n '1,40p' monai/transforms/lazy/utils.py | cat -n

echo "== warnings usages in file =="
rg -n "warnings\.warn|stacklevel|category=UserWarning|UserWarning" monai/transforms/lazy/utils.py

echo "== ruff availability/config references =="
(git ls-files | grep -E '(^|/)(pyproject\.toml|ruff\.toml|\.ruff\.toml)$' && rg -n '(^tool\.ruff|B028|PERF|warn|stacklevel)' pyproject.toml ruff.toml .ruff.toml 2>/dev/null || true) || true

Repository: Project-MONAI/MONAI

Length of output: 11556


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== resample def and full relevant section =="
rg -n -A 70 -B 20 "^def resample" monai/transforms/lazy/utils.py

echo "== pyproject ruff config =="
sed -n '45,80p' pyproject.toml | cat -n

echo "== Python warnings default stacklevel behavior probe =="
python3 - <<'PY'
import warnings

def f():
    warnings.warn("no stacklevel")
    warnings.warn("with stacklevel=2", stacklevel=2)

try:
    import ruff
except Exception as e:
    print("ruff import unavailable:", type(e).__name__, e)

f()
PY

Repository: Project-MONAI/MONAI

Length of output: 6928


Set an explicit caller-facing warning category and stack level.

warnings.warn defaults to UserWarning, but the dtype-conversion warning should pass it explicitly. Add stacklevel=2 so the message points to the caller instead of this helper.

Proposed fix
         warnings.warn(
             f"Lazy resampling computes in floating point and converts the input of dtype {img.dtype} to "
-            "float32; the original data type is not preserved. For integer data such as label maps, set "
-            "`lazy=False` for the affected transforms (or cast back afterwards) if the data type must be preserved."
+            "float32; the original data type is not preserved. For integer data such as label maps, set "
+            "`lazy=False` for the affected transforms (or cast back afterwards) if the data type must be preserved.",
+            category=UserWarning,
+            stacklevel=2,
         )
📝 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
if not (torch.is_floating_point(img) or torch.is_complex(img)):
warnings.warn(
f"Lazy resampling computes in floating point and converts the input of dtype {img.dtype} to "
"float32; the original data type is not preserved. For integer data such as label maps, set "
"`lazy=False` for the affected transforms (or cast back afterwards) if the data type must be preserved."
)
if not (torch.is_floating_point(img) or torch.is_complex(img)):
warnings.warn(
f"Lazy resampling computes in floating point and converts the input of dtype {img.dtype} to "
"float32; the original data type is not preserved. For integer data such as label maps, set "
"`lazy=False` for the affected transforms (or cast back afterwards) if the data type must be preserved.",
category=UserWarning,
stacklevel=2,
)
🧰 Tools
🪛 Ruff (0.16.0)

[warning] 196-196: No explicit stacklevel keyword argument found

Set stacklevel=2

(B028)

🤖 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 `@monai/transforms/lazy/utils.py` around lines 195 - 200, Update the
warnings.warn call in the lazy resampling dtype-check block to explicitly use
UserWarning and set stacklevel=2, preserving the existing warning message and
condition.

Source: Linters/SAST tools

Comment on lines +49 to +54
def test_resample_warns_on_non_float_dtype(self):
"""Lazy resampling upcasts non-floating-point inputs to float32; the user should be warned (see issue #6713)."""
img = convert_to_tensor(get_arange_img((3, 3)), dtype=torch.uint8)
with self.assertWarns(Warning):
out = resample(img, torch.eye(3), {"lazy_resample_mode": "auto"})
self.assertIs(out.dtype, torch.float32)

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the test file and inspect relevant test/source behavior.
if [ -f tests/transforms/functional/test_resample.py ]; then
  echo "== test file snippet =="
  cat -n tests/transforms/functional/test_resample.py | sed -n '1,100p'
else
  echo "tests/transforms/functional/test_resample.py not found"
  fd -i 'test_resample\.py'
fi

echo "== resample definitions/usages matching warning contract =="
rg -n "resample|lazy_resample_mode|converts the input|UserWarning|Warning" tests/transforms/functional/test_resample.py -S || true

echo "== likely implementation files =="
rg -n "def resample|class .*Resample|lazy_resample_mode|converts the input|UserWarning" -S --glob '*.py' | head -n 120

Repository: Project-MONAI/MONAI

Length of output: 4348


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
fd -i 'functional\.py$' monai/transforms/lazy || true
fd -i 'lazy.*\.py$' monai/transforms || true
rg -n "def resample\b" --glob '*.py' | head -n 80
echo "== warning strings =="
rg -n "converts the input|dtype torch\.(uint8|float)|dtype .*float32|lazy_dtype|lazy_resample_mode" --glob '*.py' | head -n 200

echo "== inspect candidate implementation =="
while IFS= read -r f; do
  echo "--- $f ---"
  ast-grep outline "$f" --match 'resample' --view expanded || true
  cat -n "$f" | sed -n '1,220p'
done < <(fd -i 'functional\.py$' monai/transforms/lazy)

Repository: Project-MONAI/MONAI

Length of output: 215


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== resample definitions/usages =="
rg -n "def resample\b|class .*Resample|resample\b" --glob '*.py' | head -n 200 || true
echo "== warning strings and lazy options =="
rg -n "converts the input|lazy_dtype|lazy_resample_mode|warn\(|warnings\.warn|UserWarning|Warning" --glob '*.py' | head -n 250 || true

echo "== inspect monai/transforms/lazy/functional.py =="
wc -l monai/transforms/lazy/functional.py
cat -n monai/transforms/lazy/functional.py | sed -n '1,260p'

echo "== Python unittest assertWarns semantics probe =="
python3 - <<'PY'
import warnings, unittest

class Bad(unittest.TestCase):
    def pass_warning(self):
        with warnings.catch_warnings():
            warnings.simplefilter("error")
            with self.assertWarns(Warning):
                warnings.warn("hello", DeprecationWarning)
            with self.assertWarnsRegex(Warning, "hello"):
                warnings.warn("hello", DeprecationWarning)
try:
    Bad().pass_warning()
    print("both accept unrelated warning subtype under current semantics")
except Exception as e:
    print(type(e).__name__, e)
PY

Repository: Project-MONAI/MONAI

Length of output: 191


🏁 Script executed:

#!/bin/bash
set -u

echo "== locate functional.py =="
fd -a 'functional.py$' . | sed 's#^\./##' | grep 'monai/transforms/lazy/functional.py' || true

echo "== resample references =="
rg -n "def resample\b|lazy_dtype|lazy_resample_mode|warnings\.warn|UserWarning|Warning" -S --glob '*.py' . | head -n 300

echo "== inspect monai/transforms/lazy/functional.py =="
if [ -f monai/transforms/lazy/functional.py ]; then
  wc -l monai/transforms/lazy/functional.py
  cat -n monai/transforms/lazy/functional.py | sed -n '1,260p'
else
  echo "monai/transforms/lazy/functional.py missing"
fi

Repository: Project-MONAI/MONAI

Length of output: 47659


🏁 Script executed:

#!/bin/bash
set -u

python3 - <<'PY'
import ast
path = "monai/transforms/lazy/utils.py"
with open(path) as f:
    tree = ast.parse(f.read())

for node in ast.walk(tree):
    if isinstance(node, ast.FunctionDef) and node.name == "resample":
        file = node.lineno; col = node.col_offset
        print(f"resample starts line {file} col {col}")
        print(ast.get_source_segment(open(path).read(), node))
        break
else:
    print("resample function not found")

with open(path) as f:
    for i, line in enumerate(f, start=1):
        if 140 <= i <= 220:
            print(f"{i}: {line.rstrip()}")
PY

Repository: Project-MONAI/MONAI

Length of output: 9441


Assert the warning category and message.

self.assertWarns(Warning) only checks for any Warning subclass, while resample() emits a UserWarning with the dtype conversion message. Use assertWarnsRegex(UserWarning, ...) with a regex that covers the emitted text.

Proposed test change
-        with self.assertWarns(Warning):
+        with self.assertWarnsRegex(
+            UserWarning, r"converts the input of dtype torch\.uint8 to float32"
+        ):
             out = resample(img, torch.eye(3), {"lazy_resample_mode": "auto"})
📝 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_resample_warns_on_non_float_dtype(self):
"""Lazy resampling upcasts non-floating-point inputs to float32; the user should be warned (see issue #6713)."""
img = convert_to_tensor(get_arange_img((3, 3)), dtype=torch.uint8)
with self.assertWarns(Warning):
out = resample(img, torch.eye(3), {"lazy_resample_mode": "auto"})
self.assertIs(out.dtype, torch.float32)
def test_resample_warns_on_non_float_dtype(self):
"""Lazy resampling upcasts non-floating-point inputs to float32; the user should be warned (see issue `#6713`)."""
img = convert_to_tensor(get_arange_img((3, 3)), dtype=torch.uint8)
with self.assertWarnsRegex(
UserWarning, r"converts the input of dtype torch\.uint8 to float32"
):
out = resample(img, torch.eye(3), {"lazy_resample_mode": "auto"})
self.assertIs(out.dtype, torch.float32)
🤖 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 `@tests/transforms/functional/test_resample.py` around lines 49 - 54, Update
test_resample_warns_on_non_float_dtype to assert the specific UserWarning
category and match the emitted dtype-conversion message with
assertWarnsRegex(UserWarning, ...), while preserving the existing resample call
and float32 dtype assertion.

Source: Path instructions

Comment thread tests/transforms/functional/test_resample.py
…ocstring

- pass stacklevel=2 so the warning points to the caller
- assert the input tensor dtype is not mutated by resample()
- narrow the no-warning test docstring to float32 (float16/bfloat16 are
  still upcast to float32 without a warning)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Vishnu Kannaujia <vishnu.kannaujia@gmail.com>
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.

Lazy DivisiblePadd converts uint8 to float32

1 participant