Skip to content

Commit 41dde86

Browse files
authored
Merge branch 'dev' into GHSA-wg9g-w2j2-8pgr
2 parents eb0396e + ddf3795 commit 41dde86

16 files changed

Lines changed: 433 additions & 26 deletions

monai/apps/nnunet/nnunetv2_runner.py

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import glob
1616
import os
17+
import re
1718
import shlex
1819
import subprocess
1920
from typing import Any
@@ -34,6 +35,8 @@
3435

3536
__all__ = ["nnUNetV2Runner"]
3637

38+
DATASET_ID_FORMAT = r"Dataset[0-9]{3}|[0-9]+" # regex format for a valid nnUnet dataset name
39+
3740

3841
class nnUNetV2Runner: # noqa: N801
3942
"""
@@ -195,6 +198,13 @@ def __init__(
195198

196199
# dataset_name_or_id has to be a string
197200
self.dataset_name_or_id = str(self.input_info.pop("dataset_name_or_id", 1))
201+
self.dataset_name: str | None = None
202+
203+
# ensure the dataset name is a single identifier/number, this prevents code injection when composing commands
204+
if re.fullmatch(DATASET_ID_FORMAT, self.dataset_name_or_id) is None:
205+
raise ValueError(
206+
f"Value for dataset_name_or_id `{self.dataset_name_or_id}` not a valid dataset name or ID."
207+
)
198208

199209
try:
200210
from nnunetv2.utilities.dataset_name_id_conversion import maybe_convert_to_dataset_name
@@ -239,7 +249,7 @@ def convert_dataset(self):
239249

240250
from nnunetv2.utilities.dataset_name_id_conversion import maybe_convert_to_dataset_name
241251

242-
self.dataset_name = maybe_convert_to_dataset_name(int(self.dataset_name_or_id))
252+
self.dataset_name = maybe_convert_to_dataset_name(self.dataset_name_or_id)
243253

244254
datalist_json = ConfigParser.load_config_file(self.input_info.pop("datalist"))
245255

@@ -548,7 +558,7 @@ def train_single_model_command(
548558
Raises:
549559
ValueError: If gpu_id is an empty tuple or list.
550560
"""
551-
env = os.environ.copy()
561+
env: dict[str, str] = os.environ.copy()
552562
device_setting: str = "0"
553563
num_gpus = 1
554564
if isinstance(gpu_id, str):
@@ -574,22 +584,25 @@ def train_single_model_command(
574584

575585
cmd = [
576586
"nnUNetv2_train",
577-
f"{self.dataset_name_or_id}",
578-
f"{config}",
579-
f"{fold}",
587+
self.dataset_name_or_id,
588+
config,
589+
fold,
580590
"-tr",
581-
f"{self.trainer_class_name}",
591+
self.trainer_class_name,
582592
"-num_gpus",
583-
f"{num_gpus}",
593+
num_gpus,
584594
]
595+
585596
if self.export_validation_probabilities:
586597
cmd.append("--npz")
598+
587599
for _key, _value in kwargs.items():
588-
if _key == "p" or _key == "pretrained_weights":
589-
cmd.extend([f"-{_key}", f"{_value}"])
590-
else:
591-
cmd.extend([f"--{_key}", f"{_value}"])
592-
return cmd, env
600+
prefix = "-" if _key in {"p", "pretrained_weights"} else "--"
601+
cmd += [f"{prefix}{_key}", str(_value)]
602+
603+
cmd_str: list[str] = [str(c) for c in cmd]
604+
605+
return cmd_str, env
593606

594607
def train(
595608
self,
@@ -641,7 +654,14 @@ def train_parallel_cmd(
641654
None (all available GPUs).
642655
kwargs: this optional parameter allows you to specify additional arguments defined in the
643656
``train_single_model`` method.
657+
658+
Raises:
659+
ValueError: self.dataset_name must have a value, ie. when using an existing dataset or after creating one.
644660
"""
661+
662+
if self.dataset_name is None:
663+
raise ValueError(f"A valid dataset name must be given in {self.dataset_name=}.")
664+
645665
# unpack compressed files
646666
folder_names = []
647667
for root, _, files in os.walk(os.path.join(self.nnunet_preprocessed, self.dataset_name)):
@@ -696,7 +716,14 @@ def train_parallel(
696716
None (all available GPUs).
697717
kwargs: this optional parameter allows you to specify additional arguments defined in the
698718
``train_single_model`` method.
719+
720+
Raises:
721+
ValueError: self.dataset_name must have a value, ie. when using an existing dataset or after creating one.
699722
"""
723+
724+
if self.dataset_name is None:
725+
raise ValueError(f"A valid dataset name must be given in {self.dataset_name=}.")
726+
700727
all_cmds = self.train_parallel_cmd(configs=configs, gpu_id_for_all=gpu_id_for_all, **kwargs)
701728
for s, cmds in enumerate(all_cmds):
702729
for gpu_id, gpu_cmd in cmds.items():
@@ -908,7 +935,14 @@ def predict_ensemble_postprocessing(
908935
run_postprocessing: whether to conduct post-processing
909936
kwargs: this optional parameter allows you to specify additional arguments defined in the
910937
``predict`` method.
938+
939+
Raises:
940+
ValueError: self.dataset_name must have a value, ie. when using an existing dataset or after creating one.
911941
"""
942+
943+
if self.dataset_name is None:
944+
raise ValueError(f"A valid dataset name must be given in {self.dataset_name=}.")
945+
912946
from nnunetv2.ensembling.ensemble import ensemble_folders
913947
from nnunetv2.postprocessing.remove_connected_components import apply_postprocessing_to_folder
914948
from nnunetv2.utilities.file_path_utilities import get_output_folder

monai/losses/image_dissimilarity.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,9 +233,11 @@ def __init__(
233233
self.kernel_type = look_up_option(kernel_type, ["gaussian", "b-spline"])
234234
self.num_bins = num_bins
235235
self.kernel_type = kernel_type
236+
self.bin_centers: torch.Tensor | None
237+
self.register_buffer("bin_centers", None, persistent=False)
236238
if self.kernel_type == "gaussian":
237239
self.preterm = 1 / (2 * sigma**2)
238-
self.bin_centers = bin_centers[None, None, ...]
240+
self.register_buffer("bin_centers", bin_centers[None, None, ...], persistent=False)
239241
self.smooth_nr = float(smooth_nr)
240242
self.smooth_dr = float(smooth_dr)
241243

@@ -314,6 +316,8 @@ def parzen_windowing_gaussian(self, img: torch.Tensor) -> tuple[torch.Tensor, to
314316
"""
315317
img = torch.clamp(img, 0, 1)
316318
img = img.reshape(img.shape[0], -1, 1) # (batch, num_sample, 1)
319+
if self.bin_centers is None:
320+
raise ValueError("bin_centers must be defined for gaussian parzen windowing.")
317321
weight = torch.exp(
318322
-self.preterm.to(img) * (img - self.bin_centers.to(img)) ** 2
319323
) # (batch, num_sample, num_bin)

monai/networks/blocks/crossattention.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
from __future__ import annotations
1313

14+
from typing import Optional
15+
1416
import torch
1517
import torch.nn as nn
1618

@@ -139,7 +141,7 @@ def __init__(
139141
)
140142
self.input_size = input_size
141143

142-
def forward(self, x: torch.Tensor, context: torch.Tensor | None = None):
144+
def forward(self, x: torch.Tensor, context: Optional[torch.Tensor] = None): # noqa: UP045
143145
"""
144146
Args:
145147
x (torch.Tensor): input tensor. B x (s_dim_1 * ... * s_dim_n) x C

monai/networks/blocks/selfattention.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
from __future__ import annotations
1313

14+
from typing import Optional
15+
1416
import torch
1517
import torch.nn as nn
1618
import torch.nn.functional as F
@@ -158,7 +160,7 @@ def __init__(
158160
)
159161
self.input_size = input_size
160162

161-
def forward(self, x, attn_mask: torch.Tensor | None = None):
163+
def forward(self, x, attn_mask: Optional[torch.Tensor] = None): # noqa: UP045
162164
"""
163165
Args:
164166
x (torch.Tensor): input tensor. B x (s_dim_1 * ... * s_dim_n) x C

monai/networks/blocks/transformerblock.py

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
from __future__ import annotations
1313

14+
from typing import Optional
15+
1416
import torch
1517
import torch.nn as nn
1618

@@ -23,6 +25,11 @@ class TransformerBlock(nn.Module):
2325
An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale <https://arxiv.org/abs/2010.11929>"
2426
"""
2527

28+
# Treat ``with_cross_attention`` as a TorchScript constant so the cross-attention branch in
29+
# ``forward`` is pruned when it is False. Otherwise scripting tries to compile the
30+
# ``self.cross_attn(..., context=context)`` call against ``nn.Identity`` and fails.
31+
__constants__ = ["with_cross_attention"]
32+
2633
def __init__(
2734
self,
2835
hidden_size: int,
@@ -46,11 +53,20 @@ def __init__(
4653
dropout_rate (float, optional): fraction of the input units to drop. Defaults to 0.0.
4754
qkv_bias(bool, optional): apply bias term for the qkv linear layer. Defaults to False.
4855
save_attn (bool, optional): to make accessible the attention matrix. Defaults to False.
56+
causal (bool, optional): whether to apply causal masking in self-attention. Defaults to False.
57+
sequence_length (int | None, optional): sequence length required for causal masking. Defaults to None.
58+
with_cross_attention (bool, optional): whether to include cross-attention layers that attend to an
59+
external context tensor. When False, cross_attn is set to nn.Identity() so that the attribute
60+
always exists for typing and checkpoint compatibility. Defaults to False.
4961
use_flash_attention: if True, use Pytorch's inbuilt flash attention for a memory efficient attention mechanism
5062
(see https://pytorch.org/docs/2.2/generated/torch.nn.functional.scaled_dot_product_attention.html).
5163
include_fc: whether to include the final linear layer. Default to True.
5264
use_combined_linear: whether to use a single linear layer for qkv projection, default to True.
5365
66+
Raises:
67+
ValueError: if dropout_rate is not in [0, 1].
68+
ValueError: if hidden_size is not divisible by num_heads.
69+
5470
"""
5571

5672
super().__init__()
@@ -79,17 +95,24 @@ def __init__(
7995
self.with_cross_attention = with_cross_attention
8096

8197
self.norm_cross_attn = nn.LayerNorm(hidden_size)
82-
self.cross_attn = CrossAttentionBlock(
83-
hidden_size=hidden_size,
84-
num_heads=num_heads,
85-
dropout_rate=dropout_rate,
86-
qkv_bias=qkv_bias,
87-
causal=False,
88-
use_flash_attention=use_flash_attention,
89-
)
98+
self.cross_attn: CrossAttentionBlock | nn.Identity
99+
if with_cross_attention:
100+
self.cross_attn = CrossAttentionBlock(
101+
hidden_size=hidden_size,
102+
num_heads=num_heads,
103+
dropout_rate=dropout_rate,
104+
qkv_bias=qkv_bias,
105+
causal=False,
106+
use_flash_attention=use_flash_attention,
107+
)
108+
else:
109+
self.cross_attn = nn.Identity()
90110

91111
def forward(
92-
self, x: torch.Tensor, context: torch.Tensor | None = None, attn_mask: torch.Tensor | None = None
112+
self,
113+
x: torch.Tensor,
114+
context: Optional[torch.Tensor] = None, # noqa: UP045
115+
attn_mask: Optional[torch.Tensor] = None, # noqa: UP045
93116
) -> torch.Tensor:
94117
x = x + self.attn(self.norm1(x), attn_mask=attn_mask)
95118
if self.with_cross_attention:

monai/networks/nets/masked_autoencoder_vit.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,3 +209,41 @@ def forward(self, x, masking_ratio: float | None = None):
209209

210210
x = x[:, 1:, :]
211211
return x, mask
212+
213+
def load_old_state_dict(self, old_state_dict: dict, verbose: bool = False) -> None:
214+
"""
215+
Load a state dict from a MaskedAutoEncoderViT model trained with an older version of MONAI
216+
where ``CrossAttentionBlock`` was unconditionally instantiated in ``TransformerBlock``
217+
even when ``with_cross_attention=False``. Old checkpoints contain stale
218+
``blocks.{i}.cross_attn.*`` and ``decoder_blocks.{i}.cross_attn.*`` keys that are not
219+
present in the current model and are automatically dropped.
220+
221+
Args:
222+
old_state_dict: state dict from the older MaskedAutoEncoderViT model.
223+
verbose: if True, print keys that are missing or unmatched. Defaults to False.
224+
"""
225+
new_state_dict = self.state_dict()
226+
if all(k in new_state_dict for k in old_state_dict):
227+
if verbose:
228+
print("All keys match, loading state dict.")
229+
self.load_state_dict(old_state_dict)
230+
return
231+
232+
if verbose:
233+
for k in new_state_dict:
234+
if k not in old_state_dict:
235+
print(f"key {k} not found in old state dict")
236+
print("----------------------------------------------")
237+
for k in old_state_dict:
238+
if k not in new_state_dict:
239+
print(f"key {k} not found in new state dict")
240+
241+
# copy over all matching keys; stale cross_attn.* keys in blocks and decoder_blocks
242+
# are left as unmatched leftovers and are not inserted into new_state_dict
243+
for k in new_state_dict:
244+
if k in old_state_dict:
245+
new_state_dict[k] = old_state_dict.pop(k)
246+
247+
if verbose:
248+
print("remaining keys in old_state_dict:", old_state_dict.keys())
249+
self.load_state_dict(new_state_dict)

monai/networks/nets/vit.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,41 @@ def forward(self, x):
131131
if hasattr(self, "classification_head"):
132132
x = self.classification_head(x[:, 0])
133133
return x, hidden_states_out
134+
135+
def load_old_state_dict(self, old_state_dict: dict, verbose: bool = False) -> None:
136+
"""
137+
Load a state dict from a ViT model trained with an older version of MONAI where
138+
``CrossAttentionBlock`` was unconditionally instantiated in ``TransformerBlock``
139+
even when ``with_cross_attention=False``. Old checkpoints contain stale
140+
``blocks.{i}.cross_attn.*`` keys that are not present in the current model and
141+
are automatically dropped.
142+
143+
Args:
144+
old_state_dict: state dict from the older ViT model.
145+
verbose: if True, print keys that are missing or unmatched. Defaults to False.
146+
"""
147+
new_state_dict = self.state_dict()
148+
if all(k in new_state_dict for k in old_state_dict):
149+
if verbose:
150+
print("All keys match, loading state dict.")
151+
self.load_state_dict(old_state_dict)
152+
return
153+
154+
if verbose:
155+
for k in new_state_dict:
156+
if k not in old_state_dict:
157+
print(f"key {k} not found in old state dict")
158+
print("----------------------------------------------")
159+
for k in old_state_dict:
160+
if k not in new_state_dict:
161+
print(f"key {k} not found in new state dict")
162+
163+
# copy over all matching keys; stale cross_attn.* keys are left as unmatched
164+
# leftovers and are not inserted into new_state_dict
165+
for k in new_state_dict:
166+
if k in old_state_dict:
167+
new_state_dict[k] = old_state_dict.pop(k)
168+
169+
if verbose:
170+
print("remaining keys in old_state_dict:", old_state_dict.keys())
171+
self.load_state_dict(new_state_dict)

monai/networks/nets/vitautoenc.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,41 @@ def forward(self, x):
133133
x = self.conv3d_transpose(x)
134134
x = self.conv3d_transpose_1(x)
135135
return x, hidden_states_out
136+
137+
def load_old_state_dict(self, old_state_dict: dict, verbose: bool = False) -> None:
138+
"""
139+
Load a state dict from a ViTAutoEnc model trained with an older version of MONAI where
140+
``CrossAttentionBlock`` was unconditionally instantiated in ``TransformerBlock``
141+
even when ``with_cross_attention=False``. Old checkpoints contain stale
142+
``blocks.{i}.cross_attn.*`` keys that are not present in the current model and
143+
are automatically dropped.
144+
145+
Args:
146+
old_state_dict: state dict from the older ViTAutoEnc model.
147+
verbose: if True, print keys that are missing or unmatched. Defaults to False.
148+
"""
149+
new_state_dict = self.state_dict()
150+
if all(k in new_state_dict for k in old_state_dict):
151+
if verbose:
152+
print("All keys match, loading state dict.")
153+
self.load_state_dict(old_state_dict)
154+
return
155+
156+
if verbose:
157+
for k in new_state_dict:
158+
if k not in old_state_dict:
159+
print(f"key {k} not found in old state dict")
160+
print("----------------------------------------------")
161+
for k in old_state_dict:
162+
if k not in new_state_dict:
163+
print(f"key {k} not found in new state dict")
164+
165+
# copy over all matching keys; stale cross_attn.* keys are left as unmatched
166+
# leftovers and are not inserted into new_state_dict
167+
for k in new_state_dict:
168+
if k in old_state_dict:
169+
new_state_dict[k] = old_state_dict.pop(k)
170+
171+
if verbose:
172+
print("remaining keys in old_state_dict:", old_state_dict.keys())
173+
self.load_state_dict(new_state_dict)

0 commit comments

Comments
 (0)