Skip to content

Commit bd2aea2

Browse files
author
Donglai Wei
committed
disable decoding and no saving
1 parent 1eebd6c commit bd2aea2

19 files changed

Lines changed: 1039 additions & 311 deletions

connectomics/config/gpu_utils.py

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import torch
99
import psutil
10+
import warnings
1011
from typing import Dict, Tuple
1112

1213

@@ -33,26 +34,39 @@ def get_gpu_info() -> Dict[str, any]:
3334
if not torch.cuda.is_available():
3435
return info
3536

36-
info["num_gpus"] = torch.cuda.device_count()
37+
try:
38+
info["num_gpus"] = torch.cuda.device_count()
39+
except Exception as e:
40+
warnings.warn(f"Failed to query CUDA device count: {e}")
41+
return info
3742

3843
for i in range(info["num_gpus"]):
39-
# Get GPU name
40-
info["gpu_names"].append(torch.cuda.get_device_name(i))
41-
42-
# Get memory info
43-
props = torch.cuda.get_device_properties(i)
44-
total_memory = props.total_memory / (1024**3) # Convert to GB
45-
info["total_memory_gb"].append(total_memory)
46-
47-
# Try to get available memory (may require GPU to be initialized)
4844
try:
49-
torch.cuda.set_device(i)
50-
torch.cuda.empty_cache()
51-
available_memory = (props.total_memory - torch.cuda.memory_allocated(i)) / (1024**3)
52-
info["available_memory_gb"].append(available_memory)
53-
except Exception:
54-
# Fallback: assume 90% is available
55-
info["available_memory_gb"].append(total_memory * 0.9)
45+
# Get GPU name
46+
info["gpu_names"].append(torch.cuda.get_device_name(i))
47+
48+
# Get memory info
49+
props = torch.cuda.get_device_properties(i)
50+
total_memory = props.total_memory / (1024**3) # Convert to GB
51+
info["total_memory_gb"].append(total_memory)
52+
53+
# Try to get available memory (may require GPU to be initialized)
54+
try:
55+
torch.cuda.set_device(i)
56+
torch.cuda.empty_cache()
57+
available_memory = (props.total_memory - torch.cuda.memory_allocated(i)) / (1024**3)
58+
info["available_memory_gb"].append(available_memory)
59+
except Exception:
60+
# Fallback: assume 90% is available
61+
info["available_memory_gb"].append(total_memory * 0.9)
62+
except Exception as e:
63+
# Keep the process alive when a specific GPU index is invalid/broken.
64+
# This is especially useful on clusters with dead devices or mismatched
65+
# CUDA_VISIBLE_DEVICES / SLURM GPU cgroup mappings.
66+
warnings.warn(f"Skipping GPU index {i} during CUDA probe: {e}")
67+
info["gpu_names"].append(f"<unavailable:{i}>")
68+
info["total_memory_gb"].append(0.0)
69+
info["available_memory_gb"].append(0.0)
5670

5771
return info
5872

connectomics/config/hydra_config.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,8 @@ class ModelConfig:
204204
)
205205
deep_supervision_clamp_min: float = -20.0 # Clamp logits to prevent numerical instability
206206
deep_supervision_clamp_max: float = 20.0 # Especially important at coarser scales
207+
enable_nan_detection: bool = True # Detect NaN/Inf losses and print diagnostics
208+
debug_on_nan: bool = True # Enter pdb on NaN/Inf detection (disable for non-interactive jobs)
207209

