diff --git a/docs/source/guide/get_started/installation.rst b/docs/source/guide/get_started/installation.rst index dd2ed9b352a..bb8f6c72c61 100644 --- a/docs/source/guide/get_started/installation.rst +++ b/docs/source/guide/get_started/installation.rst @@ -11,9 +11,9 @@ The current version of OpenVINO™ Training Extensions was tested in the followi - Python >= 3.10 -*********************************************** -Install OpenVINO™ Training Extensions for users -*********************************************** +********************************************************** +Install OpenVINO™ Training Extensions for users (CUDA/CPU) +********************************************************** 1. Install OpenVINO™ Training Extensions package: @@ -57,6 +57,68 @@ Install OpenVINO™ Training Extensions for users 3. Once the package is installed in the virtual environment, you can use full OpenVINO™ Training Extensions command line functionality. +************************************************************* +Install OpenVINO™ Training Extensions for users (XPU devices) +************************************************************* + +1. Follow the first two steps from above instructions +on cloning the repository and creating a virtual environment. + +2. Install Intel Extensions For Pytorch (IPEX). +Follow the `official documentation `_ to install prerequisites such as OneAPI and proper drivers. + +.. code-block:: shell + + python -m pip install torch==2.1.0a0 torchvision==0.16.0a0 torchaudio==2.1.0a0 intel-extension-for-pytorch==2.1.10+xpu --extra-index-url https://pytorch-extension.intel.com/release-whl/stable/xpu/us/ + +3. Install MMCV. +It is required to install mmcv from source to properly build it with IPEX. + +.. code-block:: shell + + git clone https://github.com/open-mmlab/mmcv + cd mmcv + git checkout v2.1.0 + MMCV_WITH_OPS=1 pip install -e . + +4. Install OpenVINO™ Training Extensions +package from either: + +* A local source in development mode + +.. code-block:: shell + + pip install -e . + +* PyPI + +.. code-block:: shell + + pip install otx + +5. Install requirements for training +excluding Pytorch. + +.. code-block:: shell + + otx install -v --do-not-install-torch + +6. Activate OneAPI environment +and export required IPEX system variables + +.. code-block:: shell + + source /path/to/intel/oneapi/setvars.sh + export LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30 + export IPEX_FP32_MATH_MODE=TF32 + +7. Once the package is installed in the virtual environment, you can use full +OpenVINO™ Training Extensions command line functionality. + +.. code-block:: shell + + otx --help + **************************************************** Install OpenVINO™ Training Extensions for developers **************************************************** diff --git a/src/otx/algo/__init__.py b/src/otx/algo/__init__.py index 968d579e5f7..29312f92f25 100644 --- a/src/otx/algo/__init__.py +++ b/src/otx/algo/__init__.py @@ -3,6 +3,24 @@ # """Module for OTX custom algorithms, e.g., model, losses, hook, etc...""" -from . import action_classification, classification, detection, segmentation, visual_prompting +from . import ( + accelerators, + action_classification, + classification, + detection, + plugins, + segmentation, + strategies, + visual_prompting, +) -__all__ = ["action_classification", "classification", "detection", "segmentation", "visual_prompting"] +__all__ = [ + "action_classification", + "classification", + "detection", + "segmentation", + "visual_prompting", + "strategies", + "accelerators", + "plugins", +] diff --git a/src/otx/algo/accelerators/__init__.py b/src/otx/algo/accelerators/__init__.py new file mode 100644 index 00000000000..5fc4b9d9d1d --- /dev/null +++ b/src/otx/algo/accelerators/__init__.py @@ -0,0 +1,8 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +"""Lightning accelerator for XPU device.""" + +from .xpu import XPUAccelerator + +__all__ = ["XPUAccelerator"] diff --git a/src/otx/algo/accelerators/xpu.py b/src/otx/algo/accelerators/xpu.py new file mode 100644 index 00000000000..f5969336ab4 --- /dev/null +++ b/src/otx/algo/accelerators/xpu.py @@ -0,0 +1,88 @@ +"""Lightning accelerator for XPU device.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +from __future__ import annotations + +from typing import Any, Union + +import numpy as np +import torch +from lightning.pytorch.accelerators import AcceleratorRegistry +from lightning.pytorch.accelerators.accelerator import Accelerator +from mmcv.ops.nms import NMSop +from mmcv.ops.roi_align import RoIAlign +from mmengine.structures import instance_data + +from otx.algo.detection.utils import monkey_patched_nms, monkey_patched_roi_align +from otx.utils.utils import is_xpu_available + + +class XPUAccelerator(Accelerator): + """Support for a XPU, optimized for large-scale machine learning.""" + + accelerator_name = "xpu" + + def setup_device(self, device: torch.device) -> None: + """Sets up the specified device.""" + if device.type != "xpu": + msg = f"Device should be xpu, got {device} instead" + raise RuntimeError(msg) + + torch.xpu.set_device(device) + self.patch_packages_xpu() + + @staticmethod + def parse_devices(devices: str | list | torch.device) -> list: + """Parses devices for multi-GPU training.""" + if isinstance(devices, list): + return devices + return [devices] + + @staticmethod + def get_parallel_devices(devices: list) -> list[torch.device]: + """Generates a list of parrallel devices.""" + return [torch.device("xpu", idx) for idx in devices] + + @staticmethod + def auto_device_count() -> int: + """Returns number of XPU devices available.""" + return torch.xpu.device_count() + + @staticmethod + def is_available() -> bool: + """Checks if XPU available.""" + return is_xpu_available() + + def get_device_stats(self, device: str | torch.device) -> dict[str, Any]: + """Returns XPU devices stats.""" + return {} + + def teardown(self) -> None: + """Cleans-up XPU-related resources.""" + self.revert_packages_xpu() + + def patch_packages_xpu(self) -> None: + """Patch packages when xpu is available.""" + # patch instance_data from mmengie + long_type_tensor = Union[torch.LongTensor, torch.xpu.LongTensor] + bool_type_tensor = Union[torch.BoolTensor, torch.xpu.BoolTensor] + instance_data.IndexType = Union[str, slice, int, list, long_type_tensor, bool_type_tensor, np.ndarray] + + # patch nms and roi_align + self._nms_op_forward = NMSop.forward + self._roi_align_forward = RoIAlign.forward + NMSop.forward = monkey_patched_nms + RoIAlign.forward = monkey_patched_roi_align + + def revert_packages_xpu(self) -> None: + """Revert packages when xpu is available.""" + NMSop.forward = self._nms_op_forward + RoIAlign.forward = self._roi_align_forward + + +AcceleratorRegistry.register( + XPUAccelerator.accelerator_name, + XPUAccelerator, + description="Accelerator supports XPU devices", +) diff --git a/src/otx/algo/detection/utils/__init__.py b/src/otx/algo/detection/utils/__init__.py new file mode 100644 index 00000000000..2ab46a64ac4 --- /dev/null +++ b/src/otx/algo/detection/utils/__init__.py @@ -0,0 +1,8 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +"""utils for detection task.""" + +from .mmcv_patched_ops import monkey_patched_nms, monkey_patched_roi_align + +__all__ = ["monkey_patched_nms", "monkey_patched_roi_align"] diff --git a/src/otx/algo/detection/utils/mmcv_patched_ops.py b/src/otx/algo/detection/utils/mmcv_patched_ops.py new file mode 100644 index 00000000000..ec3a884232d --- /dev/null +++ b/src/otx/algo/detection/utils/mmcv_patched_ops.py @@ -0,0 +1,73 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +"""utils for detection task.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import torch +from mmcv.utils import ext_loader +from torchvision.ops import nms as tv_nms +from torchvision.ops import roi_align as tv_roi_align + +if TYPE_CHECKING: + from mmcv.ops.nms import NMSop + from mmcv.ops.roi_align import RoIAlign + +ext_module = ext_loader.load_ext("_ext", ["nms", "softnms", "nms_match", "nms_rotated", "nms_quadri"]) + + +def monkey_patched_nms( + ctx: NMSop, + bboxes: torch.Tensor, + scores: torch.Tensor, + iou_threshold: float, + offset: float, + score_threshold: float, + max_num: int, +) -> torch.Tensor: + """Runs MMCVs NMS with torchvision.nms, or forces NMS from MMCV to run on CPU.""" + _ = ctx + is_filtering_by_score = score_threshold > 0 + if is_filtering_by_score: + valid_mask = scores > score_threshold + bboxes, scores = bboxes[valid_mask], scores[valid_mask] + valid_inds = torch.nonzero(valid_mask, as_tuple=False).squeeze(dim=1) + + if bboxes.dtype == torch.bfloat16: + bboxes = bboxes.to(torch.float32) + if scores.dtype == torch.bfloat16: + scores = scores.to(torch.float32) + + if offset == 0: + inds = tv_nms(bboxes, scores, float(iou_threshold)) + else: + device = bboxes.device + bboxes = bboxes.to("cpu") + scores = scores.to("cpu") + inds = ext_module.nms(bboxes, scores, iou_threshold=float(iou_threshold), offset=offset) + bboxes = bboxes.to(device) + scores = scores.to(device) + + if max_num > 0: + inds = inds[:max_num] + if is_filtering_by_score: + inds = valid_inds[inds] + return inds + + +def monkey_patched_roi_align(self: RoIAlign, _input: torch.Tensor, rois: torch.Tensor) -> torch.Tensor: + """Replaces MMCVs roi align with the one from torchvision. + + Args: + self: patched instance + _input: NCHW images + rois: Bx5 boxes. First column is the index into N. The other 4 columns are xyxy. + """ + if "aligned" in tv_roi_align.__code__.co_varnames: + return tv_roi_align(_input, rois, self.output_size, self.spatial_scale, self.sampling_ratio, self.aligned) + if self.aligned: + rois -= rois.new_tensor([0.0] + [0.5 / self.spatial_scale] * 4) + return tv_roi_align(_input, rois, self.output_size, self.spatial_scale, self.sampling_ratio) diff --git a/src/otx/algo/plugins/__init__.py b/src/otx/algo/plugins/__init__.py new file mode 100644 index 00000000000..91be640aea6 --- /dev/null +++ b/src/otx/algo/plugins/__init__.py @@ -0,0 +1,8 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +"""Plugin for mixed-precision training on XPU.""" + +from .xpu_precision import MixedPrecisionXPUPlugin + +__all__ = ["MixedPrecisionXPUPlugin"] diff --git a/src/otx/algo/plugins/xpu_precision.py b/src/otx/algo/plugins/xpu_precision.py new file mode 100644 index 00000000000..fb2a08eb182 --- /dev/null +++ b/src/otx/algo/plugins/xpu_precision.py @@ -0,0 +1,117 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +"""Plugin for mixed-precision training on XPU.""" + +from __future__ import annotations + +from contextlib import contextmanager +from typing import TYPE_CHECKING, Any, Callable, Generator + +import torch +from lightning.pytorch.plugins.precision.precision import Precision +from lightning.pytorch.utilities import GradClipAlgorithmType +from lightning.pytorch.utilities.exceptions import MisconfigurationException +from torch import Tensor +from torch.optim import LBFGS, Optimizer + +if TYPE_CHECKING: + import lightning.pytorch as pl + from lightning_fabric.utilities.types import Optimizable + + +class MixedPrecisionXPUPlugin(Precision): + """Plugin for Automatic Mixed Precision (AMP) training with ``torch.xpu.autocast``. + + Args: + scaler: An optional :class:`torch.cuda.amp.GradScaler` to use. + """ + + def __init__(self, scaler: torch.cuda.amp.GradScaler | None = None) -> None: + self.scaler = scaler + + def pre_backward(self, tensor: Tensor, module: pl.LightningModule) -> Tensor: + """Apply grad scaler before backward.""" + if self.scaler is not None: + tensor = self.scaler.scale(tensor) + return super().pre_backward(tensor, module) + + def optimizer_step( # type: ignore[override] + self, + optimizer: Optimizable, + model: pl.LightningModule, + closure: Callable, + **kwargs: dict, + ) -> None | dict: + """Make an optimizer step using scaler if it was passed.""" + if self.scaler is None: + # skip scaler logic, as bfloat16 does not require scaler + return super().optimizer_step( + optimizer, + model=model, + closure=closure, + **kwargs, + ) + if isinstance(optimizer, LBFGS): + msg = "Native AMP and the LBFGS optimizer are not compatible." + raise MisconfigurationException( + msg, + ) + closure_result = closure() + + if not _optimizer_handles_unscaling(optimizer): + # Unscaling needs to be performed here in case we are going to apply gradient clipping. + # Optimizers that perform unscaling in their `.step()` method are not supported (e.g., fused Adam). + # Note: `unscale` happens after the closure is executed, but before the `on_before_optimizer_step` hook. + self.scaler.unscale_(optimizer) + + self._after_closure(model, optimizer) + skipped_backward = closure_result is None + # in manual optimization, the closure does not return a value + if not model.automatic_optimization or not skipped_backward: + # note: the scaler will skip the `optimizer.step` if nonfinite gradients are found + step_output = self.scaler.step(optimizer, **kwargs) + self.scaler.update() + return step_output + return closure_result + + def clip_gradients( + self, + optimizer: Optimizer, + clip_val: int | float = 0.0, + gradient_clip_algorithm: GradClipAlgorithmType = GradClipAlgorithmType.NORM, + ) -> None: + """Handle grad clipping with scaler.""" + if clip_val > 0 and _optimizer_handles_unscaling(optimizer): + msg = f"The current optimizer, {type(optimizer).__qualname__}, does not allow for gradient clipping" + " because it performs unscaling of gradients internally. HINT: Are you using a 'fused' optimizer?" + raise RuntimeError( + msg, + ) + super().clip_gradients(optimizer=optimizer, clip_val=clip_val, gradient_clip_algorithm=gradient_clip_algorithm) + + @contextmanager + def forward_context(self) -> Generator[None, None, None]: + """Enable autocast context.""" + with torch.xpu.autocast(True): + yield + + def state_dict(self) -> dict[str, Any]: + """Returns state dict of the plugin.""" + if self.scaler is not None: + return self.scaler.state_dict() + return {} + + def load_state_dict(self, state_dict: dict[str, torch.Tensor]) -> None: + """Loads state dict to the plugin.""" + if self.scaler is not None: + self.scaler.load_state_dict(state_dict) + + +def _optimizer_handles_unscaling(optimizer: torch.optim.Optimizer) -> bool: + """Determines if a PyTorch optimizer handles unscaling gradients in the step method ratherthan through the scaler. + + Since, the current implementation of this function checks a PyTorch internal variable on the optimizer, the return + value will only be reliable for built-in PyTorch optimizers. + """ + return getattr(optimizer, "_step_supports_amp_scaling", False) diff --git a/src/otx/algo/strategies/__init__.py b/src/otx/algo/strategies/__init__.py new file mode 100644 index 00000000000..392a1b82b22 --- /dev/null +++ b/src/otx/algo/strategies/__init__.py @@ -0,0 +1,8 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +"""Lightning strategy for single XPU device.""" + +from .xpu_single import SingleXPUStrategy + +__all__ = ["SingleXPUStrategy"] diff --git a/src/otx/algo/strategies/xpu_single.py b/src/otx/algo/strategies/xpu_single.py new file mode 100644 index 00000000000..aa1ecf3d559 --- /dev/null +++ b/src/otx/algo/strategies/xpu_single.py @@ -0,0 +1,69 @@ +"""Lightning strategy for single XPU device.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import torch +from lightning.pytorch.strategies import StrategyRegistry +from lightning.pytorch.strategies.single_device import SingleDeviceStrategy +from lightning.pytorch.utilities.exceptions import MisconfigurationException + +from otx.utils.utils import is_xpu_available + +if TYPE_CHECKING: + import lightning.pytorch as pl + from lightning.pytorch.plugins.precision import PrecisionPlugin + from lightning_fabric.plugins import CheckpointIO + from lightning_fabric.utilities.types import _DEVICE + + +class SingleXPUStrategy(SingleDeviceStrategy): + """Strategy for training on single XPU device.""" + + strategy_name = "xpu_single" + + def __init__( + self, + device: _DEVICE = "xpu:0", + accelerator: pl.accelerators.Accelerator | None = None, + checkpoint_io: CheckpointIO | None = None, + precision_plugin: PrecisionPlugin | None = None, + ): + if not is_xpu_available(): + msg = "`SingleXPUStrategy` requires XPU devices to run" + raise MisconfigurationException(msg) + + super().__init__( + accelerator=accelerator, + device=device, + checkpoint_io=checkpoint_io, + precision_plugin=precision_plugin, + ) + + @property + def is_distributed(self) -> bool: + """Returns true if the strategy supports distributed training.""" + return False + + def setup_optimizers(self, trainer: pl.Trainer) -> None: + """Sets up optimizers.""" + super().setup_optimizers(trainer) + if len(self.optimizers) != 1: # type: ignore[has-type] + msg = "XPU strategy doesn't support multiple optimizers" + raise RuntimeError(msg) + if trainer.task != "SEMANTIC_SEGMENTATION": + model, optimizer = torch.xpu.optimize(trainer.model, optimizer=self.optimizers[0]) # type: ignore[has-type] + self.optimizers = [optimizer] + self.model = model + + +StrategyRegistry.register( + SingleXPUStrategy.strategy_name, + SingleXPUStrategy, + description="Strategy that enables training on single XPU", +) diff --git a/src/otx/cli/install.py b/src/otx/cli/install.py index 37523539b87..c67229d4ca5 100644 --- a/src/otx/cli/install.py +++ b/src/otx/cli/install.py @@ -64,10 +64,15 @@ def add_install_parser(subcommands_action: _ActionSubCommands) -> None: help="Do not install PyTorch. Choose this option if you already install PyTorch.", action="store_true", ) + subcommands_action.add_subcommand("install", parser, help="Install OTX requirements.") -def otx_install(option: str | None = None, verbose: bool = False, do_not_install_torch: bool = False) -> int: +def otx_install( + option: str | None = None, + verbose: bool = False, + do_not_install_torch: bool = False, +) -> int: """Install OTX requirements. Args: diff --git a/src/otx/core/types/device.py b/src/otx/core/types/device.py index 0d11e0393f7..bd87a5721df 100644 --- a/src/otx/core/types/device.py +++ b/src/otx/core/types/device.py @@ -10,7 +10,7 @@ class DeviceType(str, Enum): """OTX Device type definition.""" - # ("cpu", "gpu", "tpu", "ipu", "hpu", "mps", "auto") + # ("cpu", "gpu", "tpu", "ipu", "hpu", "mps", "xpu", "auto") auto = "auto" gpu = "gpu" @@ -19,3 +19,4 @@ class DeviceType(str, Enum): ipu = "ipu" hpu = "hpu" mps = "mps" + xpu = "xpu" diff --git a/src/otx/engine/engine.py b/src/otx/engine/engine.py index 8165423c7af..edd03472562 100644 --- a/src/otx/engine/engine.py +++ b/src/otx/engine/engine.py @@ -13,6 +13,7 @@ import torch from lightning import Trainer, seed_everything +from otx.algo.plugins import MixedPrecisionXPUPlugin from otx.core.config.device import DeviceConfig from otx.core.config.explain import ExplainConfig from otx.core.config.hpo import HpoConfig @@ -25,6 +26,7 @@ from otx.core.types.precision import OTXPrecisionType from otx.core.types.task import OTXTaskType from otx.core.utils.cache import TrainerArgumentsCache +from otx.utils.utils import is_xpu_available from .hpo import execute_hpo, update_hyper_parameter from .utils.auto_configurator import DEFAULT_CONFIG_PER_TASK, AutoConfigurator @@ -135,6 +137,7 @@ def __init__( label_info=self._datamodule.label_info if self._datamodule is not None else None, ) ) + self.optimizer: list[OptimizerCallable] | OptimizerCallable | None = ( optimizer if optimizer is not None else self._auto_configurator.get_optimizer() ) @@ -783,6 +786,8 @@ def device(self) -> DeviceConfig: @device.setter def device(self, device: DeviceType) -> None: + if is_xpu_available() and device == DeviceType.auto: + device = DeviceType.xpu self._device = DeviceConfig(accelerator=device) self._cache.update(accelerator=self._device.accelerator, devices=self._device.devices) @@ -804,8 +809,17 @@ def _build_trainer(self, **kwargs) -> None: """Instantiate the trainer based on the model parameters.""" if self._cache.requires_update(**kwargs) or self._trainer is None: self._cache.update(**kwargs) + # set up xpu device + if self._device.accelerator == DeviceType.xpu: + self._cache.update(strategy="xpu_single") + # add plugin for Automatic Mixed Precision on XPU + if self._cache.args["precision"] == 16: + self._cache.update(plugins=[MixedPrecisionXPUPlugin()]) + self._cache.args["precision"] = None + kwargs = self._cache.args self._trainer = Trainer(**kwargs) + self._trainer.task = self.task self.work_dir = self._trainer.default_root_dir @property diff --git a/src/otx/utils/utils.py b/src/otx/utils/utils.py index 89cf03a2c79..797274aa634 100644 --- a/src/otx/utils/utils.py +++ b/src/otx/utils/utils.py @@ -8,10 +8,19 @@ from decimal import Decimal from typing import TYPE_CHECKING, Any +import torch + if TYPE_CHECKING: from pathlib import Path +XPU_AVAILABLE = None +try: + import intel_extension_for_pytorch # noqa: F401 +except ImportError: + XPU_AVAILABLE = False + + def get_using_dot_delimited_key(key: str, target: Any) -> Any: # noqa: ANN401 """Get values of attribute in target object using dot delimited key. @@ -114,3 +123,11 @@ def remove_matched_files(directory: Path, pattern: str, file_to_leave: Path | No for weight in directory.rglob(pattern): if weight != file_to_leave: weight.unlink() + + +def is_xpu_available() -> bool: + """Checks if XPU device is available.""" + global XPU_AVAILABLE # noqa: PLW0603 + if XPU_AVAILABLE is None: + XPU_AVAILABLE = hasattr(torch, "xpu") and torch.xpu.is_available() + return XPU_AVAILABLE diff --git a/tests/unit/algo/accelerators/__init__.py b/tests/unit/algo/accelerators/__init__.py new file mode 100644 index 00000000000..9996ffc6523 --- /dev/null +++ b/tests/unit/algo/accelerators/__init__.py @@ -0,0 +1,4 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +"""Unit tests of accelerators of OTX algo.""" diff --git a/tests/unit/algo/accelerators/test_xpu.py b/tests/unit/algo/accelerators/test_xpu.py new file mode 100644 index 00000000000..793bbe18331 --- /dev/null +++ b/tests/unit/algo/accelerators/test_xpu.py @@ -0,0 +1,62 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +"""Test for otx.algo.accelerators.xpu""" + + +import pytest +import torch +from otx.algo.accelerators import XPUAccelerator +from otx.utils.utils import is_xpu_available + + +class TestXPUAccelerator: + @pytest.fixture() + def accelerator(self, mocker): + mock_torch = mocker.patch("otx.algo.accelerators.xpu.torch") + mocker.patch.object(XPUAccelerator, "patch_packages_xpu") + mocker.patch.object(XPUAccelerator, "teardown") + return XPUAccelerator(), mock_torch + + def test_setup_device(self, accelerator): + accelerator, mock_torch = accelerator + device = torch.device("xpu") + accelerator.setup_device(device) + assert mock_torch.xpu.set_device.called + + def test_parse_devices(self, accelerator): + accelerator, _ = accelerator + devices = [1, 2, 3] + parsed_devices = accelerator.parse_devices(devices) + assert isinstance(parsed_devices, list) + assert parsed_devices == devices + + def test_get_parallel_devices(self, accelerator, mocker): + accelerator, _ = accelerator + devices = [1, 2, 3] + parallel_devices = accelerator.get_parallel_devices(devices) + assert isinstance(parallel_devices, list) + for device in parallel_devices: + assert isinstance(device, mocker.MagicMock) + + def test_auto_device_count(self, accelerator, mocker): + accelerator, mock_torch = accelerator + count = accelerator.auto_device_count() + assert isinstance(count, mocker.MagicMock) + assert mock_torch.xpu.device_count.called + + def test_is_available(self, accelerator): + accelerator, _ = accelerator + available = accelerator.is_available() + assert isinstance(available, bool) + assert available == is_xpu_available() + + def test_get_device_stats(self, accelerator): + accelerator, _ = accelerator + device = torch.device("xpu") + stats = accelerator.get_device_stats(device) + assert isinstance(stats, dict) + + def test_teardown(self, accelerator): + accelerator, _ = accelerator + accelerator.teardown() diff --git a/tests/unit/algo/detection/utils/__init__.py b/tests/unit/algo/detection/utils/__init__.py new file mode 100644 index 00000000000..a3c91b9065c --- /dev/null +++ b/tests/unit/algo/detection/utils/__init__.py @@ -0,0 +1,4 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +"""Test of utils for OTX Detection task.""" diff --git a/tests/unit/algo/detection/utils/test_mmcv_patched_ops.py b/tests/unit/algo/detection/utils/test_mmcv_patched_ops.py new file mode 100644 index 00000000000..09daa1b2cab --- /dev/null +++ b/tests/unit/algo/detection/utils/test_mmcv_patched_ops.py @@ -0,0 +1,139 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +"""Test of mmcv_patched_ops.""" + +import pytest +import torch +from mmcv.ops import nms +from otx.algo.detection.utils.mmcv_patched_ops import monkey_patched_nms + + +class TestMonkeyPatchedNMS: + @pytest.fixture() + def setup(self): + self.ctx = None + self.bboxes = torch.tensor( + [[0.324, 0.422, 0.469, 0.123], [0.324, 0.422, 0.469, 0.123], [0.314, 0.423, 0.469, 0.123]], + ) + self.scores = torch.tensor([0.9, 0.2, 0.3]) + self.iou_threshold = 0.5 + self.offset = 0 + self.score_threshold = 0 + self.max_num = 0 + + def test_case1(self, setup): + # Testing when is_filtering_by_score is False + result = monkey_patched_nms( + self.ctx, + self.bboxes, + self.scores, + self.iou_threshold, + self.offset, + self.score_threshold, + self.max_num, + ) + assert torch.equal(result, torch.tensor([0, 2, 1])) + + def test_case2(self, setup): + # Testing when is_filtering_by_score is True + self.score_threshold = 0.8 + result = monkey_patched_nms( + self.ctx, + self.bboxes, + self.scores, + self.iou_threshold, + self.offset, + self.score_threshold, + self.max_num, + ) + assert torch.equal(result, torch.tensor([0])) + + def test_case3(self, setup): + # Testing when bboxes and scores have torch.bfloat16 dtype + self.bboxes = torch.tensor( + [[0.324, 0.422, 0.469, 0.123], [0.324, 0.422, 0.469, 0.123], [0.314, 0.423, 0.469, 0.123]], + dtype=torch.bfloat16, + ) + self.scores = torch.tensor([0.9, 0.2, 0.3], dtype=torch.bfloat16) + result1 = monkey_patched_nms( + self.ctx, + self.bboxes, + self.scores, + self.iou_threshold, + self.offset, + self.score_threshold, + self.max_num, + ) + assert torch.equal(result1, torch.tensor([0, 2, 1])) + + def test_case4(self, setup): + # Testing when offset is not 0 + self.offset = 1 + result = monkey_patched_nms( + self.ctx, + self.bboxes, + self.scores, + self.iou_threshold, + self.offset, + self.score_threshold, + self.max_num, + ) + assert torch.equal(result, torch.tensor([0])) + + def test_case5(self, setup): + # Testing when max_num is greater than 0 + self.max_num = 1 + result = monkey_patched_nms( + self.ctx, + self.bboxes, + self.scores, + self.iou_threshold, + self.offset, + self.score_threshold, + self.max_num, + ) + assert torch.equal(result, torch.tensor([0])) + + def test_case6(self, setup): + # Testing that monkey_patched_nms equals mmcv nms + self.score_threshold = 0.7 + result1 = monkey_patched_nms( + self.ctx, + self.bboxes, + self.scores, + self.iou_threshold, + self.offset, + self.score_threshold, + self.max_num, + ) + result2 = nms(self.bboxes, self.scores, self.iou_threshold, score_threshold=self.score_threshold) + assert torch.equal(result1, result2[1]) + # test random bboxes and scores + bboxes = torch.rand((100, 4)) + scores = torch.rand(100) + result1 = monkey_patched_nms( + self.ctx, + bboxes, + scores, + self.iou_threshold, + self.offset, + self.score_threshold, + self.max_num, + ) + result2 = nms(bboxes, scores, self.iou_threshold, score_threshold=self.score_threshold) + assert torch.equal(result1, result2[1]) + # no score threshold + self.iou_threshold = 0.7 + self.score_threshold = 0.0 + result1 = monkey_patched_nms( + self.ctx, + bboxes, + scores, + self.iou_threshold, + self.offset, + self.score_threshold, + self.max_num, + ) + result2 = nms(bboxes, scores, self.iou_threshold) + assert torch.equal(result1, result2[1]) diff --git a/tests/unit/algo/plugins/__init__.py b/tests/unit/algo/plugins/__init__.py new file mode 100644 index 00000000000..be7fe475146 --- /dev/null +++ b/tests/unit/algo/plugins/__init__.py @@ -0,0 +1,4 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +"""Unit tests of plugins of OTX algo.""" diff --git a/tests/unit/algo/plugins/test_plugins.py b/tests/unit/algo/plugins/test_plugins.py new file mode 100644 index 00000000000..a84f4ec18d6 --- /dev/null +++ b/tests/unit/algo/plugins/test_plugins.py @@ -0,0 +1,56 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +"""Test for otx.algo.plugins.xpu_precision""" + + +import pytest +import torch +from otx.algo.plugins.xpu_precision import MixedPrecisionXPUPlugin +from torch.optim import Optimizer + + +class TestMixedPrecisionXPUPlugin: + @pytest.fixture() + def plugin(self): + return MixedPrecisionXPUPlugin() + + def test_init(self, plugin): + assert plugin.scaler is None + + def test_pre_backward(self, plugin, mocker): + tensor = torch.zeros(1) + module = mocker.MagicMock() + output = plugin.pre_backward(tensor, module) + assert output == tensor + + def test_optimizer_step_no_scaler(self, plugin, mocker): + optimizer = mocker.MagicMock(Optimizer) + model = mocker.MagicMock() + closure = mocker.MagicMock() + kwargs = {} + mock_optimizer_step = mocker.patch( + "otx.algo.plugins.xpu_precision.Precision.optimizer_step", + ) + out = plugin.optimizer_step(optimizer, model, closure, **kwargs) + assert isinstance(out, mocker.MagicMock) + mock_optimizer_step.assert_called_once() + + def test_optimizer_step_with_scaler(self, plugin, mocker): + optimizer = mocker.MagicMock(Optimizer) + model = mocker.MagicMock() + closure = mocker.MagicMock() + plugin.scaler = mocker.MagicMock() + kwargs = {} + out = plugin.optimizer_step(optimizer, model, closure, **kwargs) + assert isinstance(out, mocker.MagicMock) + + def test_clip_gradients(self, plugin, mocker): + optimizer = mocker.MagicMock(Optimizer) + clip_val = 0.1 + gradient_clip_algorithm = "norm" + mock_clip_gradients = mocker.patch( + "otx.algo.plugins.xpu_precision.Precision.clip_gradients", + ) + plugin.clip_gradients(optimizer, clip_val, gradient_clip_algorithm) + mock_clip_gradients.assert_called_once() diff --git a/tests/unit/algo/strategies/__init__.py b/tests/unit/algo/strategies/__init__.py new file mode 100644 index 00000000000..8830174eb83 --- /dev/null +++ b/tests/unit/algo/strategies/__init__.py @@ -0,0 +1,4 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +"""Unit tests of strategies of OTX algo.""" diff --git a/tests/unit/algo/strategies/test_strategies.py b/tests/unit/algo/strategies/test_strategies.py new file mode 100644 index 00000000000..1e7ddfb1809 --- /dev/null +++ b/tests/unit/algo/strategies/test_strategies.py @@ -0,0 +1,51 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +"""Tests the XPU strategy.""" + + +import pytest +import pytorch_lightning as pl +import torch +from lightning.pytorch.utilities.exceptions import MisconfigurationException +from otx.algo.strategies.xpu_single import SingleXPUStrategy + + +class TestSingleXPUStrategy: + def test_init(self, mocker): + with pytest.raises(MisconfigurationException): + strategy = SingleXPUStrategy(device="xpu:0") + mocked_is_xpu_available = mocker.patch( + "otx.algo.strategies.xpu_single.is_xpu_available", + return_value=True, + ) + strategy = SingleXPUStrategy(device="xpu:0") + assert mocked_is_xpu_available.call_count == 1 + assert strategy._root_device.type == "xpu" + assert strategy.accelerator is None + + @pytest.fixture() + def strategy(self, mocker): + mocker.patch( + "otx.algo.strategies.xpu_single.is_xpu_available", + return_value=True, + ) + return SingleXPUStrategy(device="xpu:0") + + def test_is_distributed(self, strategy): + assert not strategy.is_distributed + + def test_setup_optimizers(self, strategy, mocker): + mocker.patch("otx.algo.strategies.xpu_single.torch") + mocker.patch( + "otx.algo.strategies.xpu_single.torch.xpu.optimize", + return_value=(mocker.MagicMock(), mocker.MagicMock()), + ) + trainer = pl.Trainer() + trainer.task = "CLASSIFICATION" + # Create mock optimizers and models for testing + model = torch.nn.Linear(10, 2) + strategy._optimizers = [torch.optim.Adam(model.parameters(), lr=0.001)] + strategy._model = model + strategy.setup_optimizers(trainer) + assert len(strategy.optimizers) == 1