Skip to content

Commit 4df5095

Browse files
author
Donglai Wei
committed
Plan CPU-only sliding window
1 parent d164d56 commit 4df5095

5 files changed

Lines changed: 94 additions & 3 deletions

File tree

connectomics/config/hydra_config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,9 @@ class SlidingWindowConfig:
943943
padding_mode: str = "constant" # Padding mode at volume boundaries
944944
pad_size: Optional[List[int]] = None # Padding size for context (e.g., [16, 32, 32])
945945
save_channels: Optional[List[int]] = None # Channel indices to save (None = all channels)
946+
keep_input_on_cpu: bool = False # Keep full input volume on CPU; MONAI moves windows to sw_device
947+
sw_device: Optional[str] = None # MONAI window compute device (e.g., "cuda")
948+
output_device: Optional[str] = None # MONAI stitched output device (e.g., "cpu")
946949

947950

948951
@dataclass

connectomics/inference/sliding.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from typing import Optional, Tuple, Union
1111
import warnings
1212

13+
import torch
1314
from monai.inferers import SlidingWindowInferer
1415

1516

@@ -97,6 +98,28 @@ def build_sliding_inferer(cfg):
9798
mode = getattr(sliding_cfg, "blending", "gaussian") if sliding_cfg else "gaussian"
9899
sigma_scale = float(getattr(sliding_cfg, "sigma_scale", 0.125)) if sliding_cfg else 0.125
99100
padding_mode = getattr(sliding_cfg, "padding_mode", "constant") if sliding_cfg else "constant"
101+
keep_input_on_cpu = bool(getattr(sliding_cfg, "keep_input_on_cpu", False)) if sliding_cfg else False
102+
sw_device = getattr(sliding_cfg, "sw_device", None) if sliding_cfg else None
103+
output_device = getattr(sliding_cfg, "output_device", None) if sliding_cfg else None
104+
105+
if isinstance(sw_device, str) and sw_device.lower() in {"", "none", "null"}:
106+
sw_device = None
107+
if isinstance(output_device, str) and output_device.lower() in {"", "none", "null"}:
108+
output_device = None
109+
110+
if keep_input_on_cpu:
111+
# MONAI defaults both devices to inputs.device; when inputs stay on CPU,
112+
# explicitly route windows to CUDA and stitch on CPU unless overridden.
113+
if sw_device is None and torch.cuda.is_available():
114+
sw_device = "cuda"
115+
if output_device is None:
116+
output_device = "cpu"
117+
if sw_device is None:
118+
warnings.warn(
119+
"inference.sliding_window.keep_input_on_cpu=True but no sw_device was set "
120+
"and CUDA is unavailable. Sliding-window inference will run on CPU.",
121+
UserWarning,
122+
)
100123

101124
inferer = SlidingWindowInferer(
102125
roi_size=roi_size,
@@ -105,13 +128,16 @@ def build_sliding_inferer(cfg):
105128
mode=mode,
106129
sigma_scale=sigma_scale,
107130
padding_mode=padding_mode,
131+
sw_device=sw_device,
132+
device=output_device,
108133
progress=True,
109134
)
110135

111136
print(
112137
" Sliding-window inference configured: "
113138
f"roi_size={roi_size}, overlap={overlap}, sw_batch={sw_batch_size}, "
114-
f"mode={mode}, sigma_scale={sigma_scale}, padding={padding_mode}"
139+
f"mode={mode}, sigma_scale={sigma_scale}, padding={padding_mode}, "
140+
f"keep_input_on_cpu={keep_input_on_cpu}, sw_device={sw_device}, output_device={output_device}"
115141
)
116142

117143
return inferer

connectomics/inference/tta.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,18 @@ def _run_network(self, images: torch.Tensor) -> torch.Tensor:
168168
with torch.no_grad():
169169
if self.sliding_inferer is not None:
170170
return self.sliding_inferer(inputs=images, network=self._sliding_window_predict)
171+
keep_input_on_cpu = bool(
172+
getattr(
173+
getattr(getattr(self.cfg, "inference", None), "sliding_window", None),
174+
"keep_input_on_cpu",
175+
False,
176+
)
177+
)
178+
if keep_input_on_cpu and images.device.type == "cpu":
179+
raise RuntimeError(
180+
"inference.sliding_window.keep_input_on_cpu=True requires sliding-window "
181+
"inference to be enabled (set inference.sliding_window.window_size or model output size)."
182+
)
171183
return self._sliding_window_predict(images)
172184

