Skip to content

Commit 0a8d945

Browse files
garciadiasclaudepre-commit-ci[bot]ericspod
authored
8587 test erros on pytorch release 2508 on series 50 (#8770)
Fixes #8587. ### Description On NVIDIA Blackwell GPUs (compute capability 12.x, sm_120), running MONAI with `USE_COMPILED=True` produces **silently incorrect spatial transform results**. The root cause is that the MONAI compiled C extension (`monai._C`) is built at install time against a fixed set of CUDA architectures (`TORCH_CUDA_ARCH_LIST`). Blackwell (sm_120) is not included in the default build list (which tops out at sm_90, Hopper), so `grid_pull` executes against a mismatched PTX or JIT path and silently returns wrong values — there is no runtime error. This affects two independent code paths: **1. `spatial_resample` / `Resample`** (`monai/transforms/spatial/functional.py`) When `USE_COMPILED=True`, `spatial_resample` unconditionally calls `grid_pull`. On Blackwell GPUs this produces incorrect resampling output without raising any exception. **2. `Warp`** (`monai/networks/blocks/warp.py`) `Warp.__init__` stores interpolation and padding modes as **integers** when `USE_COMPILED=True` (as required by `grid_pull`). The PyTorch-native fallback path (`F.grid_sample`) requires **string** modes. Without a Blackwell-aware fallback, there was no path to trigger this mismatch — but once the device check forces the fallback, the integer modes cause a type error at runtime. #### Additional change to `runner.py` (⚠️not directly related to the issue) Add per-test timeout via --timeout flag Tests that hang indefinitely (e.g. GPU ops stuck on Blackwell) block the entire suite. Add a --timeout SECONDS option to tests/runner.py that uses SIGALRM to interrupt any individual test that exceeds the limit; the test is recorded as an error and the runner continues with the next test. - Default is 0 (disabled); SIGALRM support is checked at runtime so the flag is silently ignored on Windows. - runtests.sh gains a matching --timeout [secs] flag (default 180s when the flag is given without a value) that is forwarded to runner.py for unit tests. ```bash #Usage: ./runtests.sh -u --timeout # 3-minute per-test limit ./runtests.sh -u --timeout 60 # 1-minute per-test limit python tests/runner.py --timeout 180 ``` **Fix** A private helper `_compiled_unsupported(device: torch.device) -> bool` is added to `monai/transforms/spatial/functional.py`. It returns `True` for CUDA devices with compute capability major ≥ 12, and `False` for all other devices (CPU, older GPUs). - In `spatial_resample`, the compiled path is now gated on `USE_COMPILED and not _compiled_unsupported(img.device)`, falling back to the PyTorch-native `affine_grid + grid_sample` path on unsupported devices. - In `Warp`, the same gate is applied in `forward()`. Additionally, `__init__` now always stores **both** the compiled integer modes (for `grid_pull`) and the native string modes (for `F.grid_sample`), ensuring the fallback path has correctly-typed arguments regardless of how `USE_COMPILED` was set at initialisation time. Behaviour on all previously supported GPU architectures (sm_75 through sm_90) is unchanged. ### Types of changes - [x] Non-breaking change (fix or new feature that would not break existing functionality). - [ ] Breaking change (fix or new feature that would cause existing functionality to change). - [x] New tests added to cover the changes. - [x] Integration tests passed locally by running `./runtests.sh -f -u --net --coverage`. - [x] Quick tests passed locally by running `./runtests.sh --quick --unittests --disttests`. - [x] In-line docstrings updated. - [ ] Documentation updated, tested `make html` command in the `docs/` folder. --------- Signed-off-by: R. Garcia-Dias <rafaelagd@gmail.com> Signed-off-by: Claude <noreply@anthropic.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Eric Kerfoot <17726042+ericspod@users.noreply.github.com>
1 parent 6187529 commit 0a8d945

17 files changed

Lines changed: 310 additions & 33 deletions

File tree

Dockerfile.slim

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ ARG IMAGE=debian:12-slim
1818

1919
FROM ${IMAGE} AS build
2020

21-
ARG TORCH_CUDA_ARCH_LIST="7.5 8.0 8.6 8.9 9.0+PTX"
21+
ARG TORCH_CUDA_ARCH_LIST="7.5 8.0 8.6 8.9 9.0+PTX 12.0"
2222

2323
ENV DEBIAN_FRONTEND=noninteractive
2424
ENV APT_INSTALL="apt install -y --no-install-recommends"
@@ -28,7 +28,7 @@ RUN apt update && apt upgrade -y && \
2828
wget https://developer.download.nvidia.com/compute/cuda/repos/debian12/x86_64/cuda-keyring_1.1-1_all.deb && \
2929
dpkg -i cuda-keyring_1.1-1_all.deb && \
3030
apt update && \
31-
${APT_INSTALL} cuda-toolkit-12 && \
31+
${APT_INSTALL} cuda-toolkit-12-9 && \
3232
rm -rf /usr/lib/python*/EXTERNALLY-MANAGED /var/lib/apt/lists/* && \
3333
python -m pip install --upgrade --no-cache-dir --no-build-isolation pip
3434

@@ -53,7 +53,8 @@ COPY tests ./tests
5353
COPY monai ./monai
5454

5555
# install full deps
56-
RUN python -m pip install --no-cache-dir --no-build-isolation -r requirements-dev.txt
56+
RUN python -m pip install --no-cache-dir --no-build-isolation -U wheel wheel-stub
57+
RUN python -m pip install --no-cache-dir --no-build-isolation "torch>=2.8.0,<2.11" -r requirements-dev.txt
5758
5859
# compile ext
5960
RUN CUDA_HOME=/usr/local/cuda FORCE_CUDA=1 USE_COMPILED=1 BUILD_MONAI=1 python setup.py develop

monai/apps/auto3dseg/bundle_gen.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,8 +264,7 @@ def _run_cmd(self, cmd: str, devices_info: str = "") -> subprocess.CompletedProc
264264
look_up_option(self.device_setting["MN_START_METHOD"], ["bcprun"])
265265
except ValueError as err:
266266
raise NotImplementedError(
267-
f"{self.device_setting['MN_START_METHOD']} is not supported yet."
268-
"Try modify BundleAlgo._run_cmd for your cluster."
267+
f"{self.device_setting['MN_START_METHOD']} is not supported yet. Try modify BundleAlgo._run_cmd for your cluster."
269268
) from err
270269

271270
return _run_cmd_bcprun(cmd, n=self.device_setting["NUM_NODES"], p=self.device_setting["n_devices"])

monai/apps/detection/networks/retinanet_detector.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -342,8 +342,7 @@ def set_regular_matcher(
342342
"""
343343
if fg_iou_thresh < bg_iou_thresh:
344344
raise ValueError(
345-
"Require fg_iou_thresh >= bg_iou_thresh. "
346-
f"Got fg_iou_thresh={fg_iou_thresh}, bg_iou_thresh={bg_iou_thresh}."
345+
f"Required condition fg_iou_thresh >= bg_iou_thresh not met ({fg_iou_thresh=}, {bg_iou_thresh=})."
347346
)
348347
self.proposal_matcher = Matcher(
349348
fg_iou_thresh, bg_iou_thresh, allow_low_quality_matches=allow_low_quality_matches
@@ -519,7 +518,7 @@ def forward(
519518
else:
520519
if self.inferer is None:
521520
raise ValueError(
522-
"`self.inferer` is not defined." "Please refer to function self.set_sliding_window_inferer(*)."
521+
"`self.inferer` is not defined. Please refer to function self.set_sliding_window_inferer(*)."
523522
)
524523
head_outputs = predict_with_inferer(
525524
images, self.network, keys=[self.cls_key, self.box_reg_key], inferer=self.inferer

monai/auto3dseg/analyzer.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -965,8 +965,7 @@ def __call__(self, data: dict) -> dict:
965965
self.hist_range = nr_channels * self.hist_range
966966
if len(self.hist_range) != nr_channels:
967967
raise ValueError(
968-
f"There is a mismatch between the number of channels ({nr_channels}) "
969-
f"and histogram ranges ({len(self.hist_range)})."
968+
f"There is a mismatch between the number of channels ({nr_channels}) and histogram ranges ({len(self.hist_range)})."
970969
)
971970

972971
# perform calculation

monai/csrc/ext.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,15 @@ PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
6363
.value("seventh", monai::InterpolationType::SeventhOrder)
6464
.export_values();
6565

66+
// build-time compile capability info
67+
m.def("max_compute_capability", []() {
68+
#ifdef MONAI_MAX_COMPUTE_CAPABILITY
69+
return MONAI_MAX_COMPUTE_CAPABILITY;
70+
#else
71+
return 0;
72+
#endif
73+
}, "Maximum compute capability (major*100+minor) the extension was compiled for, or 0 if unknown");
74+
6675
// resample
6776
m.def("grid_pull", &monai::grid_pull, "GridPull");
6877
m.def("grid_pull_backward", &monai::grid_pull_backward, "GridPull backward");

monai/data/dataset.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -609,7 +609,7 @@ def __init__(
609609
# the cache is created without multi-threading
610610
self._read_env: Any | None = None
611611
# this runs on the primary thread/process
612-
self._fill_cache_start_reader(show_progress=self.progress)
612+
self._read_env = self._fill_cache_start_reader(show_progress=self.progress)
613613
print(f"Accessing lmdb file: {self.db_file.absolute()}.")
614614

615615
def set_data(self, data: Sequence):
@@ -637,6 +637,12 @@ def _fill_cache_start_reader(self, show_progress=True):
637637
Args:
638638
show_progress: whether to show the progress bar if possible.
639639
"""
640+
# Close any open read environment before attempting write-mode access
641+
# to prevent "environment already open" errors when multiple LMDBDataset
642+
# instances target the same db file
643+
if self._read_env is not None:
644+
self._read_env.close()
645+
self._read_env = None
640646
# create cache
641647
self.lmdb_kwargs["readonly"] = False
642648
env = lmdb.open(path=f"{self.db_file}", subdir=False, **self.lmdb_kwargs)
@@ -664,7 +670,7 @@ def _fill_cache_start_reader(self, show_progress=True):
664670
size = env.info()["map_size"]
665671
new_size = size * 2
666672
warnings.warn(
667-
f"Resizing the cache database from {int(size) >> 20}MB" f" to {int(new_size) >> 20}MB."
673+
f"Resizing the cache database from {int(size) >> 20}MB to {int(new_size) >> 20}MB."
668674
)
669675
env.set_mapsize(new_size)
670676
except lmdb.MapResizedError:

monai/data/wsi_reader.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -416,8 +416,7 @@ def get_data(
416416
# Check if there are three color channels for RGB
417417
elif mode in "RGB" and patch.shape[self.channel_dim] != 3:
418418
raise ValueError(
419-
f"The image is expected to have three color channels in '{mode}' mode but has "
420-
f"{patch.shape[self.channel_dim]}. "
419+
f"The image is expected to have three color channels in '{mode}' mode but has {patch.shape[self.channel_dim]}. "
421420
)
422421
# Get patch-related metadata
423422
metadata: dict = self._get_metadata(wsi=each_wsi, patch=patch, location=location, size=size, level=level)

monai/networks/blocks/warp.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from monai.config.deviceconfig import USE_COMPILED
2121
from monai.networks.layers.spatial_transforms import grid_pull
2222
from monai.networks.utils import meshgrid_ij
23+
from monai.transforms.spatial.functional import _compiled_unsupported
2324
from monai.utils import GridSampleMode, GridSamplePadMode, optional_import
2425

2526
_C, _ = optional_import("monai._C")
@@ -63,6 +64,18 @@ def __init__(self, mode=GridSampleMode.BILINEAR.value, padding_mode=GridSamplePa
6364
super().__init__()
6465
# resolves _interp_mode for different methods
6566

67+
# Native string modes are always stored for the PyTorch fallback path.
68+
# When USE_COMPILED=True but the device is unsupported at runtime (e.g. Blackwell GPU),
69+
# forward() falls back to F.grid_sample which requires string, not integer, modes.
70+
self._interp_mode_native = (
71+
GridSampleMode(mode).value if mode in (m.value for m in GridSampleMode) else GridSampleMode.BILINEAR.value
72+
)
73+
self._padding_mode_native = (
74+
GridSamplePadMode(padding_mode).value
75+
if padding_mode in (p.value for p in GridSamplePadMode)
76+
else GridSamplePadMode.BORDER.value
77+
)
78+
6679
if USE_COMPILED:
6780
if mode in (inter.value for inter in GridSampleMode):
6881
mode = GridSampleMode(mode)
@@ -77,7 +90,7 @@ def __init__(self, mode=GridSampleMode.BILINEAR.value, padding_mode=GridSamplePa
7790
self._interp_mode = mode
7891
else:
7992
warnings.warn("monai.networks.blocks.Warp: Using PyTorch native grid_sample.")
80-
self._interp_mode = GridSampleMode(mode).value
93+
self._interp_mode = self._interp_mode_native
8194

8295
# resolves _padding_mode for different methods
8396
if USE_COMPILED:
@@ -93,7 +106,7 @@ def __init__(self, mode=GridSampleMode.BILINEAR.value, padding_mode=GridSamplePa
93106
padding_mode = 0 # default to nearest
94107
self._padding_mode = padding_mode
95108
else:
96-
self._padding_mode = GridSamplePadMode(padding_mode).value
109+
self._padding_mode = self._padding_mode_native
97110

98111
self.ref_grid = None
99112
self.jitter = jitter
@@ -138,13 +151,15 @@ def forward(self, image: torch.Tensor, ddf: torch.Tensor):
138151
grid = self.get_reference_grid(ddf, jitter=self.jitter) + ddf
139152
grid = grid.permute([0] + list(range(2, 2 + spatial_dims)) + [1]) # (batch, ..., spatial_dims)
140153

141-
if not USE_COMPILED: # pytorch native grid_sample
154+
_use_compiled = USE_COMPILED and not _compiled_unsupported(image.device)
155+
156+
if not _use_compiled: # pytorch native grid_sample
142157
for i, dim in enumerate(grid.shape[1:-1]):
143158
grid[..., i] = grid[..., i] * 2 / (dim - 1) - 1
144159
index_ordering: list[int] = list(range(spatial_dims - 1, -1, -1))
145160
grid = grid[..., index_ordering] # z, y, x -> x, y, z
146161
return F.grid_sample(
147-
image, grid, mode=self._interp_mode, padding_mode=f"{self._padding_mode}", align_corners=True
162+
image, grid, mode=self._interp_mode_native, padding_mode=self._padding_mode_native, align_corners=True
148163
)
149164

150165
# using csrc resampling

monai/transforms/regularization/array.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424

2525

2626
class Mixer(RandomizableTransform):
27-
2827
def __init__(self, batch_size: int, alpha: float = 1.0) -> None:
2928
"""
3029
Mixer is a base class providing the basic logic for the mixup-class of

monai/transforms/spatial/array.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from monai.transforms.croppad.array import CenterSpatialCrop, ResizeWithPadOrCrop
3535
from monai.transforms.inverse import InvertibleTransform
3636
from monai.transforms.spatial.functional import (
37+
_compiled_unsupported,
3738
affine_func,
3839
convert_box_to_points,
3940
convert_points_to_box,
@@ -2105,14 +2106,15 @@ def __call__(
21052106
_align_corners = self.align_corners if align_corners is None else align_corners
21062107
img_t, *_ = convert_data_type(img, torch.Tensor, dtype=_dtype, device=_device)
21072108
sr = min(len(img_t.peek_pending_shape() if isinstance(img_t, MetaTensor) else img_t.shape[1:]), 3)
2109+
_use_compiled = USE_COMPILED and not _compiled_unsupported(img_t.device)
21082110
backend, _interp_mode, _padding_mode, _ = resolves_modes(
21092111
self.mode if mode is None else mode,
21102112
self.padding_mode if padding_mode is None else padding_mode,
21112113
backend=None,
2112-
use_compiled=USE_COMPILED,
2114+
use_compiled=_use_compiled,
21132115
)
21142116

2115-
if USE_COMPILED or backend == TransformBackends.NUMPY:
2117+
if _use_compiled or backend == TransformBackends.NUMPY:
21162118
grid_t, *_ = convert_to_dst_type(grid[:sr], img_t, dtype=grid.dtype, wrap_sequence=True)
21172119
if isinstance(grid, torch.Tensor) and grid_t.data_ptr() == grid.data_ptr():
21182120
grid_t = grid_t.clone(memory_format=torch.contiguous_format)
@@ -2123,7 +2125,7 @@ def __call__(
21232125
grid_t[i] = ((_dim - 1) / _dim) * grid_t[i] + t if _align_corners else grid_t[i] + t
21242126
elif _align_corners:
21252127
grid_t[i] = ((_dim - 1) / _dim) * (grid_t[i] + 0.5)
2126-
if USE_COMPILED and backend == TransformBackends.TORCH: # compiled is using torch backend param name
2128+
if _use_compiled and backend == TransformBackends.TORCH: # compiled is using torch backend param name
21272129
grid_t = moveaxis(grid_t, 0, -1) # type: ignore
21282130
out = grid_pull(
21292131
img_t.unsqueeze(0),
@@ -2141,6 +2143,20 @@ def __call__(
21412143
[_map_coord(c, grid_np, order=_interp_mode, mode=_padding_mode) for c in img_np]
21422144
)
21432145
out = convert_to_dst_type(out, img_t)[0]
2146+
else:
2147+
# Fallback to PyTorch grid_sample when compiled extension is unsupported.
2148+
# Convert grid coordinates from compiled convention [0, size-1] to PyTorch [-1, 1]
2149+
for i, dim in enumerate(img_t.shape[1 : 1 + sr]):
2150+
_dim = max(2, dim)
2151+
grid_t[i] = (grid_t[i] * 2.0 / _dim) - 1.0
2152+
grid_t = moveaxis(grid_t, 0, -1) # type: ignore
2153+
out = torch.nn.functional.grid_sample(
2154+
img_t.unsqueeze(0),
2155+
grid_t.unsqueeze(0),
2156+
mode=_interp_mode,
2157+
padding_mode=_padding_mode,
2158+
align_corners=None if _align_corners == TraceKeys.NONE else _align_corners, # type: ignore
2159+
)[0]
21442160
else:
21452161
grid_t = moveaxis(grid[list(range(sr - 1, -1, -1))], 0, -1) # type: ignore
21462162
grid_t = convert_to_dst_type(grid_t, img_t, wrap_sequence=True)[0].unsqueeze(0)

0 commit comments

Comments
 (0)