From 9f02114e64cc505c605674a7b5d5789ebe3efc6c Mon Sep 17 00:00:00 2001 From: samet-akcay Date: Wed, 25 Jun 2025 13:38:08 +0100 Subject: [PATCH 1/9] fix(data): enable pin_memory for DataLoader instances across the codebase This commit updates various DataLoader instances in the project to enable the option, enhancing performance for data loading on GPU. Changes were made in the following files: - : Updated train and test DataLoader configurations. - : Modified datamodule DataLoader to include . - : Added to evaluation DataLoader. - : Updated DataLoader for datasets to utilize . - : Enabled for reference dataset DataLoader. - : Adjusted inference DataLoader to include . These changes aim to optimize memory usage and improve data transfer speeds during model training and inference. Signed-off-by: samet-akcay --- examples/api/02_data/mvtecad2.py | 16 ++++++++++++++-- src/anomalib/cli/cli.py | 5 +---- src/anomalib/data/datamodules/image/mvtecad2.py | 1 + src/anomalib/engine/engine.py | 8 ++------ .../models/image/winclip/lightning_model.py | 2 +- tools/inference/lightning_inference.py | 2 +- 6 files changed, 20 insertions(+), 14 deletions(-) diff --git a/examples/api/02_data/mvtecad2.py b/examples/api/02_data/mvtecad2.py index f241983696..d8f2ed6080 100644 --- a/examples/api/02_data/mvtecad2.py +++ b/examples/api/02_data/mvtecad2.py @@ -93,8 +93,20 @@ ) # Create dataloaders -train_loader = DataLoader(train_dataset, batch_size=4, shuffle=True, collate_fn=train_dataset.collate_fn) -test_loader = DataLoader(test_dataset, batch_size=4, shuffle=False, collate_fn=test_dataset.collate_fn) +train_loader = DataLoader( + train_dataset, + batch_size=4, + shuffle=True, + collate_fn=train_dataset.collate_fn, + pin_memory=True, +) +test_loader = DataLoader( + test_dataset, + batch_size=4, + shuffle=False, + collate_fn=test_dataset.collate_fn, + pin_memory=True, +) # Get some sample images train_samples = next(iter(train_loader)) diff --git a/src/anomalib/cli/cli.py b/src/anomalib/cli/cli.py index 4601f9a85d..a06f797bfb 100644 --- a/src/anomalib/cli/cli.py +++ b/src/anomalib/cli/cli.py @@ -304,10 +304,7 @@ def instantiate_classes(self) -> None: self.config_init = self.parser.instantiate_classes(self.config) self.datamodule = self._get(self.config_init, "data") if isinstance(self.datamodule, Dataset): - # Let PyTorch handle pin_memory automatically - # This ensures optimal behavior for both CPU and GPU users - # nosemgrep: trailofbits.python.automatic-memory-pinning.automatic-memory-pinning # noqa: ERA001 - self.datamodule = DataLoader(self.datamodule, collate_fn=self.datamodule.collate_fn) + self.datamodule = DataLoader(self.datamodule, collate_fn=self.datamodule.collate_fn, pin_memory=True) self.model = self._get(self.config_init, "model") self._configure_optimizers_method_to_model() self.instantiate_engine() diff --git a/src/anomalib/data/datamodules/image/mvtecad2.py b/src/anomalib/data/datamodules/image/mvtecad2.py index 9f8ba5ac1f..9daff86f0b 100644 --- a/src/anomalib/data/datamodules/image/mvtecad2.py +++ b/src/anomalib/data/datamodules/image/mvtecad2.py @@ -257,4 +257,5 @@ def test_dataloader(self, test_type: str | TestType | None = None) -> EVAL_DATAL batch_size=self.eval_batch_size, num_workers=self.num_workers, collate_fn=dataset.collate_fn, + pin_memory=True, ) diff --git a/src/anomalib/engine/engine.py b/src/anomalib/engine/engine.py index 0c31023467..cd71e6a241 100644 --- a/src/anomalib/engine/engine.py +++ b/src/anomalib/engine/engine.py @@ -648,14 +648,10 @@ def predict( msg = f"Unknown type for dataloaders {type(dataloaders)}" raise TypeError(msg) if dataset is not None: - # Let PyTorch handle pin_memory automatically - # This ensures optimal behavior for both CPU and GPU users - # nosemgrep: trailofbits.python.automatic-memory-pinning.automatic-memory-pinning # noqa: ERA001 - dataloaders.append(DataLoader(dataset, collate_fn=dataset.collate_fn)) + dataloaders.append(DataLoader(dataset, collate_fn=dataset.collate_fn, pin_memory=True)) if data_path is not None: dataset = PredictDataset(data_path) - # nosemgrep: trailofbits.python.automatic-memory-pinning.automatic-memory-pinning # noqa: ERA001 - dataloaders.append(DataLoader(dataset, collate_fn=dataset.collate_fn)) + dataloaders.append(DataLoader(dataset, collate_fn=dataset.collate_fn, pin_memory=True)) dataloaders = dataloaders or None if self._should_run_validation(model or self.model, ckpt_path): diff --git a/src/anomalib/models/image/winclip/lightning_model.py b/src/anomalib/models/image/winclip/lightning_model.py index fb6702e66c..ee38611ee3 100644 --- a/src/anomalib/models/image/winclip/lightning_model.py +++ b/src/anomalib/models/image/winclip/lightning_model.py @@ -149,7 +149,7 @@ def setup(self, stage: str) -> None: self.few_shot_source, transform=self.pre_processor.test_transform if self.pre_processor else None, ) - dataloader = DataLoader(reference_dataset, batch_size=1, shuffle=False) + dataloader = DataLoader(reference_dataset, batch_size=1, shuffle=False, pin_memory=True) else: logger.info("Collecting reference images from training dataset") dataloader = self.trainer.datamodule.train_dataloader() diff --git a/tools/inference/lightning_inference.py b/tools/inference/lightning_inference.py index f92c0d2ab6..3aee61f435 100644 --- a/tools/inference/lightning_inference.py +++ b/tools/inference/lightning_inference.py @@ -53,7 +53,7 @@ def infer(args: Namespace) -> None: # create the dataset dataset = PredictDataset(**args.data) - dataloader = DataLoader(dataset, collate_fn=dataset.collate_fn) + dataloader = DataLoader(dataset, collate_fn=dataset.collate_fn, pin_memory=True) engine.predict(model=model, dataloaders=[dataloader], ckpt_path=args.ckpt_path) From 58da4c2eedebc8a6d22cc72bedb31faab988d1a0 Mon Sep 17 00:00:00 2001 From: samet-akcay Date: Wed, 25 Jun 2025 13:43:28 +0100 Subject: [PATCH 2/9] refactor(models): streamline decoder retrieval in function This commit refactors the function in to utilize a dictionary mapping for decoder architectures, improving readability and maintainability. The previous conditional checks have been replaced with a more efficient approach, enhancing the overall structure of the code. Signed-off-by: samet-akcay --- .../components/de_resnet.py | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/anomalib/models/image/reverse_distillation/components/de_resnet.py b/src/anomalib/models/image/reverse_distillation/components/de_resnet.py index 44dcf4c0d6..a01c056dbc 100644 --- a/src/anomalib/models/image/reverse_distillation/components/de_resnet.py +++ b/src/anomalib/models/image/reverse_distillation/components/de_resnet.py @@ -495,18 +495,20 @@ def get_decoder(name: str) -> ResNet: Returns: ResNet: Decoder ResNet architecture. """ - if name in { - "resnet18", - "resnet34", - "resnet50", - "resnet101", - "resnet152", - "resnext50_32x4d", - "resnext101_32x8d", - "wide_resnet50_2", - "wide_resnet101_2", - }: - decoder = globals()[f"de_{name}"] + decoder_map = { + "resnet18": de_resnet18, + "resnet34": de_resnet34, + "resnet50": de_resnet50, + "resnet101": de_resnet101, + "resnet152": de_resnet152, + "resnext50_32x4d": de_resnext50_32x4d, + "resnext101_32x8d": de_resnext101_32x8d, + "wide_resnet50_2": de_wide_resnet50_2, + "wide_resnet101_2": de_wide_resnet101_2, + } + + if name in decoder_map: + decoder = decoder_map[name] else: msg = f"Decoder with architecture {name} not supported" raise ValueError(msg) From 6b07c77e0af8a55befbb91cdea05870e008c5164 Mon Sep 17 00:00:00 2001 From: samet-akcay Date: Wed, 25 Jun 2025 13:52:50 +0100 Subject: [PATCH 3/9] fix(model): update logit_scale initialization to use torch.log for consistency This commit modifies the initialization of the logit_scale parameter in the CLIP model to utilize torch.log instead of np.log. This change ensures consistency in tensor operations and improves compatibility with PyTorch's computation graph. Signed-off-by: samet-akcay --- src/anomalib/models/video/ai_vad/clip/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/anomalib/models/video/ai_vad/clip/model.py b/src/anomalib/models/video/ai_vad/clip/model.py index 06ee36922e..664556533c 100644 --- a/src/anomalib/models/video/ai_vad/clip/model.py +++ b/src/anomalib/models/video/ai_vad/clip/model.py @@ -317,7 +317,7 @@ def __init__( self.ln_final = LayerNorm(transformer_width) self.text_projection = nn.Parameter(torch.empty(transformer_width, embed_dim)) - self.logit_scale = nn.Parameter(torch.ones([]) * np.log(1 / 0.07)) + self.logit_scale = nn.Parameter(torch.ones([]) * torch.log(torch.tensor(1 / 0.07))) self.initialize_parameters() From c0525deafe46502049d6b2aeac00142681bd72ba Mon Sep 17 00:00:00 2001 From: samet-akcay Date: Wed, 25 Jun 2025 13:56:48 +0100 Subject: [PATCH 4/9] fix(model): update anomaly map generation to use torch tensors for calculations This commit modifies the anomaly map generation logic to utilize PyTorch tensors instead of NumPy arrays for various calculations. This change enhances compatibility with the PyTorch computation graph and improves performance by leveraging GPU acceleration. Key updates include the conversion of statistical calculations and tensor operations to use PyTorch functions, ensuring consistency in tensor handling throughout the code. Signed-off-by: samet-akcay --- src/anomalib/models/image/uflow/anomaly_map.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/anomalib/models/image/uflow/anomaly_map.py b/src/anomalib/models/image/uflow/anomaly_map.py index 4457bd17e5..03606f0d35 100644 --- a/src/anomalib/models/image/uflow/anomaly_map.py +++ b/src/anomalib/models/image/uflow/anomaly_map.py @@ -190,27 +190,27 @@ def binomial_test( torch.Tensor: Log probability tensor of shape ``(batch_size, 1, height, width)`` """ - tau = st.chi2.ppf(probability_thr, 1) - half_win = np.max([int(window_size // 2), 1]) + tau = torch.tensor(st.chi2.ppf(probability_thr, 1)) + half_win = max(int(window_size // 2), 1) n_chann = z.shape[1] # Candidates z2 = F.pad(z**2, tuple(4 * [half_win]), "reflect").detach().cpu() z2_unfold_h = z2.unfold(-2, 2 * half_win + 1, 1) - z2_unfold_hw = z2_unfold_h.unfold(-2, 2 * half_win + 1, 1).numpy() - observed_candidates_k = np.sum(z2_unfold_hw >= tau, axis=(-2, -1)) + z2_unfold_hw = z2_unfold_h.unfold(-2, 2 * half_win + 1, 1) + observed_candidates_k = torch.sum(z2_unfold_hw >= tau, dim=(-2, -1)) # All volume together - observed_candidates = np.sum(observed_candidates_k, axis=1, keepdims=True) + observed_candidates = torch.sum(observed_candidates_k, dim=1, keepdim=True) x = observed_candidates / n_chann n = int((2 * half_win + 1) ** 2) # Low precision if not high_precision: - log_prob = torch.tensor(st.binom.logsf(x, n, 1 - probability_thr) / np.log(10)) - # High precision - good and slow + log_prob = torch.tensor(st.binom.logsf(x.numpy(), n, 1 - probability_thr) / torch.log(torch.tensor(10.0))) else: + # High precision - good and slow to_mp = np.frompyfunc(mp.mpf, 1, 1) mpn = mp.mpf(n) mpp = probability_thr @@ -222,7 +222,7 @@ def integral(tensor: torch.Tensor) -> torch.Tensor: return integrate.quad(binomial_density, tensor, n)[0] integral_array = np.vectorize(integral) - prob = integral_array(x) + prob = integral_array(x.numpy()) log_prob = torch.tensor(np.log10(prob)) return log_prob From cdf8450b089ab31ec729878ddd7ef3b9e94e1a49 Mon Sep 17 00:00:00 2001 From: samet-akcay Date: Wed, 25 Jun 2025 15:24:24 +0100 Subject: [PATCH 5/9] refactor(model): enhance anomaly map generation with PyTorch for statistical calculations This commit refactors the anomaly map generation logic to replace NumPy-based statistical calculations with PyTorch equivalents, specifically using the distribution for computing tau. Additionally, it improves precision handling by allowing the use of float64 in high precision mode. The changes streamline the computation process and maintain compatibility with the PyTorch computation graph. Signed-off-by: samet-akcay --- .../models/image/uflow/anomaly_map.py | 46 ++++++++----------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/src/anomalib/models/image/uflow/anomaly_map.py b/src/anomalib/models/image/uflow/anomaly_map.py index 03606f0d35..bb4943107f 100644 --- a/src/anomalib/models/image/uflow/anomaly_map.py +++ b/src/anomalib/models/image/uflow/anomaly_map.py @@ -15,22 +15,19 @@ See Also: - :class:`AnomalyMapGenerator`: Main class for generating anomaly maps - - :func:`compute_anomaly_map`: Function to generate anomaly maps from latents """ # Copyright (C) 2023-2024 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -import numpy as np +import math + import scipy.stats as st import torch import torch.nn.functional as F # noqa: N812 -from mpmath import binomial, mp from omegaconf import ListConfig -from scipy import integrate from torch import Tensor, nn - -mp.dps = 15 # Set precision for NFA computation (in case of high_precision=True) +from torch.distributions import Normal class AnomalyMapGenerator(nn.Module): @@ -61,7 +58,6 @@ class AnomalyMapGenerator(nn.Module): torch.Size([1, 1, 256, 256]) See Also: - - :func:`compute_anomaly_map`: Main method for likelihood-based maps - :func:`compute_anomaly_mask`: Optional method for NFA-based segmentation """ @@ -190,13 +186,21 @@ def binomial_test( torch.Tensor: Log probability tensor of shape ``(batch_size, 1, height, width)`` """ - tau = torch.tensor(st.chi2.ppf(probability_thr, 1)) + # Calculate tau using pure PyTorch + normal_dist = Normal(0, 1) + p_adjusted = (probability_thr + 1) / 2 + tau = normal_dist.icdf(torch.tensor(p_adjusted)) ** 2 half_win = max(int(window_size // 2), 1) n_chann = z.shape[1] + # Use float64 for high precision mode + dtype = torch.float64 if high_precision else torch.float32 + z = z.to(dtype) + tau = tau.to(dtype) + # Candidates - z2 = F.pad(z**2, tuple(4 * [half_win]), "reflect").detach().cpu() + z2 = F.pad(z**2, tuple(4 * [half_win]), "reflect") z2_unfold_h = z2.unfold(-2, 2 * half_win + 1, 1) z2_unfold_hw = z2_unfold_h.unfold(-2, 2 * half_win + 1, 1) observed_candidates_k = torch.sum(z2_unfold_hw >= tau, dim=(-2, -1)) @@ -206,23 +210,9 @@ def binomial_test( x = observed_candidates / n_chann n = int((2 * half_win + 1) ** 2) - # Low precision - if not high_precision: - log_prob = torch.tensor(st.binom.logsf(x.numpy(), n, 1 - probability_thr) / torch.log(torch.tensor(10.0))) - else: - # High precision - good and slow - to_mp = np.frompyfunc(mp.mpf, 1, 1) - mpn = mp.mpf(n) - mpp = probability_thr - - def binomial_density(tensor: torch.tensor) -> torch.Tensor: - return binomial(mpn, to_mp(tensor)) * (1 - mpp) ** tensor * mpp ** (mpn - tensor) - - def integral(tensor: torch.Tensor) -> torch.Tensor: - return integrate.quad(binomial_density, tensor, n)[0] - - integral_array = np.vectorize(integral) - prob = integral_array(x.numpy()) - log_prob = torch.tensor(np.log10(prob)) + # Use scipy for the binomial test as PyTorch does not have a stable/direct equivalent. + # nosemgrep: trailofbits.python.numpy-in-pytorch-modules.numpy-in-pytorch-modules + x_np = x.detach().cpu().numpy() + log_prob_np = st.binom.logsf(x_np, n, 1 - probability_thr) / math.log(10) - return log_prob + return torch.from_numpy(log_prob_np).to(z.device) From f43b2ea199af0b0ea8af72ebebf89e66e5dd3cad Mon Sep 17 00:00:00 2001 From: samet-akcay Date: Wed, 25 Jun 2025 15:25:14 +0100 Subject: [PATCH 6/9] chore(license): update license year Signed-off-by: samet-akcay --- src/anomalib/models/image/uflow/anomaly_map.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/anomalib/models/image/uflow/anomaly_map.py b/src/anomalib/models/image/uflow/anomaly_map.py index bb4943107f..f27723d10b 100644 --- a/src/anomalib/models/image/uflow/anomaly_map.py +++ b/src/anomalib/models/image/uflow/anomaly_map.py @@ -17,7 +17,7 @@ - :class:`AnomalyMapGenerator`: Main class for generating anomaly maps """ -# Copyright (C) 2023-2024 Intel Corporation +# Copyright (C) 2023-2025 Intel Corporation # SPDX-License-Identifier: Apache-2.0 import math From f237dcb1c3751869d4687cd635765753f3dde702 Mon Sep 17 00:00:00 2001 From: samet-akcay Date: Wed, 25 Jun 2025 15:35:31 +0100 Subject: [PATCH 7/9] fix(download): enhance URL validation and update download logic This commit improves the URL validation in the download function to ensure only http and https schemes are allowed. Additionally, it adds comments to clarify the safety of using under these conditions, enhancing code readability and security awareness. Signed-off-by: samet-akcay --- src/anomalib/data/utils/download.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/anomalib/data/utils/download.py b/src/anomalib/data/utils/download.py index 050ce177e2..1d2ac472b0 100644 --- a/src/anomalib/data/utils/download.py +++ b/src/anomalib/data/utils/download.py @@ -309,6 +309,7 @@ def download_and_extract(root: Path, info: DownloadInfo) -> None: # audit url. allowing only http:// or https:// if info.url.startswith("http://") or info.url.startswith("https://"): with DownloadProgressBar(unit="B", unit_scale=True, miniters=1, desc=info.name) as progress_bar: + # nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected # noqa: ERA001, E501 urlretrieve( # noqa: S310 # nosec B310 url=f"{info.url}", filename=downloaded_file_path, From c3e0f79159642a9ac2d8194fb596f518a07ecf4c Mon Sep 17 00:00:00 2001 From: samet-akcay Date: Wed, 9 Jul 2025 14:05:34 +0100 Subject: [PATCH 8/9] feat(model): add model revision support in Huggingface backend This commit introduces an optional parameter in the Huggingface backend's initialization, allowing users to specify the model revision when loading the processor and model. The changes ensure that the correct model version is utilized, enhancing flexibility and usability for different model configurations. Signed-off-by: samet-akcay --- .../image/vlm_ad/backends/huggingface.py | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/anomalib/models/image/vlm_ad/backends/huggingface.py b/src/anomalib/models/image/vlm_ad/backends/huggingface.py index bfa65555e3..8aa78bde6c 100644 --- a/src/anomalib/models/image/vlm_ad/backends/huggingface.py +++ b/src/anomalib/models/image/vlm_ad/backends/huggingface.py @@ -86,16 +86,19 @@ class Huggingface(Backend): def __init__( self, model_name: str, + revision: str = "main", ) -> None: """Initialize the Huggingface backend. Args: model_name (str): Name of the Hugging Face model to use + revision (str, optional): Model revision to use. Defaults to "main". """ self.model_name: str = model_name self._ref_images: list[str] = [] self._processor: ProcessorMixin | None = None self._model: PreTrainedModel | None = None + self.revision: str = revision @property def processor(self) -> "ProcessorMixin": @@ -111,7 +114,15 @@ def processor(self) -> "ProcessorMixin": if transformers is None: msg = "transformers is not installed." raise ValueError(msg) - self._processor = transformers.LlavaNextProcessor.from_pretrained(self.model_name) + loaded_processor = transformers.LlavaNextProcessor.from_pretrained( + self.model_name, + revision=self.revision, + ) + if isinstance(loaded_processor, tuple): + self._processor = loaded_processor[0] + else: + self._processor = loaded_processor + assert self._processor is not None return self._processor @property @@ -128,7 +139,15 @@ def model(self) -> "PreTrainedModel": if transformers is None: msg = "transformers is not installed." raise ValueError(msg) - self._model = transformers.LlavaNextForConditionalGeneration.from_pretrained(self.model_name) + loaded_model = transformers.LlavaNextForConditionalGeneration.from_pretrained( + self.model_name, + revision=self.revision, + ) + if isinstance(loaded_model, tuple): + self._model = loaded_model[0] + else: + self._model = loaded_model + assert self._model is not None return self._model @staticmethod From ab5d728bebf9b5b1d97c63cd0f6aa8923d59e687 Mon Sep 17 00:00:00 2001 From: samet-akcay Date: Wed, 9 Jul 2025 14:27:56 +0100 Subject: [PATCH 9/9] refactor(model): simplify Huggingface backend by removing revision parameter This commit removes the optional `revision` parameter from the Huggingface backend's initialization, defaulting to "main" for model loading. This change streamlines the code and ensures consistent model version usage, enhancing clarity and maintainability. Signed-off-by: Samet Akcay --- src/anomalib/models/image/vlm_ad/backends/huggingface.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/anomalib/models/image/vlm_ad/backends/huggingface.py b/src/anomalib/models/image/vlm_ad/backends/huggingface.py index 8aa78bde6c..c2bf47244f 100644 --- a/src/anomalib/models/image/vlm_ad/backends/huggingface.py +++ b/src/anomalib/models/image/vlm_ad/backends/huggingface.py @@ -86,19 +86,16 @@ class Huggingface(Backend): def __init__( self, model_name: str, - revision: str = "main", ) -> None: """Initialize the Huggingface backend. Args: model_name (str): Name of the Hugging Face model to use - revision (str, optional): Model revision to use. Defaults to "main". """ self.model_name: str = model_name self._ref_images: list[str] = [] self._processor: ProcessorMixin | None = None self._model: PreTrainedModel | None = None - self.revision: str = revision @property def processor(self) -> "ProcessorMixin": @@ -116,7 +113,7 @@ def processor(self) -> "ProcessorMixin": raise ValueError(msg) loaded_processor = transformers.LlavaNextProcessor.from_pretrained( self.model_name, - revision=self.revision, + revision="main", ) if isinstance(loaded_processor, tuple): self._processor = loaded_processor[0] @@ -141,7 +138,7 @@ def model(self) -> "PreTrainedModel": raise ValueError(msg) loaded_model = transformers.LlavaNextForConditionalGeneration.from_pretrained( self.model_name, - revision=self.revision, + revision="main", ) if isinstance(loaded_model, tuple): self._model = loaded_model[0]