173185
def _sliding_window_predict(self, inputs: torch.Tensor) -> torch.Tensor:
@@ -399,6 +411,8 @@ def predict(self, images: torch.Tensor, mask: Optional[torch.Tensor] = None) ->
399411
else False
400412
)
401413
if apply_mask and mask is not None:
414+
if mask.device != ensemble_result.device:
415+
mask = mask.to(device=ensemble_result.device, non_blocking=True)
402416
if mask.shape != ensemble_result.shape:
403417
if not (mask.shape[1] == 1 and mask.shape[0] == ensemble_result.shape[0]):
404418
warnings.warn(

connectomics/training/lit/model.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ def _save_metrics_to_file(self, metrics_dict: Dict[str, Any]):
396396
def _compute_test_metrics(self, decoded_predictions: np.ndarray, labels: torch.Tensor, volume_name: str = None):
397397
"""Update configured torchmetrics using decoded predictions and print per-volume metrics."""
398398
pred_tensor = torch.from_numpy(decoded_predictions).float().to(self.device)
399-
labels_tensor = labels.float()
399+
labels_tensor = labels.float().to(pred_tensor.device)
400400

401401
# Remove batch and channel dimensions
402402
pred_tensor = pred_tensor.squeeze()
@@ -770,13 +770,57 @@ def on_test_start(self):
770770
# Keep in training mode (e.g., for Monte Carlo Dropout uncertainty estimation)
771771
self.train()
772772

773+
sliding_cfg = getattr(getattr(self.cfg, "inference", None), "sliding_window", None)
774+
if bool(getattr(sliding_cfg, "keep_input_on_cpu", False)):
775+
print(
776+
" Sliding-window CPU input mode enabled: keeping test image tensors on CPU "
777+
"and letting MONAI move window batches to the configured sw_device."
778+
)
779+
773780
def on_test_end(self):
774781
"""Called at the end of testing."""
775782
# Note: Metrics are logged in test_step with on_epoch=True,
776783
# which automatically computes and logs them at the end of testing.
777784
# Logging here causes a Lightning warning since self.log() is not allowed in on_test_end.
778785
pass
779786

787+
def transfer_batch_to_device(self, batch: Any, device: torch.device, dataloader_idx: int) -> Any:
788+
"""Keep large test/predict input volumes on CPU for MONAI sliding-window inference."""
789+
sliding_cfg = getattr(getattr(self.cfg, "inference", None), "sliding_window", None)
790+
keep_input_on_cpu = bool(getattr(sliding_cfg, "keep_input_on_cpu", False))
791+
792+
trainer = getattr(self, "_trainer", None)
793+
is_test_or_predict = bool(
794+
getattr(trainer, "testing", False) or getattr(trainer, "predicting", False)
795+
)
796+
797+
preserve_cpu_input = keep_input_on_cpu and is_test_or_predict and isinstance(batch, dict)
798+
cpu_image = None
799+
cpu_label = None
800+
cpu_mask = None
801+
if preserve_cpu_input:
802+
image = batch.get("image")
803+
label = batch.get("label")
804+
mask = batch.get("mask")
805+
if torch.is_tensor(image):
806+
cpu_image = image
807+
if torch.is_tensor(label):
808+
cpu_label = label
809+
if torch.is_tensor(mask):
810+
cpu_mask = mask
811+
812+
moved_batch = super().transfer_batch_to_device(batch, device, dataloader_idx)
813+
814+
if preserve_cpu_input and isinstance(moved_batch, dict):
815+
if cpu_image is not None:
816+
moved_batch["image"] = cpu_image
817+
if cpu_label is not None:
818+
moved_batch["label"] = cpu_label
819+
if cpu_mask is not None:
820+
moved_batch["mask"] = cpu_mask
821+
822+
return moved_batch
823+
780824
def test_step(self, batch: Dict[str, torch.Tensor], batch_idx: int) -> STEP_OUTPUT:
781825
"""
782826
Test step with optional sliding-window inference and metrics computation.
@@ -885,6 +929,7 @@ def test_step(self, batch: Dict[str, torch.Tensor], batch_idx: int) -> STEP_OUTP
885929
print(f"INFERENCE PLAN: {volume_name}")
886930
print(f"{'='*70}")
887931
print(f"Input shape: {tuple(images.shape)}")
932+
print(f"Input device: {images.device}")
888933

889934
# Extract sliding window parameters if available
890935
if hasattr(self.cfg, 'inference') and hasattr(self.cfg.inference, 'sliding_window'):

tutorials/neuron_nisb_base_40nm.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,11 +170,14 @@ test:
170170
inference:
171171
sliding_window:
172172
window_size: [128, 128, 128]
173-
sw_batch_size: 4
173+
sw_batch_size: 1
174174
overlap: 0.5
175175
blending: gaussian
176176
sigma_scale: 0.25
177177
padding_mode: replicate
178+
keep_input_on_cpu: true
179+
sw_device: cuda
180+
output_device: cpu
178181

179182
test_time_augmentation:
180183
enabled: false

0 commit comments

Comments
 (0)