Warn when lazy resampling upcasts non-float input to float32 (#6713) - #9044
Warn when lazy resampling upcasts non-float input to float32 (#6713)#9044vishnukannaujia wants to merge 2 commits into
Conversation
…-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>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthrough
Estimated code review effort: 2 (Simple) | ~10 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
tests/transforms/functional/test_resample.py (1)
51-54: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winAssert that the input dtype remains unchanged.
The PR objective promises that only the resampling result changes dtype. This test checks
out.dtypebut notimg.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 winDocument the new warning in
resample.Add
Warnings:,Returns:, andRaises:sections to theresampledocstring. Document the dtype conversion, returned tensor, andNotImplementedErrorpath.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
📒 Files selected for processing (2)
monai/transforms/lazy/utils.pytests/transforms/functional/test_resample.py
| 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." | ||
| ) |
There was a problem hiding this comment.
📐 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) || trueRepository: 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()
PYRepository: 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.
| 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
| 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) |
There was a problem hiding this comment.
🎯 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 120Repository: 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)
PYRepository: 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"
fiRepository: 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()}")
PYRepository: 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.
| 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
…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>
Fixes #6713
Summary
When integer data (e.g. a
uint8label map) is passed through a lazyCompose, the lazy resampling path silently converts it tofloat32. The same pipeline withlazy=Falsepreserves 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_sampleonly supports floating point, andmonai/transforms/lazy/utils.py::resamplecasts tofloat32(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 aUserWarningwhen the input tensor has a non-floating-point, non-complex dtype (i.e. it will be upcast tofloat32), pointing users tolazy=Falseif the dtype must be preserved.Before → after
Tests
Added to
tests/transforms/functional/test_resample.py:test_resample_warns_on_non_float_dtype— warning is emitted foruint8input (fails before this change, passes after).test_resample_no_warning_for_float_dtype— no warning forfloat32input.Verified locally: targeted +
tests/transforms/functionaland the pad transform suites pass;ruff/black/isortclean;mypyclean on the changed module.🤖 Generated with Claude Code