208210
# Loss configuration
209211
loss_functions: List[str] = field(default_factory=lambda: ["DiceLoss", "BCEWithLogitsLoss"])
@@ -362,6 +364,8 @@ class NNUNetPreprocessingConfig:
362364
source_spacing: Optional[List[float]] = None # If None, falls back to *_resolution fields
363365
normalization: str = "zscore" # "zscore", "none", "0-1", or "divide-K"
364366
normalization_use_nonzero_mask: bool = True
367+
clip_percentile_low: float = 0.0 # Optional clipping before normalization (fraction)
368+
clip_percentile_high: float = 1.0 # Optional clipping before normalization (fraction)
365369
force_separate_z: Optional[bool] = None # None = auto
366370
anisotropy_threshold: float = 3.0
367371
image_order: int = 3
@@ -894,7 +898,6 @@ class InferenceDataConfig:
894898
- Inference: outputs/experiment_name/YYYYMMDD_HHMMSS/inference/last.ckpt/{output_name}
895899
"""
896900

897-
test_path: str = "" # Base path for test data (e.g., "/path/to/dataset/test/")
898901
test_image: Any = None # str, List[str], or None - Can be single file or list of files
899902
test_label: Any = None # str, List[str], or None - Can be single file or list of files
900903
test_mask: Any = None # str, List[str], or None - Optional mask for inference
@@ -1150,6 +1153,7 @@ class TestDataConfig:
11501153

11511154
# These can be strings (single file), lists (multiple files), or None
11521155
# Using Any to support both str and List[str] (OmegaConf doesn't support Union of containers)
1156+
test_path: str = "" # Base path for test data (prepended to test_* paths if set)
11531157
test_image: Any = None # str, List[str], or None
11541158
test_label: Any = None # str, List[str], or None
11551159
test_mask: Any = None # str, List[str], or None

connectomics/data/augment/build.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ def build_train_transforms(
104104
normalization_use_nonzero_mask=getattr(
105105
nnunet_pre_cfg, "normalization_use_nonzero_mask", True
106106
),
107+
clip_percentile_low=getattr(nnunet_pre_cfg, "clip_percentile_low", 0.0),
108+
clip_percentile_high=getattr(nnunet_pre_cfg, "clip_percentile_high", 1.0),
107109
force_separate_z=getattr(nnunet_pre_cfg, "force_separate_z", None),
108110
anisotropy_threshold=getattr(nnunet_pre_cfg, "anisotropy_threshold", 3.0),
109111
image_order=getattr(nnunet_pre_cfg, "image_order", 3),
@@ -350,6 +352,8 @@ def _build_eval_transforms_impl(cfg: Config, mode: str = "val", keys: list[str]
350352
normalization_use_nonzero_mask=getattr(
351353
nnunet_pre_cfg, "normalization_use_nonzero_mask", True
352354
),
355+
clip_percentile_low=getattr(nnunet_pre_cfg, "clip_percentile_low", 0.0),
356+
clip_percentile_high=getattr(nnunet_pre_cfg, "clip_percentile_high", 1.0),
353357
force_separate_z=getattr(nnunet_pre_cfg, "force_separate_z", None),
354358
anisotropy_threshold=getattr(nnunet_pre_cfg, "anisotropy_threshold", 3.0),
355359
image_order=getattr(nnunet_pre_cfg, "image_order", 3),

connectomics/data/dataset/dataset_volume_cached.py

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ def __init__(
159159
patch_size: Tuple[int, int, int] = (112, 112, 112),
160160
iter_num: int = 500,
161161
transforms: Optional[Compose] = None,
162+
pre_cache_transforms: Optional[Any] = None,
162163
mode: str = "train",
163164
pad_size: Optional[Tuple[int, ...]] = None,
164165
pad_mode: str = "reflect",
@@ -181,6 +182,7 @@ def __init__(
181182

182183
self.iter_num = iter_num if iter_num > 0 else len(image_paths)
183184
self.transforms = transforms
185+
self.pre_cache_transforms = pre_cache_transforms
184186
self.mode = mode
185187
self.pad_size = pad_size
186188
self.pad_mode = pad_mode
@@ -218,17 +220,8 @@ def __init__(
218220
elif img.ndim == 3:
219221
img = img[None, ...] # Add channel for 3D
220222

221-
# Apply padding if specified
222-
if self.pad_size is not None:
223-
img = self._apply_padding(img)
224-
225-
# Ensure volume is at least as large as patch_size in all dimensions
226-
# This prevents crops from being smaller than patch_size
227-
img = self._ensure_minimum_size(img)
228-
229-
self.cached_images.append(img)
230-
231223
# Load label if available
224+
lbl = None
232225
if lbl_path:
233226
lbl = read_volume(lbl_path)
234227
# Add channel dimension for both 2D and 3D
@@ -237,6 +230,37 @@ def __init__(
237230
elif lbl.ndim == 3:
238231
lbl = lbl[None, ...] # Add channel for 3D
239232

233+
# Load mask if available
234+
mask = None
235+
if mask_path:
236+
mask = read_volume(mask_path)
237+
if mask.ndim == 2:
238+
mask = mask[None, ...]
239+
elif mask.ndim == 3:
240+
mask = mask[None, ...]
241+
242+
# Apply one-time preprocessing before caching (e.g., nnU-Net crop/resample/normalize).
243+
if self.pre_cache_transforms is not None:
244+
sample = {"image": img}
245+
if lbl is not None:
246+
sample["label"] = lbl
247+
if mask is not None:
248+
sample["mask"] = mask
249+
sample = self.pre_cache_transforms(sample)
250+
img = sample["image"]
251+
lbl = sample.get("label")
252+
mask = sample.get("mask")
253+
254+
# Apply padding if specified
255+
if self.pad_size is not None:
256+
img = self._apply_padding(img)
257+
258+
# Ensure volume is at least as large as patch_size in all dimensions
259+
# This prevents crops from being smaller than patch_size
260+
img = self._ensure_minimum_size(img)
261+
self.cached_images.append(img)
262+
263+
if lbl is not None:
240264
# Apply padding if specified (same padding as image)
241265
if self.pad_size is not None:
242266
lbl = self._apply_padding(
@@ -245,24 +269,17 @@ def __init__(
245269

246270
# Ensure label is at least as large as patch_size
247271
lbl = self._ensure_minimum_size(lbl, mode="constant", constant_values=0)
248-
249272
self.cached_labels.append(lbl)
250273
else:
251274
self.cached_labels.append(None)
252275

253-
# Load mask if available
254-
if mask_path:
255-
mask = read_volume(mask_path)
256-
if mask.ndim == 3:
257-
mask = mask[None, ...]
258-
276+
if mask is not None:
259277
# Apply padding if specified (same padding as label)
260278
if self.pad_size is not None:
261279
mask = self._apply_padding(mask, mode="constant", constant_values=0)
262280

263281
# Ensure mask is at least as large as patch_size
264282
mask = self._ensure_minimum_size(mask, mode="constant", constant_values=0)
265-
266283
self.cached_masks.append(mask)
267284
else:
268285
self.cached_masks.append(None)

connectomics/data/io/monai_transforms.py

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ class NNUNetPreprocessd(MapTransform):
101101
This transform optionally applies:
102102
- foreground crop
103103
- spacing-aware resampling
104+
- optional percentile clipping
104105
- intensity normalization
105106
106107
It stores enough metadata in ``image_meta_dict['nnunet_preprocess']`` to
@@ -117,6 +118,8 @@ def __init__(
117118
target_spacing: Optional[Sequence[float]] = None,
118119
normalization: str = "zscore",
119120
normalization_use_nonzero_mask: bool = True,
121+
clip_percentile_low: float = 0.0,
122+
clip_percentile_high: float = 1.0,
120123
force_separate_z: Optional[bool] = None,
121124
anisotropy_threshold: float = 3.0,
122125
image_order: int = 3,
@@ -132,11 +135,27 @@ def __init__(
132135
self.target_spacing = list(target_spacing) if target_spacing is not None else None
133136
self.normalization = normalization
134137
self.normalization_use_nonzero_mask = normalization_use_nonzero_mask
138+
self.clip_percentile_low = float(clip_percentile_low)
139+
self.clip_percentile_high = float(clip_percentile_high)
135140
self.force_separate_z = force_separate_z
136141
self.anisotropy_threshold = anisotropy_threshold
137142
self.image_order = image_order
138143
self.label_order = label_order
139144
self.order_z = order_z
145+
self._debug_stats_printed = False # Print one-line pre/post stats once per transform instance
146+
if not (0.0 <= self.clip_percentile_low <= 1.0):
147+
raise ValueError(
148+
f"clip_percentile_low must be in [0, 1], got {self.clip_percentile_low}"
149+
)
150+
if not (0.0 <= self.clip_percentile_high <= 1.0):
151+
raise ValueError(
152+
f"clip_percentile_high must be in [0, 1], got {self.clip_percentile_high}"
153+
)
154+
if self.clip_percentile_low > self.clip_percentile_high:
155+
raise ValueError(
156+
"clip_percentile_low must be <= clip_percentile_high, got "
157+
f"{self.clip_percentile_low} > {self.clip_percentile_high}"
158+
)
140159

141160
def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]:
142161
d = dict(data)
@@ -164,6 +183,8 @@ def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]:
164183
"target_spacing": target_spacing,
165184
"normalization": self.normalization,
166185
"normalization_use_nonzero_mask": self.normalization_use_nonzero_mask,
186+
"clip_percentile_low": self.clip_percentile_low,
187+
"clip_percentile_high": self.clip_percentile_high,
167188
"crop_bbox": None,
168189
"cropped_spatial_shape": list(image_spatial_shape),
169190
"resampled_spatial_shape": list(image_spatial_shape),
@@ -230,13 +251,37 @@ def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]:
230251
self._get_spatial_shape(image_np, spatial_dims)
231252
)
232253

233-
# Normalize image after spatial transforms.
254+
pre_stats = None
255+
if not self._debug_stats_printed:
256+
pre_stats = self._format_debug_stats(image_np)
257+
258+
# Optional clipping before normalization (mirrors SmartNormalizeIntensityd order).
259+
image_np = self._clip_image_percentiles(
260+
image_np,
261+
spatial_dims=spatial_dims,
262+
low=self.clip_percentile_low,
263+
high=self.clip_percentile_high,
264+
use_nonzero_mask=self.normalization_use_nonzero_mask,
265+
)
266+
267+
# Normalize image after spatial transforms and clipping.
234268
image_np = self._normalize_image(
235269
image_np,
236270
spatial_dims=spatial_dims,
237271
mode=self.normalization,
238272
use_nonzero_mask=self.normalization_use_nonzero_mask,
239273
)
274+
if not self._debug_stats_printed:
275+
post_stats = self._format_debug_stats(image_np)
276+
print(
277+
"[NNUNetPreprocessd] "
278+
f"key={self.image_key} mode={self.normalization} "
279+
f"clip=({self.clip_percentile_low:.3f},{self.clip_percentile_high:.3f}) "
280+
f"nonzero_mask={self.normalization_use_nonzero_mask} "
281+
f"pre={pre_stats} post={post_stats}",
282+
flush=True,
283+
)
284+
self._debug_stats_printed = True
240285
d[self.image_key] = self._from_numpy(image_np, image_state)
241286

242287
meta_dict["nnunet_preprocess"] = preprocess_meta
@@ -299,6 +344,20 @@ def _normalize_spacing(
299344
return values[-spatial_dims:]
300345
return None
301346

347+
@staticmethod
348+
def _format_debug_stats(array: np.ndarray) -> str:
349+
arr = np.asarray(array, dtype=np.float32)
350+
if arr.size == 0:
351+
return "empty"
352+
finite = arr[np.isfinite(arr)]
353+
if finite.size == 0:
354+
return "all_nonfinite"
355+
return (
356+
f"shape={tuple(int(v) for v in arr.shape)} "
357+
f"min={float(finite.min()):.4f} max={float(finite.max()):.4f} "
358+
f"mean={float(finite.mean()):.4f} std={float(finite.std()):.4f}"
359+
)
360+
302361
@staticmethod
303362
def _create_nonzero_mask(image: np.ndarray, spatial_dims: int) -> Optional[np.ndarray]:
304363
if image.ndim == spatial_dims + 1:
@@ -449,6 +508,48 @@ def _resample_spatial(
449508
)
450509
return stacked
451510

511+
@staticmethod
512+
def _clip_image_percentiles(
513+
image: np.ndarray,
514+
spatial_dims: int,
515+
low: float,
516+
high: float,
517+
use_nonzero_mask: bool,
518+
) -> np.ndarray:
519+
if low <= 0.0 and high >= 1.0:
520+
return image
521+
522+
image = image.astype(np.float32, copy=False)
523+
low_q = float(low) * 100.0
524+
high_q = float(high) * 100.0
525+
526+
def _apply(volume: np.ndarray, mask: Optional[np.ndarray]) -> np.ndarray:
527+
if mask is not None and np.any(mask):
528+
values = volume[mask]
529+
else:
530+
values = volume.reshape(-1)
531+
if values.size == 0:
532+
return volume
533+
low_val = float(np.percentile(values, low_q))
534+
high_val = float(np.percentile(values, high_q))
535+
if high_val < low_val:
536+
low_val, high_val = high_val, low_val
537+
return np.clip(volume, low_val, high_val)
538+
539+
if image.ndim == spatial_dims + 1:
540+
nonzero_mask = np.any(image != 0, axis=0) if use_nonzero_mask else None
541+
for c in range(image.shape[0]):
542+
image[c] = _apply(image[c], nonzero_mask)
543+
if nonzero_mask is not None:
544+
image[c][~nonzero_mask] = 0
545+
return image
546+
547+
mask = image != 0 if use_nonzero_mask else None
548+
image = _apply(image, mask)
549+
if mask is not None:
550+
image[~mask] = 0
551+
return image
552+
452553
@staticmethod
453554
def _normalize_image(
454555
image: np.ndarray,

connectomics/data/process/segment.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
Segmentation processing functions for PyTorch Connectomics.
33
"""
44

5-
from __future__ import print_function, division
6-
from typing import Optional, Union, List
5+
from typing import List, Optional, Union
6+
7+
import cc3d
78
import numpy as np
89
import torch
910
from skimage.morphology import binary_dilation, dilation, erosion
10-
import cc3d
1111

1212
RATES_TYPE = Optional[Union[List[int], int]]
1313

0 commit comments

Comments
 (0)