From aaed479cd7176e83990f8956cbb67d340cba3231 Mon Sep 17 00:00:00 2001 From: kprokofi Date: Fri, 19 Jun 2026 22:42:21 +0900 Subject: [PATCH 01/15] enhance create_engine, update docs --- library/README.md | 351 ++++++++++++++---- .../source/guide/get_started/api_tutorial.rst | 6 +- .../base/how_to_train/classification.rst | 2 +- .../tutorials/base/how_to_train/detection.rst | 2 +- .../how_to_train/instance_segmentation.rst | 2 +- .../how_to_train/semantic_segmentation.rst | 2 +- .../backend/lightning/cli/__init__.py | 11 - .../src/getitune/backend/lightning/engine.py | 111 +++--- .../src/getitune/backend/openvino/engine.py | 35 ++ .../getitune/backend/ultralytics/engine.py | 66 ++++ .../backend/ultralytics/tools/configurator.py | 6 +- library/src/getitune/benchmark/experiment.py | 6 +- library/src/getitune/benchmark/manifest.py | 2 +- library/src/getitune/cli/cli.py | 2 +- library/src/getitune/engine/__init__.py | 51 +-- library/src/getitune/engine/engine.py | 34 ++ library/src/getitune/engine/utils/__init__.py | 4 + library/src/getitune/engine/utils/create.py | 213 +++++++++++ library/src/getitune/models/__init__.py | 17 + .../src/getitune/tools/auto_configurator.py | 2 +- library/src/getitune/utils/__init__.py | 9 +- .../cli/utils.py => utils/recipes.py} | 6 +- library/tests/integration/api/test_engine.py | 2 +- library/tests/integration/api/test_xai.py | 4 +- library/tests/integration/conftest.py | 2 +- library/tests/perf_v2/benchmark.py | 4 +- .../unit/backend/lightning/test_engine.py | 6 +- .../unit/backend/lightning/utils/test_api.py | 2 +- .../backend/lightning/utils/test_imports.py | 2 +- 29 files changed, 751 insertions(+), 211 deletions(-) delete mode 100644 library/src/getitune/backend/lightning/cli/__init__.py create mode 100644 library/src/getitune/engine/utils/create.py rename library/src/getitune/{backend/lightning/cli/utils.py => utils/recipes.py} (95%) diff --git a/library/README.md b/library/README.md index 4125d6c45c3..980126fe193 100644 --- a/library/README.md +++ b/library/README.md @@ -44,9 +44,9 @@ Each supported task ships with curated "recipes": YAML files that bundle the mod ### Key Features -- **Multi-task support**: classification, object detection, rotated detection, instance segmentation, semantic segmentation, and keypoint detection, see the [full model list below](#supported-tasks--models). +- **Multi-task support**: classification, object detection, instance segmentation, semantic segmentation, and keypoint detection, see the [full model list below](#supported-tasks--models). - **Tiling** for large images across detection and segmentation tasks. -- **Multiple backends**: train with PyTorch Lightning, export and run inference with ONNX and OpenVINO™. +- **Multiple backends**: train with PyTorch Lightning, Ultralytics YOLO, export and run inference with ONNX and OpenVINO™. - **Hardware acceleration**: Intel GPU (XPU) and NVIDIA CUDA support. - [Datumaro](https://github.com/open-edge-platform/datumaro/tree/develop/src/datumaro/experimental) **data frontend** with automatic format detection (COCO, YOLO, VOC, native). - **Distributed training** across multiple GPUs. @@ -64,8 +64,7 @@ All recipes live under `src/getitune/recipe//`. Pass any of these YAMLs di | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Classification (multi-class / multi-label / hierarchical) | [multi_class_cls](src/getitune/recipe/classification/multi_class_cls/), [multi_label_cls](src/getitune/recipe/classification/multi_label_cls/), [h_label_cls](src/getitune/recipe/classification/h_label_cls/) | `dino_v2`, `vit_tiny`, `efficientnet_b0`, `efficientnet_b3`, `efficientnet_v2`, `mobilenet_v3_large` | | Object detection | [detection](src/getitune/recipe/detection/) | `atss_mobilenetv2`, `ssd_mobilenetv2`, `yolox_{tiny,s,l,x}`, `rtdetr_50`, `dfine_x`, `deim_dfine_{l,m,x}`, `deimv2_{s,m,l}`, `rfdetr_{small,medium,large}`, `yolo26_{n,s,m}` | -| Rotated detection | [rotated_detection](src/getitune/recipe/rotated_detection/) | `maskrcnn_r50`, `maskrcnn_efficientnetb2b` (with `_tile` variants) | -| Instance segmentation | [instance_segmentation](src/getitune/recipe/instance_segmentation/) | `maskrcnn_{r50,swint,efficientnetb2b}`, `rtmdet_inst_tiny`, `rfdetr_seg_{small,medium,large,xlarge}`, `yolo26_{n,s,m}_seg` | +| Instance segmentation | [instance_segmentation](src/getitune/recipe/instance_segmentation/) | `maskrcnn_{r50,swint,efficientnetb2b}`, `rtmdet_inst_tiny`, `rfdetr_seg_{small,medium,large,xlarge}`, `yolo26_{n,s,m}_seg` | | Semantic segmentation | [semantic_segmentation](src/getitune/recipe/semantic_segmentation/) | `dino_v2`, `litehrnet_{s,18,x}`, `segnext_{t,s,b}` (with `_tile` variants) | | Keypoint detection | [keypoint_detection](src/getitune/recipe/keypoint_detection/) | `rtmpose_tiny` | @@ -79,16 +78,16 @@ Licensing Information: Ultralytics YOLO models are distributed under the AGPL-3. Requirements: **Python 3.11–3.14**, **PyTorch 2.10**, **OpenVINO™ 2026.1**, **NumPy ≥ 2.0**. -> **Note:** `getitune` is not yet published to PyPI. Until the first release lands, install from source. - ## Quick Install ```bash # With uv (recommended) -uv pip install "getitune[cpu]" +uv pip install "getitune" # Or with pip -pip install "getitune[cpu]" +pip install "getitune" + +# For hardware-specific PyTorch wheels, see "Advanced Installation: Specify Hardware Backend" below. ```
@@ -96,43 +95,104 @@ pip install "getitune[cpu]" `getitune` ships three mutually exclusive extras that select the right PyTorch wheel for your hardware: -| Extra | PyTorch wheel | Use when | -| -------- | ---------------------------------------------------------------------- | ------------------------------------ | -| `[cpu]` | `torch==2.10.0+cpu` (Linux/Windows) or default `torch==2.10.0` (macOS) | No GPU, or running on Apple silicon. | -| `[xpu]` | `torch==2.10.0+xpu` + `triton-xpu` | Intel discrete or integrated GPUs. | -| `[cuda]` | `torch==2.10.0+cu128` | NVIDIA GPUs with CUDA 12.8 drivers. | +| Extra | PyTorch wheel | Use when | Setup Guide | +| -------- | ---------------------------------------------------------------------- | ------------------------------------ | --- | +| `[cpu]` | `torch==2.10.0+cpu` (Linux/Windows) or default `torch==2.10.0` (macOS) | No GPU, or running on Apple silicon. | — | +| `[xpu]` | `torch==2.10.0+xpu` + `triton-xpu` | Intel discrete or integrated GPUs. | [Intel GPU drivers](https://github.com/intel/compute-runtime/releases) | +| `[cuda]` | `torch==2.10.0+cu128` | NVIDIA GPUs with CUDA 12.8 drivers. | [NVIDIA CUDA Toolkit](https://developer.nvidia.com/cuda-12-8-0-download-archive) | ```bash -pip install "getitune[cpu]" # CPU-only -pip install "getitune[xpu]" # Intel GPU (XPU) -pip install "getitune[cuda]" # NVIDIA GPU (CUDA 12.8) +# Intel GPU (XPU) +uv pip install "getitune[xpu]" --extra-index-url https://download.pytorch.org/whl/xpu + +# NVIDIA GPU (CUDA 12.8) +uv pip install "getitune[cuda]" --extra-index-url https://download.pytorch.org/whl/cu128 + +# CPU-only (no extra index needed) +uv pip install "getitune[cpu]" ``` > **macOS note**: PyTorch's `+cpu` wheel is only published for Linux and Windows. The `[cpu]` extra resolves this automatically and installs the default `torch==2.10.0` wheel on macOS. +> **Ultralytics YOLO models**: YOLO26 support requires the additional `[ultralytics]` extra. Combine it with any hardware extra: +> +> ```bash +> uv pip install "getitune[xpu,ultralytics]" # Intel GPU + YOLO +> uv pip install "getitune[cuda,ultralytics]" # NVIDIA GPU + YOLO +> uv pip install "getitune[cpu,ultralytics]" # CPU + YOLO +> ``` +
Advanced Installation: Install from Source ```bash -git clone https://github.com/open-edge-platform/training_extensions.git -cd training_extensions/library +git clone https://github.com/open-edge-platform/geti.git +cd geti/library # Recommended: use uv to honor the lockfile -uv sync --extra cpu # or --extra xpu / --extra cuda +uv sync # CPU-only +uv sync --extra xpu # Intel GPU (XPU) — setup: https://github.com/intel/compute-runtime/releases +uv sync --extra cuda # NVIDIA GPU (CUDA 12.8) — setup: https://developer.nvidia.com/cuda-12-8-0-download-archive # Or with pip in a virtual environment python -m venv .venv && source .venv/bin/activate -pip install -e ".[cpu]" # remove -e for a non-editable install + +# CPU-only +pip install -e ".[cpu]" + +# Intel GPU (XPU) +pip install -e ".[xpu]" \ + --extra-index-url https://download.pytorch.org/whl/xpu + +# NVIDIA GPU (CUDA 12.8) +pip install -e ".[cuda]" \ + --extra-index-url https://download.pytorch.org/whl/cu128 ``` +> **Ultralytics YOLO models**: Add `--extra ultralytics` for `uv sync` or `[ultralytics]` for `pip install`: +> +> ```bash +> uv sync --extra xpu --extra ultralytics # Intel GPU + YOLO +> pip install -e ".[xpu,ultralytics]" # Intel GPU + YOLO +> ``` +
--- ## Quick Start +### Discovering Recipes and Models + +To explore available models and recipes: + +```python +from getitune.utils import list_models + +# List all available models names +all_models = list_models() + +# List all available recipes / configuration files (full YAML paths) +all_recipes = list_models(return_recipes=True) + +# Filter by task +detection_models = list_models(task="DETECTION") + +# Filter by pattern +efficient_models = list_models(pattern="*efficient*") +``` + +Then pass any model name to `create_engine(model="...", data="...")`. + +> **Note on Model Resolution:** +> - If you pass a **recipe YAML path** (with `.yaml` or `.yml` suffix) that doesn't exist on disk, a `FileNotFoundError` is raised. +> - If you pass a **model name** that matches recipes under multiple tasks, a `ValueError` is raised listing the matches. Pass `task=` to disambiguate. +> - Use `list_models(task="...", return_recipes=True)` to get full recipe paths instead of just model names. + +--- + ### Training Getitune supports an API-based training approach: @@ -140,36 +200,110 @@ Getitune supports an API-based training approach: ```python from getitune.engine import create_engine -# Initialize and train using the bundled test dataset +# Initialize and train using a recipe and dataset engine = create_engine( - data="tests/assets/classification_cifar10", - model="src/getitune/recipe/classification/multi_class_cls/efficientnet_b0.yaml", + model="src/getitune/recipe/classification/multi_class_cls/efficientnet_b0.yaml", # any supported model name from the model catalog (see below example), recipe.yaml path or instantiated model class directly + data="tests/assets/classification_cifar10", # path to dataset root (any supported format, e.g., COCO, VOC, YOLO, Datumaro) or DataModule instance + work_dir="./my_workspace", # Defaults to "./getitune-workspace" + device="auto", # "auto", "cpu", "gpu", "0", "xpu", etc. ) -engine.train() +engine.train(max_epochs=50) engine.test() -exported_path = engine.export() # writes OpenVINO IR +``` + +> **Tip:** Pass `model=` as a recipe YAML path or a model name (e.g., `"efficientnet_b0"`), or a weights path (`.xml`, `.onnx`). +> If a model name matches recipes under multiple tasks, pass `task=` to disambiguate (e.g., `task="DETECTION"`). +> If you want to use an Ultralytics YOLO model, you can pass a YAML file in [Ultralytics format](https://docs.ultralytics.com/datasets/) as `data=`. + +--- + +### Export + +Export a trained model to OpenVINO IR or ONNX format: + +```python +from getitune.engine import create_engine +from getitune.types import ExportFormat, ExportPrecision + +engine = create_engine( + model="efficientnet_b0", + data="/path/to/dataset", + work_dir="./my_workspace", +) +engine.train(max_epochs=50) + +# Export to FP32 OpenVINO IR (default) +ov_ir_path = engine.export() + +# Export to FP32 ONNX +onnx_path = engine.export(export_format=ExportFormat.ONNX) + +# Export to FP16 ONNX. Same for OpenVINO IR. +onnx_path = engine.export(export_format=ExportFormat.ONNX, precision=ExportPrecision.FP16) ``` --- -### Inference +### Validation and Inference -Getitune provides inference via PyTorch and OpenVINO backends: +Getitune provides inference via PyTorch and OpenVINO backends (utilizing [ModelAPI](https://github.com/open-edge-platform/model_api)): ```python from getitune.engine import create_engine -# PyTorch inference -engine = create_engine(data="/path/to/dataset", model="path/to/recipe.yaml") -predictions = engine.predict() +# PyTorch inference with model name +engine = create_engine( + model="efficientnet_b0", + data="/path/to/dataset", +) +test_metrics = engine.test() # test on test subset +predictions = engine.predict() # predict on test subset +``` -# OpenVINO inference and optimization -ov_engine = create_engine(data="/path/to/dataset", model="path/to/exported_model.xml") -ov_engine.test() +```python + +# OpenVINO inference (from exported model) +ov_engine = create_engine( + model="/path/to/exported_model.xml", + data="/path/to/dataset", +) +ov_engine.test() # test on test subset +ov_engine.predict() # predict on test subset +``` + +```python + +# ONNX inference (from exported model) +ov_engine = create_engine( + model="/path/to/exported_model.onnx", + data="/path/to/dataset", +) +ov_engine.test() # test on test subset +ov_engine.predict() # predict on test subset +``` + +--- + +### Optimization + +Apply post-training quantization to reduce model size and accelerate inference: + +```python +from getitune.engine import create_engine + +# Load an exported OpenVINO model and optimize it +ov_engine = create_engine( + model="/path/to/exported_model.xml", + data="/path/to/dataset", +) ov_engine.optimize() # post-training quantization via NNCF +test_metrics = ov_engine.test() # test on test subset with optimized model +predictions = ov_engine.predict() # predict on test subset with optimized model ``` -> **Note:** For advanced inference options including OpenVINO optimization, check the [API Quick-Guide](https://open-edge-platform.github.io/training_extensions/latest/guide/get_started/api_tutorial.html). +> **Note:** The recommended calibration set size for optimization is around 300 images. Calibration images are automatically taken from the training subset of your dataset. +> +> After `.optimize()` the model in the Engine is replaced with an INT8 quantized version. To re-validate or run inference with the original FP32/FP16 model, pass the model XML path directly to `.test()` / `.predict()` or create the Engine again from the original `.xml`. --- @@ -189,16 +323,86 @@ Zip archives are also accepted, Datumaro extracts them on import. ```python # Works the same regardless of format, just point to the dataset root engine = create_engine( - data="/path/to/coco_or_yolo_or_voc_dataset", + data="/path/to/dataset_root", model="src/getitune/recipe/detection/yolox_s.yaml", ) engine.train() ``` +> **Note:** If you are working with Ultralytics YOLO models, you can pass a [YOLO Ultralytics](https://docs.ultralytics.com/datasets/) `data.yaml` file directly to the `data=` argument of `create_engine`. + --- ### Advanced Usage +
+Direct Backend Engine Usage + +The backends engines can also be instantiated directly: + +```python +# -- Lightning Backend -- +from getitune.backend.lightning.engine import LightningEngine +from getitune.models import EfficientNet + +engine = LightningEngine( + model=EfficientNet(label_info=10, model_name="efficientnet_b0"), + data="/path/to/dataset", + work_dir="./my_workspace", + device="auto", +) +engine.train(max_epochs=50) +engine.test() +engine.export() + + +# -- Ultralytics Backend -- +from getitune.backend.ultralytics.engine import UltralyticsEngine +from getitune.backend.ultralytics.models import UltralyticsDetectionModel + +engine = UltralyticsEngine( + model=UltralyticsDetectionModel(model_name="yolo26s"), + data="/path/to/yolo_dataset/data.yaml", + work_dir="./yolo_workspace", + device="auto", +) +engine.train(epochs=50) +engine.test() +engine.export() + + +# -- OpenVINO Backend (inference) -- +from getitune.backend.openvino.engine import OVEngine + +engine = OVEngine( + model="/path/to/exported_model.xml", + data="/path/to/dataset", +) +engine.test() +engine.optimize() +``` + +
+ +
+Common Engine Parameters + +Customize engine creation with the following parameters: + +```python +engine = create_engine( + model="efficientnet_b0", + data="/path/to/data", + work_dir="./my_workspace", # Working directory for checkpoints/logs; defaults to "./getitune-workspace" + device="gpu", # "auto", "cpu", "gpu", "0", "1", "xpu", etc. + checkpoint="/path/to/weights.pt", # Optional pretrained checkpoint for warm-start training + task="MULTI_CLASS_CLS", # Required only if model name matches multiple tasks +) +engine.train(max_epochs=50) +``` + +
+
Override training hyperparameters @@ -222,7 +426,7 @@ from torch.optim import AdamW from getitune.models import EfficientNet model = EfficientNet( - label_info=datamodule.label_info, + label_info=datamodule.label_info, # or simply num_classes, e.g., int value model_name="efficientnet_b0", optimizer=lambda params: AdamW(params, lr=0.001, weight_decay=0.01), ) @@ -265,6 +469,10 @@ data: src/getitune/recipe/_base_/data/classification.yaml overrides: max_epochs: 100 ``` +
+ +
+Override augmentations and datamodule For augmentations, override the data config. Augmentations run on CPU (`augmentations_cpu`) and GPU (`augmentations_gpu`) separately: @@ -303,47 +511,60 @@ train_subset: std: [0.229, 0.224, 0.225] ``` -Then reference your custom data config from your recipe with `data: my_data.yaml`, or build a `DataModule` explicitly (see below). +Then reference your custom data config from your recipe with `data: my_data.yaml`. -
- -
-Build a DataModule explicitly +Augmentations can also be overridden directly via the API by creating a `DataModule` instance and passing it to the engine: ```python from getitune.data.module import DataModule -from getitune.types.task import TaskType +from getitune.config.data import SubsetConfig +from getitune.models import EfficientNet +import kornia.augmentation as K +from torchvision.transforms import v2 datamodule = DataModule( - task=TaskType.DETECTION, - data_root="/path/to/coco_dataset", - input_size=(640, 640), + task="MULTI_CLASS_CLS", + data_root="tests/assets/classification_cifar10", + train_subset=SubsetConfig( + augmentations_cpu=[ + v2.Resize((256, 256)), + ], + augmentations_gpu=[ + K.RandomErasing(p=0.5), + K.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1), + ], + batch_size=32, + num_workers=8, + ), + val_subset=SubsetConfig( + subset_name="val", + augmentations_cpu=[ + v2.Resize((256, 256)), + ], + batch_size=32, + num_workers=8, + ), + test_subset=SubsetConfig( + subset_name="test", + augmentations_cpu=[ + v2.Resize((256, 256)), + ], + batch_size=32, + num_workers=8, + ), ) -engine = create_engine( - data=datamodule, - model="src/getitune/recipe/detection/yolox_s.yaml", -) -engine.train() -``` - -
- -
-Instantiate a model class directly - -```python -from getitune.models import ATSS - -model = ATSS(label_info=datamodule.label_info) +model = EfficientNet(label_info=datamodule.label_info, model_name="efficientnet_b0") engine = create_engine(data=datamodule, model=model) engine.train() ``` +> **Note:** GPU augmentations (`augmentations_gpu`) are supported only for the Lightning backend and will be ignored for Ultralytics. For YOLO models, all augmentations should be placed on CPU via `torchvision.transforms.v2`. + Available model classes: -- **Detection:** `ATSS`, `SSD`, `YOLOX`, `RTDETR`, `DFine`, `DEIMDFine`, `DEIMV2` -- **Instance segmentation:** `MaskRCNN`, `MaskRCNNTV`, `RTMDetInst` +- **Detection:** `ATSS`, `SSD`, `YOLOX`, `RTDETR`, `DFine`, `DEIMDFine`, `DEIMV2`, `RFDETR`, `UltralyticsDetectionModel` +- **Instance segmentation:** `MaskRCNN`, `MaskRCNNTV`, `RTMDetInst`, `RFDETRSeg`, `UltralyticsInstSegModel` - **Semantic segmentation:** `DinoV2Seg`, `LiteHRNet`, `SegNext` - **Classification:** `EfficientNet`, `MobileNetV3`, `VisionTransformer`, `TimmModel`, `TVModel` - **Keypoint:** `RTMPose` @@ -354,7 +575,9 @@ Available model classes: ## License -The Geti™ Library (`getitune`) is licensed under [Apache License Version 2.0](https://github.com/open-edge-platform/training_extensions/blob/develop/LICENSE). +The core Geti™ Library (`getitune`) is licensed under [Apache License Version 2.0](https://github.com/open-edge-platform/training_extensions/blob/develop/LICENSE). By contributing to the project, you agree to the license and copyright terms therein and release your contribution under these terms. +Ultralytics YOLO models are distributed under the AGPL-3.0 license, an OSI approved license ideal for open-source research, academic, and personal projects. For commercial use, enhanced support, and tailored licensing terms, please explore flexible Ultralytics licensing options at https://www.ultralytics.com/license. + --- diff --git a/library/docs/source/guide/get_started/api_tutorial.rst b/library/docs/source/guide/get_started/api_tutorial.rst index d00b98801c9..59ebdca83eb 100644 --- a/library/docs/source/guide/get_started/api_tutorial.rst +++ b/library/docs/source/guide/get_started/api_tutorial.rst @@ -74,7 +74,7 @@ If you want to use other models offered by Geti Library, you can get a list of a .. code-block:: python - from getitune.backend.lightning.cli.utils import list_models + from getitune.utils import list_models model_lists = list_models(task="DETECTION") print(model_lists) @@ -113,7 +113,7 @@ If you want to use other models offered by Geti Library, you can get a list of a .. code-block:: python - from getitune.backend.lightning.cli.utils import list_models + from getitune.utils import list_models model_lists = list_models(task="DETECTION", print_table=True) @@ -132,7 +132,7 @@ If you want to use other models offered by Geti Library, you can get a list of a .. code-block:: python - from getitune.backend.lightning.cli.utils import list_models + from getitune.utils import list_models model_lists = list_models(task="DETECTION", pattern="tile") print(model_lists) diff --git a/library/docs/source/guide/tutorials/base/how_to_train/classification.rst b/library/docs/source/guide/tutorials/base/how_to_train/classification.rst index 884cbeb3390..c8f5402be8f 100644 --- a/library/docs/source/guide/tutorials/base/how_to_train/classification.rst +++ b/library/docs/source/guide/tutorials/base/how_to_train/classification.rst @@ -125,7 +125,7 @@ The list of supported recipes for classification is available with the command l .. code-block:: python - from getitune.backend.lightning.cli.utils import list_models + from getitune.utils import list_models model_lists = list_models(task="MULTI_CLASS_CLS", pattern="*efficient") print(model_lists) diff --git a/library/docs/source/guide/tutorials/base/how_to_train/detection.rst b/library/docs/source/guide/tutorials/base/how_to_train/detection.rst index 4d45079f914..9bb74856564 100644 --- a/library/docs/source/guide/tutorials/base/how_to_train/detection.rst +++ b/library/docs/source/guide/tutorials/base/how_to_train/detection.rst @@ -140,7 +140,7 @@ The list of supported recipes for object detection is available with the command .. code-block:: python - from getitune.backend.lightning.cli.utils import list_models + from getitune.utils import list_models model_lists = list_models(task="DETECTION", pattern="atss") print(model_lists) diff --git a/library/docs/source/guide/tutorials/base/how_to_train/instance_segmentation.rst b/library/docs/source/guide/tutorials/base/how_to_train/instance_segmentation.rst index 0cdc68ad966..bfb852edb69 100644 --- a/library/docs/source/guide/tutorials/base/how_to_train/instance_segmentation.rst +++ b/library/docs/source/guide/tutorials/base/how_to_train/instance_segmentation.rst @@ -143,7 +143,7 @@ The list of supported recipes for instance segmentation is available with the co .. code-block:: python - from getitune.backend.lightning.cli.utils import list_models + from getitune.utils import list_models model_lists = list_models(task="INSTANCE_SEGMENTATION") print(model_lists) diff --git a/library/docs/source/guide/tutorials/base/how_to_train/semantic_segmentation.rst b/library/docs/source/guide/tutorials/base/how_to_train/semantic_segmentation.rst index e5c3f482aaf..fa667fb19c6 100644 --- a/library/docs/source/guide/tutorials/base/how_to_train/semantic_segmentation.rst +++ b/library/docs/source/guide/tutorials/base/how_to_train/semantic_segmentation.rst @@ -79,7 +79,7 @@ The list of supported recipes for semantic segmentation is available with the co .. code-block:: python - from getitune.backend.lightning.cli.utils import list_models + from getitune.utils import list_models model_lists = list_models(task="SEMANTIC_SEGMENTATION") print(model_lists) diff --git a/library/src/getitune/backend/lightning/cli/__init__.py b/library/src/getitune/backend/lightning/cli/__init__.py deleted file mode 100644 index dd8c80ae3b1..00000000000 --- a/library/src/getitune/backend/lightning/cli/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -"""CLI for Native backend. - -Note: This is temporary as the new CLI should cover all the utilities mentioned here. -""" - -# Copyright (C) 2025 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -from .utils import get_getitune_root_path, list_models - -__all__ = ["get_getitune_root_path", "list_models"] diff --git a/library/src/getitune/backend/lightning/engine.py b/library/src/getitune/backend/lightning/engine.py index cc624f9db02..d808868355d 100644 --- a/library/src/getitune/backend/lightning/engine.py +++ b/library/src/getitune/backend/lightning/engine.py @@ -709,83 +709,82 @@ def dummy_infer(model: LightningModel, batch_size: int = 1) -> float: def from_config( cls, config_path: PathLike, - data_root: PathLike | None = None, + data: DataModule | PathLike | None = None, work_dir: PathLike | None = None, + device: str | None = None, + checkpoint: str | None = None, + task: str | None = None, **kwargs, ) -> LightningEngine: """Builds the engine from a configuration file. Args: config_path (PathLike): The configuration file path. - data_root (PathLike | None): Root directory for the data. - Defaults to None. If data_root is None, use the data_root from the configuration file. + data (DataModule | PathLike | None): Either a pre-built + :class:`~getitune.data.module.DataModule` or a root directory + path for the data. When *None*, the data root is read from + the configuration file. work_dir (PathLike | None, optional): Working directory for the engine. Defaults to None. If work_dir is None, use the work_dir from the configuration file. - kwargs: Arguments that can override the engine's arguments. + device (str | None, optional): Device to use (e.g., ``"auto"``, ``"xpu"``, ``"cpu"``, ``"gpu"``). + Defaults to None. + checkpoint (str | None, optional): Path to a checkpoint for pretrained or warm-start weights. + Defaults to None. + task (str | None, optional): Task type for disambiguation. Defaults to None. + kwargs: Backend-specific keyword arguments forwarded to the engine constructor. Returns: - Engine: An instance of the Engine class. + LightningEngine: An instance of the LightningEngine class. Example: >>> engine = LightningEngine.from_config( - ... config="config.yaml", + ... config_path="config.yaml", + ... data="/path/to/dataset", ... ) ... engine.train() """ from getitune.cli.utils.jsonargparse import get_instantiated_classes - # For the Engine argument, prepend 'engine.' for CLI parser - filter_kwargs = ["device", "checkpoint", "task"] - for key in filter_kwargs: - if key in kwargs: - kwargs[f"engine.{key}"] = kwargs.pop(key) + # Separate a pre-built DataModule from a plain data-root path so that + # the CLI parser only receives a file-system path (or None). + provided_datamodule: DataModule | None = None + if isinstance(data, DataModule): + provided_datamodule = data + # Give the CLI parser the DataModule's own data_root so it can + # still instantiate the model with the correct label_info from + # the dataset. The config-parsed DataModule is discarded below. + data_root: PathLike | None = getattr(data, "data_root", None) + else: + data_root = data # PathLike | None + + # Route common args explicitly under the process's "engine" namespace. + process_kwargs: dict[str, object] = dict(kwargs) + if device is not None: + process_kwargs["engine.device"] = device + if checkpoint is not None: + process_kwargs["engine.checkpoint"] = checkpoint + if task is not None: + process_kwargs["engine.task"] = task instantiated_config, train_kwargs = get_instantiated_classes( config=config_path, data_root=data_root, work_dir=work_dir, - **kwargs, + **process_kwargs, ) - engine_kwargs = {**instantiated_config.get("engine", {}), **train_kwargs} - - # Remove any input that is not currently available in Engine and print a warning message. - set_valid_args = TrainerArgumentsCache.get_trainer_constructor_args().union( - set(inspect.signature(LightningEngine.__init__).parameters.keys()), + valid_keys = TrainerArgumentsCache.get_trainer_constructor_args() | set( + inspect.signature(LightningEngine.__init__).parameters, ) - # Keys that are legitimate train/test/export method kwargs. They come - # from OTXCLI.prepare_subcommand_kwargs("train") being merged into the - # engine kwargs, and should be silently dropped here -- warning about - # them is noise, because they are consumed at method-call time by the - # caller, not by the engine ctor. - known_method_kwargs = { - "resume", - "adaptive_bs", - "max_epochs", - "run_hpo", - "hpo_config", - "checkpoint", - "export_demo_package", - "export_format", - "explain", - "dump_options", - "precision", - } - removed_args = [] - for engine_key in list(engine_kwargs.keys()): - if engine_key not in set_valid_args: - engine_kwargs.pop(engine_key) - if engine_key not in known_method_kwargs: - removed_args.append(engine_key) - if removed_args: - msg = ( - f"Warning: {removed_args} -> not available in Engine constructor. " - "It will be ignored. Use what need in the right places." - ) - warn(msg, stacklevel=1) - - if (datamodule := instantiated_config.get("data")) is None: + merged = {**instantiated_config.get("engine", {}), **train_kwargs} + engine_kwargs = {k: v for k, v in merged.items() if k in valid_keys} + + # Use the caller-supplied DataModule when provided; otherwise build + # one from the parsed configuration. + if provided_datamodule is not None: + datamodule = provided_datamodule + elif (datamodule := instantiated_config.get("data")) is None: msg = "Cannot instantiate datamodule from config." raise ValueError(msg) - if not isinstance(datamodule, DataModule): + elif not isinstance(datamodule, DataModule): raise TypeError(datamodule) if (model := instantiated_config.get("model")) is None: @@ -814,7 +813,7 @@ def from_model_name( cls, model_name: str, task: TaskType, - data_root: PathLike | None = None, + data: PathLike | None = None, work_dir: PathLike | None = None, **kwargs, ) -> LightningEngine: @@ -823,8 +822,8 @@ def from_model_name( Args: model_name (str): The model name. task (TaskType): The type of getitune task. - data_root (PathLike | None): Root directory for the data. - Defaults to None. If data_root is None, use the data_root from the configuration file. + data (PathLike | None): Root directory for the data. + Defaults to None. If data is None, use the data_root from the configuration file. work_dir (PathLike | None, optional): Working directory for the engine. Defaults to None. If work_dir is None, use the work_dir from the configuration file. kwargs: Arguments that can override the engine's arguments. @@ -836,7 +835,7 @@ def from_model_name( >>> engine = LightningEngine.from_model_name( ... model_name="atss_mobilenetv2", ... task="DETECTION", - ... data_root=, + ... data=, ... ) ... engine.train() @@ -848,7 +847,7 @@ def from_model_name( >>> engine = LightningEngine( ... model_name="atss_mobilenetv2", ... task="DETECTION", - ... data_root=, + ... data=, ... **overriding, ... ) """ @@ -866,7 +865,7 @@ def from_model_name( return cls.from_config( config_path=config, - data_root=data_root, + data=data, work_dir=work_dir, **kwargs, ) diff --git a/library/src/getitune/backend/openvino/engine.py b/library/src/getitune/backend/openvino/engine.py index 9cd18331a96..205f856e066 100644 --- a/library/src/getitune/backend/openvino/engine.py +++ b/library/src/getitune/backend/openvino/engine.py @@ -483,6 +483,41 @@ def is_supported(model: MODEL, data: DATA) -> bool: return check_model and check_data + @classmethod + def from_config( + cls, + config_path: PathLike, + data: DataModule | PathLike | None = None, + work_dir: PathLike | None = None, + device: str | None = None, + checkpoint: str | None = None, + task: str | None = None, + **kwargs, + ) -> OVEngine: + """OVEngine does not support construction from a recipe config. + + OpenVINO models are selected by passing a ``.xml`` or ``.onnx`` + weights path directly to :func:`~getitune.engine.create_engine` or + as the *model* argument to :class:`OVEngine`. + + Args: + config_path: Unused — included for API compatibility. + data: Unused — included for API compatibility. + work_dir: Unused — included for API compatibility. + device: Unused — included for API compatibility. + checkpoint: Unused — included for API compatibility. + task: Unused — included for API compatibility. + **kwargs: Unused — included for API compatibility. + + Raises: + NotImplementedError: Always raised. + """ + msg = ( + f"OVEngine does not support construction from a recipe config '{config_path}'. " + "Pass a .xml or .onnx model path directly to create_engine() instead." + ) + raise NotImplementedError(msg) + def _update_checkpoint(self, checkpoint: PathLike | None) -> OVModel: """Update the OVModel with the given checkpoint path. diff --git a/library/src/getitune/backend/ultralytics/engine.py b/library/src/getitune/backend/ultralytics/engine.py index cb5da9b926c..fc8043c1498 100644 --- a/library/src/getitune/backend/ultralytics/engine.py +++ b/library/src/getitune/backend/ultralytics/engine.py @@ -13,10 +13,12 @@ from typing import TYPE_CHECKING, Any, ClassVar, Protocol, Sequence import torch +import yaml from torchvision import tv_tensors from ultralytics.utils.metrics import ClassifyMetrics, DetMetrics, SegmentMetrics from getitune.backend.ultralytics.data.geometry import scale_boxes_to_letterbox, scale_masks_to_letterbox +from getitune.backend.ultralytics.tools.configurator import Configurator from getitune.data.entity.base import ImageInfo from getitune.data.entity.sample import Prediction, SampleBatch from getitune.data.module import DataModule @@ -357,6 +359,70 @@ def is_supported(model: MODEL, data: DATA) -> bool: """Return ``True`` when *model* is an :class:`UltralyticsModel`.""" return bool(isinstance(model, UltralyticsModel) and isinstance(data, (DataModule, str, os.PathLike))) + @classmethod + def from_config( + cls, + config_path: PathLike, + data: DataModule | PathLike | None = None, + work_dir: PathLike = "./getitune-workspace", + device: str | None = None, + checkpoint: str | None = None, + task: str | None = None, # noqa: ARG003 (API compatibility with Engine.from_config) + **kwargs, + ) -> UltralyticsEngine: + """Build an engine from an Ultralytics recipe configuration file. + + Args: + config_path: Path to the Ultralytics recipe YAML file + (must contain ``backend: ultralytics``). + data: A pre-built :class:`~getitune.data.module.DataModule` or a + filesystem data-root path. Required. + work_dir: Working directory for checkpoints and exports. + Defaults to ``"./getitune-workspace"``. + device: Device to use (e.g., ``"auto"``, ``"xpu"``, ``"cpu"``, ``"gpu"``). + Defaults to None. + checkpoint: Optional path to a checkpoint for pretrained or warm-start weights. + Defaults to None. + task: Task type for disambiguation when a model name matches recipes + under multiple tasks. Not forwarded to the engine. Defaults to None. + **kwargs: Backend-specific keyword arguments forwarded to + :class:`UltralyticsEngine` (e.g. ``train_args``, ``export_args``). + + Returns: + A fully configured :class:`UltralyticsEngine`. + + Raises: + ValueError: If *data* is ``None``. + """ + if data is None: + msg = "data (a DataModule or data-root path) is required for UltralyticsEngine.from_config." + raise ValueError(msg) + + # Read the task from the raw YAML (top-level field, no interpolation needed). + with Path(str(config_path)).open() as fh: + raw = yaml.safe_load(fh) + recipe_task: str | None = (raw or {}).get("task") + + configurator = Configurator(data=data, model=Path(str(config_path)), task=recipe_task) + + # Build (or reuse) the DataModule, then derive label_info for the model. + datamodule = configurator.build_datamodule() + label_info = datamodule.label_info + model = configurator.create_model(label_info) + + engine_kwargs: dict[str, object] = {**kwargs} + if device is not None: + engine_kwargs["device"] = device + if checkpoint is not None: + engine_kwargs["checkpoint"] = checkpoint + + return configurator.create_engine( + model=model, + data=datamodule, + work_dir=work_dir, + **engine_kwargs, + ) + @property def work_dir(self) -> PathLike: """Working directory.""" diff --git a/library/src/getitune/backend/ultralytics/tools/configurator.py b/library/src/getitune/backend/ultralytics/tools/configurator.py index ade7a616ed7..386c7e8fd56 100644 --- a/library/src/getitune/backend/ultralytics/tools/configurator.py +++ b/library/src/getitune/backend/ultralytics/tools/configurator.py @@ -13,7 +13,6 @@ from jsonargparse import ArgumentParser, Namespace -from getitune.backend.ultralytics.engine import UltralyticsEngine from getitune.backend.ultralytics.models.base import UltralyticsModel from getitune.backend.ultralytics.tools.utils import ( RECIPE_DIR, @@ -32,6 +31,7 @@ if TYPE_CHECKING: from collections.abc import Mapping + from getitune.backend.ultralytics.engine import UltralyticsEngine from getitune.types import PathLike @@ -567,6 +567,8 @@ def create_engine( **engine_kwargs, ) -> UltralyticsEngine: """Instantiate the configured Ultralytics engine.""" + from getitune.backend.ultralytics.engine import UltralyticsEngine as _UltralyticsEngine + if data is None: if self._datamodule is not None: data = self._datamodule @@ -576,7 +578,7 @@ def create_engine( msg = "No data available. Pass data= to create_engine() or call build_datamodule() first." raise ValueError(msg) - return UltralyticsEngine( + return _UltralyticsEngine( model=model, data=data, work_dir=work_dir, diff --git a/library/src/getitune/benchmark/experiment.py b/library/src/getitune/benchmark/experiment.py index 42bb8f965b4..560d69d058a 100644 --- a/library/src/getitune/benchmark/experiment.py +++ b/library/src/getitune/benchmark/experiment.py @@ -322,7 +322,7 @@ def train(self) -> PhaseResult: overrides = resolve_overrides(self.scenario_overrides) engine = LightningEngine.from_config( config_path=self.recipe_path, - data_root=self.data_path, + data=self.data_path, work_dir=self.work_dir / "train", device=self.accelerator, **overrides, @@ -356,7 +356,7 @@ def test_torch(self) -> PhaseResult: overrides = resolve_overrides(self.scenario_overrides) engine = LightningEngine.from_config( config_path=self.recipe_path, - data_root=self.data_path, + data=self.data_path, work_dir=self.work_dir / "test" / "torch", device=self.accelerator, **overrides, @@ -390,7 +390,7 @@ def export(self) -> PhaseResult: overrides = resolve_overrides(self.scenario_overrides) engine = LightningEngine.from_config( config_path=self.recipe_path, - data_root=self.data_path, + data=self.data_path, work_dir=self.work_dir / "export", device=self.accelerator, **overrides, diff --git a/library/src/getitune/benchmark/manifest.py b/library/src/getitune/benchmark/manifest.py index 7f164eb0427..ebf5197a9da 100644 --- a/library/src/getitune/benchmark/manifest.py +++ b/library/src/getitune/benchmark/manifest.py @@ -12,7 +12,7 @@ import yaml -from getitune.backend.lightning.cli.utils import RECIPE_PATH +from getitune.utils import RECIPE_PATH logger = logging.getLogger(__name__) diff --git a/library/src/getitune/cli/cli.py b/library/src/getitune/cli/cli.py index aa37afe02d3..6886a353d5d 100644 --- a/library/src/getitune/cli/cli.py +++ b/library/src/getitune/cli/cli.py @@ -539,7 +539,7 @@ def run(self) -> None: """ self.console.print(f"[blue]{LIBRARY_LOGO}[/blue] ver.{__version__}", justify="center") if self.subcommand == "find": - from getitune.backend.lightning.cli.utils import list_models + from getitune.utils import list_models list_models(print_table=True, **self.config[self.subcommand]) elif self.subcommand in self.engine_subcommands(): diff --git a/library/src/getitune/engine/__init__.py b/library/src/getitune/engine/__init__.py index 960cda081ab..9547e782122 100644 --- a/library/src/getitune/engine/__init__.py +++ b/library/src/getitune/engine/__init__.py @@ -5,56 +5,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - from .engine import Engine - -if TYPE_CHECKING: - from getitune.types.types import DATA, MODEL - - -def create_engine(model: MODEL, data: DATA, **kwargs) -> Engine: - """Create an engine. - - Args: - model: The model to use - data: The data/datamodule to use - kwargs: Additional keyword arguments for engine initialization - - Returns: - An instance of an Engine subclass that supports the model and data - - Raises: - ValueError: If no compatible engine is found - """ - from getitune.backend.lightning.engine import LightningEngine - from getitune.backend.openvino.engine import OVEngine - - supported_engines: list[type[Engine]] = [LightningEngine, OVEngine] - - # Ultralytics backend (optional) - try: - from getitune.backend.ultralytics.engine import UltralyticsEngine - - supported_engines.append(UltralyticsEngine) - except ImportError: - pass - - # Dynamically discover all custom subclasses of Engine - for child_engines in Engine.__subclasses__(): - if child_engines not in supported_engines: - supported_engines.append(child_engines) - - for engine_cls in supported_engines: - if not hasattr(engine_cls, "is_supported"): - msg = f"Engine {engine_cls.__name__} does not implement is_supported method." - raise ValueError(msg) - if engine_cls.is_supported(model, data): - # Type ignore since mypy can't verify the constructor signature of subclasses - return engine_cls(model=model, data=data, **kwargs) # type: ignore[call-arg] - - msg = f"No engine found for model {model} and data {data}" - raise ValueError(msg) - +from .utils import create_engine __all__ = ["Engine", "create_engine"] diff --git a/library/src/getitune/engine/engine.py b/library/src/getitune/engine/engine.py index 11078123493..b1e2824b697 100644 --- a/library/src/getitune/engine/engine.py +++ b/library/src/getitune/engine/engine.py @@ -44,6 +44,40 @@ def is_supported(model: MODEL, data: DATA) -> bool: """Check if the engine is supported for the given model and data.""" raise NotImplementedError + @classmethod + def from_config( + cls, + config_path: PathLike, + data: DATA, + work_dir: PathLike | None = None, + device: str | None = None, + checkpoint: str | None = None, + task: str | None = None, + **kwargs, + ) -> Engine: + """Build an engine from a recipe configuration file. + + Args: + config_path: Path to the recipe YAML file. + data: DataModule or root directory for the data. + work_dir: Working directory for the engine. Defaults to None. + device: Device to use (e.g., ``"auto"``, ``"xpu"``, ``"cpu"``, ``"gpu"``). Defaults to None. + checkpoint: Optional path to a checkpoint for pretrained or warm-start weights. Defaults to None. + task: Task type for disambiguation when a model name matches recipes under multiple tasks. Defaults to None. + **kwargs: Backend-specific keyword arguments forwarded to the engine constructor. + + Returns: + An instance of the Engine subclass. + + Raises: + NotImplementedError: If the subclass does not implement this method. + """ + msg = ( + f"{cls.__name__} does not support construction from a recipe config. " + "Pass a model instance directly to create_engine() instead." + ) + raise NotImplementedError(msg) + @property @abstractmethod def work_dir(self) -> PathLike: diff --git a/library/src/getitune/engine/utils/__init__.py b/library/src/getitune/engine/utils/__init__.py index e5b4f726bc7..92bf5272175 100644 --- a/library/src/getitune/engine/utils/__init__.py +++ b/library/src/getitune/engine/utils/__init__.py @@ -2,3 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 """Util API for getitune Engine.""" + +from .create import create_engine + +__all__ = ["create_engine"] diff --git a/library/src/getitune/engine/utils/create.py b/library/src/getitune/engine/utils/create.py new file mode 100644 index 00000000000..bbff762c651 --- /dev/null +++ b/library/src/getitune/engine/utils/create.py @@ -0,0 +1,213 @@ +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +"""Utilities for creating engines from configurations and model names.""" + +from __future__ import annotations + +import os +from pathlib import Path +from typing import TYPE_CHECKING + +import yaml + +from getitune.engine.engine import Engine + +if TYPE_CHECKING: + from getitune.types.task import TaskType + from getitune.types.types import DATA, MODEL + +# Recipe directory — parallel to the package root. +_RECIPE_PATH: Path = Path(__file__).parent.parent.parent / "recipe" +_RECIPE_SUFFIXES: frozenset[str] = frozenset({".yaml", ".yml"}) +_WEIGHT_SUFFIXES: frozenset[str] = frozenset({".xml", ".onnx"}) + + +def _resolve_recipe(model: MODEL, task: TaskType | str | None) -> Path: + """Resolve a model name or YAML path to an absolute recipe path. + + Args: + model: A YAML config path or a bare model name (e.g. ``"yolox_s"``). + task: Optional task used to disambiguate when a model name matches + recipes under multiple tasks. + + Returns: + Absolute path to the resolved recipe file. + + Raises: + FileNotFoundError: If the recipe file or model name cannot be resolved. + ValueError: If *model* is a name that matches multiple tasks and *task* + is not provided. + """ + path = Path(str(model)) + + # Explicit YAML path — use directly. + if path.suffix in _RECIPE_SUFFIXES: + if not path.exists(): + msg = f"Recipe file not found: {path}" + raise FileNotFoundError(msg) + return path.resolve() + + # Bare model name — search the recipe tree. + name = path.stem if path.suffix else path.name + matches = sorted(_RECIPE_PATH.glob(f"**/{name}.yaml")) + + if not matches: + msg = ( + f"No recipe found for model '{name}' under {_RECIPE_PATH}. Check the model name or pass a full recipe path." + ) + raise FileNotFoundError(msg) + + if len(matches) == 1: + return matches[0] + + # Multiple matches — narrow by task if provided. + if task is not None: + task_str = task.value.lower() if hasattr(task, "value") else str(task).lower() + task_matches = [m for m in matches if task_str in m.parts] + if len(task_matches) == 1: + return task_matches[0] + if task_matches: + matches = task_matches # still ambiguous — fall through to error + + candidates = [str(m.relative_to(_RECIPE_PATH)) for m in matches] + msg = f"Model name '{name}' matches multiple recipes: {candidates}. Pass task= to disambiguate." + raise ValueError(msg) + + +def _read_backend(recipe_path: Path) -> str: + """Read the ``backend`` field from a recipe YAML. + + Falls back to ``'lightning'`` when the field is absent — the convention + for all Lightning-backed recipes. + + Args: + recipe_path: Absolute path to the recipe YAML file. + + Returns: + Backend name string (e.g. ``'lightning'``, ``'ultralytics'``). + """ + with recipe_path.open() as fh: + raw = yaml.safe_load(fh) + return (raw or {}).get("backend") or "lightning" + + +def create_engine( + model: MODEL, + data: DATA, + work_dir: str | None = None, + device: str | None = None, + checkpoint: str | None = None, + task: TaskType | str | None = None, + **kwargs, +) -> Engine: + """Create an engine. + + Accepts three forms for *model*: + + * **Model instance** (``LightningModel``, ``UltralyticsModel``, ``OVModel``) + or a **weights path** (``.xml``, ``.onnx``) — for OpenVINO and ONNX models + + * **Recipe path** — a ``.yaml`` / ``.yml`` file. The ``backend`` field in + the recipe selects the engine; defaults to ``lightning`` when absent. + + * **Model name** — a bare string (e.g. ``"yolox_s"``) that is resolved to a + recipe by searching the ``recipe/`` tree. Pass ``task=`` when the name + matches recipes under multiple tasks. + + Args: + model: The model to use — instance, weights path, recipe path, or model name. + data: DataModule or filesystem data-root path. + work_dir: Working directory for checkpoints, exports, and logs. + Defaults to ``"./getitune-workspace"``. + device: Device to use (e.g., ``"auto"``, ``"xpu"``, ``"cpu"``, ``"gpu"``). + Defaults to backend's default (auto). + checkpoint: Optional path to a checkpoint to load model weights from + before training (for pretrained or warm-start weights). + task: Task type for disambiguation when a model name matches recipes + under multiple tasks. Optional. + **kwargs: Additional backend-specific keyword arguments + (e.g., ``train_args``, ``export_args`` for Ultralytics; + Trainer parameters for Lightning). + + Returns: + An :class:`Engine` subclass instance. + + Raises: + FileNotFoundError: If a recipe / model name cannot be resolved or + if a YAML path does not exist on disk. + ValueError: If a model name is ambiguous, the backend is unknown, + or no engine supports the given model/data pair. + """ + from getitune.backend.lightning.engine import LightningEngine + from getitune.backend.openvino.engine import OVEngine + + backend_to_engine: dict[str, type[Engine]] = { + "lightning": LightningEngine, + "openvino": OVEngine, + } + try: + from getitune.backend.ultralytics.engine import UltralyticsEngine + + backend_to_engine["ultralytics"] = UltralyticsEngine + except ImportError: + pass + + # All known engine classes for the instance/path dispatch path, + # including any dynamically registered custom subclasses. + supported_engines: list[type[Engine]] = list(backend_to_engine.values()) + for child_engine in Engine.__subclasses__(): + if child_engine not in supported_engines: + supported_engines.append(child_engine) + + # Build kwargs dict with common arguments (skip None values so defaults apply). + common_kwargs = {} + if work_dir is not None: + common_kwargs["work_dir"] = work_dir + if device is not None: + common_kwargs["device"] = device + if checkpoint is not None: + common_kwargs["checkpoint"] = checkpoint + if task is not None: + common_kwargs["task"] = task + # Merge with user-supplied kwargs (user kwargs take precedence). + common_kwargs.update(kwargs) + + # Classify the model argument. + is_recipe_input = False + if isinstance(model, (str, os.PathLike)): + path = Path(str(model)) + if path.suffix in _RECIPE_SUFFIXES: + # Explicit YAML config path — must exist on disk. + if not path.exists(): + msg = f"Recipe file not found: {path}" + raise FileNotFoundError(msg) + is_recipe_input = True + elif path.suffix not in _WEIGHT_SUFFIXES and not path.exists(): + # No weight suffix and no file on disk → treat as a model name. + is_recipe_input = True + + # Recipe / model-name route. + if is_recipe_input: + recipe_path = _resolve_recipe(model, task) + backend = _read_backend(recipe_path) + + engine_cls = backend_to_engine.get(backend) + if engine_cls is None: + msg = ( + f"Unknown backend '{backend}' declared in '{recipe_path}'. Known backends: {sorted(backend_to_engine)}" + ) + raise ValueError(msg) + + return engine_cls.from_config(recipe_path, data, **common_kwargs) + + # Model instance or weights path. + for engine_cls in supported_engines: + if not hasattr(engine_cls, "is_supported"): + msg = f"Engine {engine_cls.__name__} does not implement is_supported." + raise ValueError(msg) + if engine_cls.is_supported(model, data): + return engine_cls(model=model, data=data, **common_kwargs) + + msg = f"No engine found for model {model!r} and data {data!r}" + raise ValueError(msg) diff --git a/library/src/getitune/models/__init__.py b/library/src/getitune/models/__init__.py index 50e945ef945..ef58ddc30d5 100644 --- a/library/src/getitune/models/__init__.py +++ b/library/src/getitune/models/__init__.py @@ -35,6 +35,15 @@ OVSegmentationModel, ) +try: + from getitune.backend.ultralytics.models import ( + UltralyticsDetectionModel, + UltralyticsInstSegModel, + ) +except ImportError: + UltralyticsDetectionModel = None # type: ignore[assignment] + UltralyticsInstSegModel = None # type: ignore[assignment] + __all__ = [ # detection "ATSS", @@ -69,3 +78,11 @@ "TimmModel", "VisionTransformer", ] + +if UltralyticsDetectionModel is not None: + __all__.extend( + [ + "UltralyticsDetectionModel", + "UltralyticsInstSegModel", + ] + ) diff --git a/library/src/getitune/tools/auto_configurator.py b/library/src/getitune/tools/auto_configurator.py index 48ac938c558..a0e70d0eb93 100644 --- a/library/src/getitune/tools/auto_configurator.py +++ b/library/src/getitune/tools/auto_configurator.py @@ -14,13 +14,13 @@ from jsonargparse import ArgumentParser, Namespace -from getitune.backend.lightning.cli.utils import get_getitune_root_path, list_models from getitune.backend.lightning.models.base import DataInputParams, LightningModel from getitune.config.data import SamplerConfig, SubsetConfig, TileConfig from getitune.data.module import DataModule from getitune.types import PathLike from getitune.types.label import LabelInfoTypes from getitune.types.task import TaskType +from getitune.utils import get_getitune_root_path, list_models from getitune.utils.utils import can_pass_tile_config, get_model_cls_from_config, should_pass_label_info if TYPE_CHECKING: diff --git a/library/src/getitune/utils/__init__.py b/library/src/getitune/utils/__init__.py index 3f20d16de0c..bc428700b91 100644 --- a/library/src/getitune/utils/__init__.py +++ b/library/src/getitune/utils/__init__.py @@ -3,6 +3,13 @@ """Utility files.""" +from .recipes import RECIPE_PATH, get_getitune_root_path, list_models from .signal import append_main_proc_signal_handler, append_signal_handler -__all__ = ["append_main_proc_signal_handler", "append_signal_handler"] +__all__ = [ + "RECIPE_PATH", + "append_main_proc_signal_handler", + "append_signal_handler", + "get_getitune_root_path", + "list_models", +] diff --git a/library/src/getitune/backend/lightning/cli/utils.py b/library/src/getitune/utils/recipes.py similarity index 95% rename from library/src/getitune/backend/lightning/cli/utils.py rename to library/src/getitune/utils/recipes.py index 153362da472..ee704c1f1c8 100644 --- a/library/src/getitune/backend/lightning/cli/utils.py +++ b/library/src/getitune/utils/recipes.py @@ -1,7 +1,7 @@ -# Copyright (C) 2024 Intel Corporation +# Copyright (C) 2026 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -"""getitune APIs for User-friendliness.""" +"""Utilities for discovering and listing model recipes.""" from __future__ import annotations @@ -43,7 +43,7 @@ def list_models( task (TaskType | None, optional): Recipe Filter by Task. pattern (Optional[str], optional): A string pattern to filter the list of available models. Defaults to None. print_table (bool, optional): Output the recipe information as a Rich.Table. - This is primarily used for `getitune find` in the CLI. + This is primarily used for ``getitune find`` in the CLI. return_recipes (bool, optional): If True, return the recipe paths instead of model names. Returns: diff --git a/library/tests/integration/api/test_engine.py b/library/tests/integration/api/test_engine.py index a3555f80279..e8d88e8084d 100644 --- a/library/tests/integration/api/test_engine.py +++ b/library/tests/integration/api/test_engine.py @@ -162,7 +162,7 @@ def test_engine_workflow( # ---- 1. Instantiate engine ------------------------------------------ engine = LightningEngine.from_config( config_path=recipe, - data_root=str(data_root), + data=str(data_root), work_dir=str(work_dir), device=fxt_accelerator, ) diff --git a/library/tests/integration/api/test_xai.py b/library/tests/integration/api/test_xai.py index 903a5dad6ca..1821049cf87 100644 --- a/library/tests/integration/api/test_xai.py +++ b/library/tests/integration/api/test_xai.py @@ -54,7 +54,7 @@ def test_forward_explain( engine = LightningEngine.from_config( config_path=recipe, - data_root=fxt_target_dataset_per_task[task], + data=fxt_target_dataset_per_task[task], device=fxt_accelerator, ) @@ -105,7 +105,7 @@ def test_predict_with_explain( tmp_path = tmp_path / f"xai_{model_name}" engine = LightningEngine.from_config( config_path=recipe, - data_root=fxt_target_dataset_per_task[task], + data=fxt_target_dataset_per_task[task], device=fxt_accelerator, work_dir=tmp_path, ) diff --git a/library/tests/integration/conftest.py b/library/tests/integration/conftest.py index 1dd3d8c040f..b449513620c 100644 --- a/library/tests/integration/conftest.py +++ b/library/tests/integration/conftest.py @@ -9,8 +9,8 @@ import pytest -from getitune.backend.lightning.cli.utils import get_getitune_root_path from getitune.types.task import TaskType +from getitune.utils import get_getitune_root_path RECIPE_PATH = get_getitune_root_path() / "recipe" diff --git a/library/tests/perf_v2/benchmark.py b/library/tests/perf_v2/benchmark.py index 1b804cf3ced..08b11beae70 100644 --- a/library/tests/perf_v2/benchmark.py +++ b/library/tests/perf_v2/benchmark.py @@ -13,10 +13,10 @@ import pandas as pd -from getitune.backend.lightning.cli.utils import RECIPE_PATH from getitune.backend.lightning.engine import LightningEngine from getitune.backend.openvino.engine import OVEngine from getitune.types.task import TaskType +from getitune.utils import RECIPE_PATH from tests.perf_v2 import CRITERIA_COLLECTIONS, DATASET_COLLECTIONS, MODEL_COLLECTIONS, summary from tests.perf_v2.utils import ( Criterion, @@ -326,7 +326,7 @@ def _initialize_engine( return LightningEngine.from_config( config_path=FOLDER_MAPPINGS[TaskType(model_info.task)] / (model_info.name + ".yaml"), - data_root=self.data_root / dataset_info.path, + data=self.data_root / dataset_info.path, work_dir=work_dir, device=self.accelerator, ) diff --git a/library/tests/unit/backend/lightning/test_engine.py b/library/tests/unit/backend/lightning/test_engine.py index 0819746e983..bc083bd7a31 100644 --- a/library/tests/unit/backend/lightning/test_engine.py +++ b/library/tests/unit/backend/lightning/test_engine.py @@ -215,7 +215,7 @@ def test_from_config_with_model_name(self, tmp_path) -> None: engine = LightningEngine.from_model_name( model_name=model_name, - data_root=data_root, + data=data_root, task=task_type, work_dir=tmp_path, **overriding, @@ -229,7 +229,7 @@ def test_from_config_with_model_name(self, tmp_path) -> None: engine = LightningEngine.from_model_name( model_name="wrong_model", task=task_type, - data_root=data_root, + data=data_root, work_dir=tmp_path, **overriding, ) @@ -248,7 +248,7 @@ def test_from_config(self, tmp_path, mocker) -> None: engine = LightningEngine.from_config( config_path=recipe_path, - data_root=data_root, + data=data_root, work_dir=tmp_path, **overriding, ) diff --git a/library/tests/unit/backend/lightning/utils/test_api.py b/library/tests/unit/backend/lightning/utils/test_api.py index 7ff63f049fb..ab67b95ea99 100644 --- a/library/tests/unit/backend/lightning/utils/test_api.py +++ b/library/tests/unit/backend/lightning/utils/test_api.py @@ -3,8 +3,8 @@ import pytest -from getitune.backend.lightning.cli.utils import RECIPE_PATH, list_models from getitune.types.task import TaskType +from getitune.utils import RECIPE_PATH, list_models def test_list_models() -> None: diff --git a/library/tests/unit/backend/lightning/utils/test_imports.py b/library/tests/unit/backend/lightning/utils/test_imports.py index 0f9ddb137aa..53217a29ba2 100644 --- a/library/tests/unit/backend/lightning/utils/test_imports.py +++ b/library/tests/unit/backend/lightning/utils/test_imports.py @@ -7,7 +7,7 @@ import pytest import getitune -from getitune.backend.lightning.cli.utils import get_getitune_root_path +from getitune.utils import get_getitune_root_path def test_get_getitune_root_path(mocker): From 3992496c0bd165ea039f58c07bba3e6dac4ecd47 Mon Sep 17 00:00:00 2001 From: kprokofi Date: Fri, 19 Jun 2026 23:02:55 +0900 Subject: [PATCH 02/15] fix linter --- library/src/getitune/backend/lightning/engine.py | 15 +++++++++------ .../src/getitune/backend/ultralytics/engine.py | 10 ++++------ .../backend/ultralytics/tools/configurator.py | 2 +- library/src/getitune/engine/utils/create.py | 9 +++++---- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/library/src/getitune/backend/lightning/engine.py b/library/src/getitune/backend/lightning/engine.py index d808868355d..e3f6ad10498 100644 --- a/library/src/getitune/backend/lightning/engine.py +++ b/library/src/getitune/backend/lightning/engine.py @@ -780,12 +780,15 @@ def from_config( # Use the caller-supplied DataModule when provided; otherwise build # one from the parsed configuration. if provided_datamodule is not None: - datamodule = provided_datamodule - elif (datamodule := instantiated_config.get("data")) is None: - msg = "Cannot instantiate datamodule from config." - raise ValueError(msg) - elif not isinstance(datamodule, DataModule): - raise TypeError(datamodule) + datamodule: DataModule = provided_datamodule + else: + raw_data: DataModule | Any = instantiated_config.get("data") + if raw_data is None: + msg = "Cannot instantiate datamodule from config." + raise ValueError(msg) + if not isinstance(raw_data, DataModule): + raise TypeError(raw_data) + datamodule = raw_data if (model := instantiated_config.get("model")) is None: msg = "Cannot instantiate model from config." diff --git a/library/src/getitune/backend/ultralytics/engine.py b/library/src/getitune/backend/ultralytics/engine.py index fc8043c1498..e0a58c3938d 100644 --- a/library/src/getitune/backend/ultralytics/engine.py +++ b/library/src/getitune/backend/ultralytics/engine.py @@ -67,7 +67,7 @@ def __init__( self, model: UltralyticsModel, data: DataModule | PathLike, - work_dir: PathLike = "./getitune-workspace", + work_dir: PathLike | None = None, device: str | DeviceType = "auto", checkpoint: PathLike | None = None, train_args: Mapping[str, Any] | None = None, @@ -94,7 +94,7 @@ def __init__( raise TypeError(msg) self._model = model - self._work_dir = Path(work_dir).resolve() + self._work_dir = Path(work_dir or "./getitune-workspace").resolve() self._work_dir.mkdir(parents=True, exist_ok=True) self._device = self._resolve_device(device) self._kwargs = kwargs @@ -364,7 +364,7 @@ def from_config( cls, config_path: PathLike, data: DataModule | PathLike | None = None, - work_dir: PathLike = "./getitune-workspace", + work_dir: PathLike | None = None, device: str | None = None, checkpoint: str | None = None, task: str | None = None, # noqa: ARG003 (API compatibility with Engine.from_config) @@ -410,7 +410,7 @@ def from_config( label_info = datamodule.label_info model = configurator.create_model(label_info) - engine_kwargs: dict[str, object] = {**kwargs} + engine_kwargs: dict[str, Any] = {**kwargs} if device is not None: engine_kwargs["device"] = device if checkpoint is not None: @@ -607,7 +607,6 @@ def _compute_best_confidence_threshold(self) -> float | None: metric = FMeasure(label_info) device = self._device yolo = self._model.yolo - yolo.model.to(device).eval() # pyrefly: ignore[missing-attribute] metric = metric.to(device) self._datamodule.setup(stage="fit") @@ -670,7 +669,6 @@ def _predict_with_datamodule(self, overrides: dict[str, Any]) -> list[Prediction yolo = self._model.yolo device = self._device - yolo.model.to(device).eval() # pyrefly: ignore[missing-attribute] predictions: list[Prediction] = [] for batch in dataloader: diff --git a/library/src/getitune/backend/ultralytics/tools/configurator.py b/library/src/getitune/backend/ultralytics/tools/configurator.py index 386c7e8fd56..cae79982d87 100644 --- a/library/src/getitune/backend/ultralytics/tools/configurator.py +++ b/library/src/getitune/backend/ultralytics/tools/configurator.py @@ -562,7 +562,7 @@ def create_engine( self, model: UltralyticsModel, data: DataModule | PathLike | None = None, - work_dir: PathLike = "./getitune-workspace", + work_dir: PathLike | None = None, device: str | DeviceType = DeviceType.auto, **engine_kwargs, ) -> UltralyticsEngine: diff --git a/library/src/getitune/engine/utils/create.py b/library/src/getitune/engine/utils/create.py index bbff762c651..215d8c06744 100644 --- a/library/src/getitune/engine/utils/create.py +++ b/library/src/getitune/engine/utils/create.py @@ -7,13 +7,14 @@ import os from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any import yaml from getitune.engine.engine import Engine if TYPE_CHECKING: + from getitune.types import PathLike from getitune.types.task import TaskType from getitune.types.types import DATA, MODEL @@ -95,7 +96,7 @@ def _read_backend(recipe_path: Path) -> str: def create_engine( model: MODEL, data: DATA, - work_dir: str | None = None, + work_dir: PathLike | None = None, device: str | None = None, checkpoint: str | None = None, task: TaskType | str | None = None, @@ -161,7 +162,7 @@ def create_engine( supported_engines.append(child_engine) # Build kwargs dict with common arguments (skip None values so defaults apply). - common_kwargs = {} + common_kwargs: dict[str, Any] = {} if work_dir is not None: common_kwargs["work_dir"] = work_dir if device is not None: @@ -207,7 +208,7 @@ def create_engine( msg = f"Engine {engine_cls.__name__} does not implement is_supported." raise ValueError(msg) if engine_cls.is_supported(model, data): - return engine_cls(model=model, data=data, **common_kwargs) + return engine_cls(model=model, data=data, **common_kwargs) # pyrefly: ignore[unexpected-keyword] msg = f"No engine found for model {model!r} and data {data!r}" raise ValueError(msg) From 73315519e1cf5a1aaeaca16b2b70df54dfbe0e4a Mon Sep 17 00:00:00 2001 From: kprokofi Date: Fri, 19 Jun 2026 23:57:51 +0900 Subject: [PATCH 03/15] fix for ultralytics checkpint --- library/src/getitune/backend/lightning/engine.py | 15 +++------------ .../src/getitune/backend/ultralytics/engine.py | 10 +++++++--- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/library/src/getitune/backend/lightning/engine.py b/library/src/getitune/backend/lightning/engine.py index e3f6ad10498..471a3ff45fe 100644 --- a/library/src/getitune/backend/lightning/engine.py +++ b/library/src/getitune/backend/lightning/engine.py @@ -11,6 +11,7 @@ import logging import multiprocessing import os +import shutil import time from contextlib import contextmanager from pathlib import Path @@ -309,18 +310,8 @@ def train( msg = "self.checkpoint should be Path or str at this time." raise TypeError(msg) - best_checkpoint_symlink = Path(self.work_dir) / "best_checkpoint.pt" - if best_checkpoint_symlink.is_symlink() and best_checkpoint_symlink.exists(): - best_checkpoint_symlink.unlink() - try: - best_checkpoint_symlink.symlink_to(self.checkpoint) - except OSError: - # Symlink creation may fail on Windows when the process lacks - # SeCreateSymbolicLinkPrivilege (e.g. frozen PyInstaller apps - # running without admin rights). Fall back to a regular copy. - import shutil - - shutil.copy2(self.checkpoint, best_checkpoint_symlink) + best_checkpoint = Path(self.work_dir) / "best_checkpoint.pt" + shutil.copy2(self.checkpoint, best_checkpoint) return self._build_train_metrics() diff --git a/library/src/getitune/backend/ultralytics/engine.py b/library/src/getitune/backend/ultralytics/engine.py index e0a58c3938d..b197d90643b 100644 --- a/library/src/getitune/backend/ultralytics/engine.py +++ b/library/src/getitune/backend/ultralytics/engine.py @@ -718,7 +718,7 @@ def _load_last_train_checkpoint(self) -> Path | None: if not checkpoint_text: return None - checkpoint = Path(checkpoint_text).resolve() + checkpoint = Path(checkpoint_text) if checkpoint.exists(): return checkpoint @@ -740,11 +740,15 @@ def _record_last_train_checkpoint(self, checkpoint: Path | None) -> None: # Copy to a canonical path so the public API never exposes # Ultralytics-internal directory structure (train/weights/best.pt). + # Unlink any pre-existing symlink so copyfile writes a real file + # instead of following the symlink and overwriting its target. canonical_path = self._work_dir / "best_checkpoint.pt" + if canonical_path.is_symlink(): + canonical_path.unlink() shutil.copyfile(checkpoint, canonical_path) - self._last_train_checkpoint = canonical_path.resolve() - checkpoint_file.write_text(str(self._last_train_checkpoint), encoding="utf-8") + self._last_train_checkpoint = canonical_path + checkpoint_file.write_text(str(canonical_path), encoding="utf-8") def _remap_results_csv(self) -> None: """Rename columns in the Ultralytics ``results.csv`` to standard metric names. From 96ce1d0e02f32811e5a62ec14a380bff795fef1c Mon Sep 17 00:00:00 2001 From: kprokofi Date: Sat, 20 Jun 2026 00:01:57 +0900 Subject: [PATCH 04/15] fix import --- .../backend/app/execution/common/geti_config_converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/backend/app/execution/common/geti_config_converter.py b/application/backend/app/execution/common/geti_config_converter.py index f0ed31860e7..ae1403f2183 100644 --- a/application/backend/app/execution/common/geti_config_converter.py +++ b/application/backend/app/execution/common/geti_config_converter.py @@ -11,9 +11,9 @@ from warnings import warn import yaml -from getitune.backend.lightning.cli.utils import get_getitune_root_path from getitune.backend.ultralytics.tools.configurator import Configurator as UltralyticsConfigurator from getitune.tools.auto_configurator import AutoConfigurator +from getitune.utils import get_getitune_root_path from loguru import logger RECIPE_PATH = get_getitune_root_path() / "recipe" From a593d291366d98d0ff7f2bce2f3b5a86d17c1266 Mon Sep 17 00:00:00 2001 From: kprokofi Date: Sat, 20 Jun 2026 00:32:26 +0900 Subject: [PATCH 05/15] add unit tests --- .../src/getitune/backend/lightning/engine.py | 2 +- library/tests/unit/engine/test_engine.py | 130 ++++++++++++++++-- 2 files changed, 123 insertions(+), 9 deletions(-) diff --git a/library/src/getitune/backend/lightning/engine.py b/library/src/getitune/backend/lightning/engine.py index 471a3ff45fe..2fb96b583a8 100644 --- a/library/src/getitune/backend/lightning/engine.py +++ b/library/src/getitune/backend/lightning/engine.py @@ -755,7 +755,7 @@ def from_config( if checkpoint is not None: process_kwargs["engine.checkpoint"] = checkpoint if task is not None: - process_kwargs["engine.task"] = task + process_kwargs["engine.task"] = task.value if isinstance(task, TaskType) else task instantiated_config, train_kwargs = get_instantiated_classes( config=config_path, data_root=data_root, diff --git a/library/tests/unit/engine/test_engine.py b/library/tests/unit/engine/test_engine.py index 3ea7e564043..260379d0181 100644 --- a/library/tests/unit/engine/test_engine.py +++ b/library/tests/unit/engine/test_engine.py @@ -12,6 +12,7 @@ from getitune.backend.openvino.models.base import OVModel from getitune.data.module import DataModule from getitune.engine import Engine, create_engine +from getitune.engine.utils.create import _RECIPE_PATH, _read_backend, _resolve_recipe class TestCreateEngine: @@ -22,14 +23,16 @@ def mock_engine_subclass(self): mock_engine_cls.is_supported.return_value = True return mock_engine_cls + @patch("getitune.backend.lightning.engine.LightningEngine.is_supported", return_value=False) + @patch("getitune.backend.openvino.engine.OVEngine.is_supported", return_value=False) @patch("getitune.engine.Engine.__subclasses__", autospec=True) - def test_create_engine(self, mock___subclasses__, mock_engine_subclass): + def test_create_engine(self, mock___subclasses__, mock_ov_is_supported, mock_lt_is_supported, mock_engine_subclass): """Test create_engine with arbitrary Engine.""" mock___subclasses__.return_value = [mock_engine_subclass] - mock_model = MagicMock() - mock_data = MagicMock() + mock_model = MagicMock(spec=LightningModel) + mock_data = MagicMock(spec=DataModule) - engine_instance = create_engine(mock_model, mock_data) + engine_instance = create_engine(mock_model, mock_data) # pyrefly: ignore[bad-argument-type] mock_engine_subclass.is_supported.assert_called_once_with(mock_model, mock_data) mock_engine_subclass.assert_called_once_with(model=mock_model, data=mock_data) @@ -38,15 +41,15 @@ def test_create_engine(self, mock___subclasses__, mock_engine_subclass): # test create_engine when is_supported returns False mock_engine_subclass.is_supported.return_value = False with pytest.raises(ValueError, match="No engine found for model .* and data .*"): - create_engine(mock_model, mock_data) + create_engine(mock_model, mock_data) # pyrefly: ignore[bad-argument-type] # test create_engine when no subclasses are found mock___subclasses__.return_value = [] - mock_model = MagicMock() - mock_data = MagicMock() + mock_model = MagicMock(spec=LightningModel) + mock_data = MagicMock(spec=DataModule) with pytest.raises(ValueError, match="No engine found for model .* and data .*"): - create_engine(mock_model, mock_data) + create_engine(mock_model, mock_data) # pyrefly: ignore[bad-argument-type] def test_create_native_engine(self, mocker): mock_model = MagicMock(spec=LightningModel) @@ -83,3 +86,114 @@ def test_create_openvino_engine(self, mocker): engine_instance = create_engine(mock_model, mock_data, work_dir="path/to/workdir") assert isinstance(engine_instance, OVEngine) mock_engine_init.assert_called_with(model=mock_model, data=mock_data, work_dir="path/to/workdir") + + +class TestResolveRecipe: + def test_recipe_path_direct_yaml(self): + """Passing an absolute .yaml path resolves to itself.""" + recipe = _RECIPE_PATH / "detection" / "yolox_s.yaml" + result = _resolve_recipe(str(recipe), task=None) + assert result == recipe.resolve() + + def test_recipe_path_not_found(self): + """Nonexistent .yaml path raises FileNotFoundError.""" + with pytest.raises(FileNotFoundError, match="Recipe file not found"): + _resolve_recipe("/nonexistent/recipe.yaml", task=None) + + def test_model_name_unique(self): + """Bare model name with a single match resolves correctly.""" + result = _resolve_recipe("yolo26_n", task=None) + assert result.name == "yolo26_n.yaml" + assert result.parent.name == "detection" + + def test_model_name_not_found(self): + """Nonexistent model name raises FileNotFoundError.""" + with pytest.raises(FileNotFoundError, match="No recipe found for model"): + _resolve_recipe("nonexistent_model_xyz", task=None) + + def test_model_name_ambiguous_without_task(self): + """Ambiguous name without task raises ValueError listing candidates.""" + with pytest.raises(ValueError, match="matches multiple recipes"): + _resolve_recipe("dino_v2", task=None) + + def test_model_name_ambiguous_with_task(self): + """Ambiguous name with task= resolves to the correct subdirectory.""" + result = _resolve_recipe("dino_v2", task="SEMANTIC_SEGMENTATION") + assert result.name == "dino_v2.yaml" + assert "semantic_segmentation" in str(result) + + +class TestReadBackend: + def test_read_backend_ultralytics(self, tmp_path): + """Recipe with backend: ultralytics returns 'ultralytics'.""" + recipe = tmp_path / "recipe.yaml" + recipe.write_text("backend: ultralytics\n") + assert _read_backend(recipe) == "ultralytics" + + def test_read_backend_lightning_default(self, tmp_path): + """Recipe without backend field defaults to 'lightning'.""" + recipe = tmp_path / "recipe.yaml" + recipe.write_text("task: DETECTION\n") + assert _read_backend(recipe) == "lightning" + + def test_read_backend_empty_file(self, tmp_path): + """Empty recipe file defaults to 'lightning'.""" + recipe = tmp_path / "recipe.yaml" + recipe.write_text("") + assert _read_backend(recipe) == "lightning" + + +class TestCreateEngineRecipe: + @patch("getitune.backend.lightning.engine.LightningEngine.from_config") + def test_recipe_path_dispatches_to_backend(self, mock_from_config, tmp_path): + """A recipe .yaml path dispatches to the correct backend's from_config.""" + recipe = tmp_path / "recipe.yaml" + recipe.write_text("backend: lightning\n") + mock_from_config.return_value = MagicMock(spec=LightningEngine) + + result = create_engine(str(recipe), data="dummy_path") + + mock_from_config.assert_called_once() + assert isinstance(result, Engine) + + @patch("getitune.backend.lightning.engine.LightningEngine.from_config") + def test_model_name_resolves_and_dispatches(self, mock_from_config): + """Bare model name is resolved to a recipe and dispatched.""" + mock_from_config.return_value = MagicMock(spec=LightningEngine) + + result = create_engine("yolox_s", data="dummy_path") + + mock_from_config.assert_called_once() + assert isinstance(result, Engine) + + def test_unknown_backend_raises(self, tmp_path): + """Recipe with unknown backend field raises ValueError.""" + recipe = tmp_path / "recipe.yaml" + recipe.write_text("backend: unknown_backend\n") + with pytest.raises(ValueError, match="Unknown backend"): + create_engine(str(recipe), data="dummy_path") + + def test_nonexistent_recipe_path_raises(self): + """Passing a nonexistent .yaml path raises FileNotFoundError.""" + with pytest.raises(FileNotFoundError, match="Recipe file not found"): + create_engine("/nonexistent/path.yaml", data="dummy_path") + + def test_nonexistent_model_name_raises(self): + """Passing a nonexistent model name raises FileNotFoundError.""" + with pytest.raises(FileNotFoundError, match="No recipe found for model"): + create_engine("nonexistent_model_xyz", data="dummy_path") + + def test_model_name_ambiguous_raises(self): + """Ambiguous model name without task raises ValueError.""" + with pytest.raises(ValueError, match="matches multiple recipes"): + create_engine("dino_v2", data="dummy_path") + + def test_task_disambiguation(self, mocker): + """Ambiguous model name with task= disambiguates successfully.""" + mock_from_config = mocker.patch( + "getitune.backend.lightning.engine.LightningEngine.from_config", + return_value=MagicMock(spec=LightningEngine), + ) + result = create_engine("dino_v2", data="dummy_path", task="SEMANTIC_SEGMENTATION") + mock_from_config.assert_called_once() + assert isinstance(result, Engine) From 487b988b504b3b42335a94ecc0084864a18a2cd1 Mon Sep 17 00:00:00 2001 From: kprokofi Date: Sat, 20 Jun 2026 00:52:58 +0900 Subject: [PATCH 06/15] unit test fix --- library/tests/unit/backend/lightning/test_engine.py | 4 ++-- .../test_hier_metric_collection_from_engine.py | 12 +++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/library/tests/unit/backend/lightning/test_engine.py b/library/tests/unit/backend/lightning/test_engine.py index bc083bd7a31..edfaab63256 100644 --- a/library/tests/unit/backend/lightning/test_engine.py +++ b/library/tests/unit/backend/lightning/test_engine.py @@ -68,7 +68,7 @@ def test_model_init(self, tmp_path, mocker): assert engine._model.label_info.num_classes == 4321 def test_training_with_override_args(self, fxt_engine, mocker) -> None: - mocker.patch("pathlib.Path.symlink_to") + mocker.patch("getitune.backend.lightning.engine.shutil.copy2") mocker.patch("getitune.backend.lightning.engine.Trainer.fit") mock_seed_everything = mocker.patch("getitune.backend.lightning.engine.seed_everything") @@ -237,7 +237,7 @@ def test_from_config_with_model_name(self, tmp_path) -> None: def test_from_config(self, tmp_path, mocker) -> None: recipe_path = "src/getitune/recipe/classification/multi_class_cls/mobilenet_v3_large.yaml" data_root = "tests/assets/classification_cifar10" - mocker.patch("pathlib.Path.symlink_to") + mocker.patch("getitune.backend.lightning.engine.shutil.copy2") mocker.patch("getitune.backend.lightning.engine.Trainer.fit") overriding = { diff --git a/library/tests/unit/metrics/test_hier_metric_collection_from_engine.py b/library/tests/unit/metrics/test_hier_metric_collection_from_engine.py index 5f4c0d77961..e02466a9cff 100644 --- a/library/tests/unit/metrics/test_hier_metric_collection_from_engine.py +++ b/library/tests/unit/metrics/test_hier_metric_collection_from_engine.py @@ -6,6 +6,8 @@ import pytest +from getitune.backend.lightning.models.base import LightningModel +from getitune.data.module import DataModule from getitune.engine import Engine, create_engine from getitune.metrics.hier_metric_collection import hier_metric_collection_callable @@ -18,12 +20,16 @@ def mock_engine_subclass(self): mock_engine_cls.is_supported.return_value = True return mock_engine_cls + @patch("getitune.backend.lightning.engine.LightningEngine.is_supported", return_value=False) + @patch("getitune.backend.openvino.engine.OVEngine.is_supported", return_value=False) @patch("getitune.engine.Engine.__subclasses__", autospec=True) - def test_hier_metric_collection_by_engine(self, mock___subclasses__, mock_engine_subclass): + def test_hier_metric_collection_by_engine( + self, mock___subclasses__, mock_ov_is_supported, mock_lt_is_supported, mock_engine_subclass + ): """Test create_engine with arbitrary Engine.""" mock___subclasses__.return_value = [mock_engine_subclass] - mock_model = MagicMock() - mock_data = MagicMock() + mock_model = MagicMock(spec=LightningModel) # pyrefly: ignore[bad-argument-type] + mock_data = MagicMock(spec=DataModule) # pyrefly: ignore[bad-argument-type] engine_instance = create_engine(mock_model, mock_data) engine_instance.train(metric=hier_metric_collection_callable) From d78c6058c37f3ea4ca1f5ee7ff56b117f35c872b Mon Sep 17 00:00:00 2001 From: Yura Volokitin Date: Mon, 22 Jun 2026 11:31:43 +0200 Subject: [PATCH 07/15] updated for Readme --- README.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6a9a2c901b2..ee6f5ad086d 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ in a rapid, feedback-driven loop. Geti runs locally as a single Docker image or for Intel® hardware with OpenVINO™ for fast inference across the full Intel® XPU portfolio.

- Geti™ - Learning Cycle + Geti™ - Learning Cycle

> [!IMPORTANT] @@ -165,8 +165,6 @@ Would you like to see a specific model added to the list? Let us know by opening - - > [!TIP] > Other projects of the Open Edge Platform enable even more tasks and models, check them: > @@ -176,6 +174,18 @@ Would you like to see a specific model added to the list? Let us know by opening > - [OpenVINO™](https://github.com/openvinotoolkit/openvino) - Software toolkit for optimizing and deploying deep learning models. > - [Model API](https://github.com/open-edge-platform/model_api) - wrapper that simplifies model loading, execution, and data processing for easy inference + +
+

+ Geti™ - Learning Cycle +

+ +
+ +**Geti™ enables a seamless end‑to‑end workflow for building state‑of‑the‑art computer vision models quickly with minimal data** + +
+ ## Quick Start Get Geti running and train your first model in a few minutes. For full instructions and all options, see the From 1699369627483acd6e3d709bc721575ad791d155 Mon Sep 17 00:00:00 2001 From: kprokofi Date: Mon, 22 Jun 2026 20:02:30 +0900 Subject: [PATCH 08/15] docs: enhance root README with comprehensive create_engine example and add PyPI warnings for YOLO26 models --- README.md | 33 +++++++++++++++++++++++++-------- library/README.md | 7 +++++++ 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index ee6f5ad086d..6a1a9f1cc8e 100644 --- a/README.md +++ b/README.md @@ -258,21 +258,38 @@ from Python. It requires **Python 3.11–3.14**, **PyTorch 2.10**, **OpenVINO™ pip install "getitune[cpu]" # or [xpu] for Intel® GPU, [cuda] for NVIDIA® GPU ``` +**Discover available models and train a model in just a few lines of code:** + ```python from getitune.engine import create_engine +from getitune.utils import list_models + +# Explore available models for your task +all_models = list_models() # List all model names +detection_models = list_models(task="DETECTION") # Filter by task +recipes = list_models(return_recipes=True) # Get full recipe YAML paths -# Initialize and train using a bundled recipe and dataset +# Create an engine using any model name or recipe path engine = create_engine( - data="tests/assets/classification_cifar10", - model="src/getitune/recipe/classification/multi_class_cls/efficientnet_b0.yaml", + model="efficientnet_b0", # model name, recipe YAML path, or exported IR/ONNX + data="/path/to/dataset", # dataset directory or YAML path + work_dir="./my_workspace", # checkpoints and logs directory + device="auto", # "auto", "cpu", "gpu", "0", "xpu", etc. ) -engine.train() -engine.test() -exported_path = engine.export() # writes OpenVINO IR + +# Train and validate +engine.train(max_epochs=50) +metrics = engine.test() + +# Export to OpenVINO IR for deployment +exported_model_path = engine.export() + +# Inference on dataset or custom inputs +predictions = engine.predict() ``` -See the [library README](library/README.md) for the full list of recipes, advanced configuration, dataset support, and -inference/optimization examples. +See the [library README](library/README.md) for the full list of recipes, advanced configuration, dataset support, +backend-specific options, and deployment/optimization examples. ## Migrating from Geti 2.x diff --git a/library/README.md b/library/README.md index 9ecd1986702..555fec0d620 100644 --- a/library/README.md +++ b/library/README.md @@ -121,6 +121,9 @@ uv pip install "getitune[cpu]" > uv pip install "getitune[cuda,ultralytics]" # NVIDIA GPU + YOLO > uv pip install "getitune[cpu,ultralytics]" # CPU + YOLO > ``` +> +> ⚠️ **Note**: The PyPI `[ultralytics]` extra includes the Ultralytics framework but **does not bundle YOLO26 model weights**. +> To use YOLO26 models, you must [install from source](#advanced-installation-install-from-source) using `[ultralytics]`, which includes pre-downloaded model weights.
@@ -370,6 +373,10 @@ engine.train(epochs=50) engine.test() engine.export() +> ⚠️ **Note for PyPI users**: Direct `UltralyticsEngine` instantiation (as shown above) with YOLO models requires +> [installing from source](#advanced-installation-install-from-source) with the `[ultralytics]` extra to access pre-downloaded model weights. +> For PyPI installs, use `create_engine()` which handles model resolution automatically. + # -- OpenVINO Backend (inference) -- from getitune.backend.openvino.engine import OVEngine From 4639f75f0d01c52189bdef4aa57152413d6bb70d Mon Sep 17 00:00:00 2001 From: Yura Volokitin Date: Mon, 22 Jun 2026 13:17:38 +0200 Subject: [PATCH 09/15] Revert "updated for Readme" This reverts commit d78c6058c37f3ea4ca1f5ee7ff56b117f35c872b. --- README.md | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index ee6f5ad086d..6a9a2c901b2 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ in a rapid, feedback-driven loop. Geti runs locally as a single Docker image or for Intel® hardware with OpenVINO™ for fast inference across the full Intel® XPU portfolio.

- Geti™ - Learning Cycle + Geti™ - Learning Cycle

> [!IMPORTANT] @@ -165,6 +165,8 @@ Would you like to see a specific model added to the list? Let us know by opening + + > [!TIP] > Other projects of the Open Edge Platform enable even more tasks and models, check them: > @@ -174,18 +176,6 @@ Would you like to see a specific model added to the list? Let us know by opening > - [OpenVINO™](https://github.com/openvinotoolkit/openvino) - Software toolkit for optimizing and deploying deep learning models. > - [Model API](https://github.com/open-edge-platform/model_api) - wrapper that simplifies model loading, execution, and data processing for easy inference - -
-

- Geti™ - Learning Cycle -

- -
- -**Geti™ enables a seamless end‑to‑end workflow for building state‑of‑the‑art computer vision models quickly with minimal data** - -
- ## Quick Start Get Geti running and train your first model in a few minutes. For full instructions and all options, see the From 4ead3a6b61a54d474e75bfcb061e90b04681ec17 Mon Sep 17 00:00:00 2001 From: kprokofi Date: Mon, 22 Jun 2026 20:26:32 +0900 Subject: [PATCH 10/15] docs: fix PyPI installation, add source-install subsection, clarify Ultralytics is source-only - Fix --extra-index-url quoting for pip install commands - Add pip install examples for XPU, CUDA, and CPU backends - Inline source-install commands (with Ultralytics) under Ultralytics warning - Link to library README for full installation details (including uv) - Fix library README: remove misleading PyPI warning, Ultralytics in source only --- README.md | 18 ++++++++++++++++-- library/README.md | 27 +++++++++++---------------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 6a1a9f1cc8e..982c4cf40d1 100644 --- a/README.md +++ b/README.md @@ -249,15 +249,29 @@ Once Geti is running, build your first model directly in the web UI: See [Training your first model](https://docs.geti.intel.com/) for the full walkthrough. -### Use the Python API (`getitune`) +### Use the Geti Library (`getitune`) Prefer to work programmatically? Geti's training engine is published on PyPI and can train, optimize, and deploy models from Python. It requires **Python 3.11–3.14**, **PyTorch 2.10**, **OpenVINO™ 2026.1**, and **NumPy ≥ 2.0**. ```bash -pip install "getitune[cpu]" # or [xpu] for Intel® GPU, [cuda] for NVIDIA® GPU +uv pip install "getitune[xpu]" --extra-index-url https://download.pytorch.org/whl/xpu # for Intel® XPU acceleration +uv pip install "getitune[cuda]" --extra-index-url https://download.pytorch.org/whl/cu128 # for NVIDIA® CUDA acceleration +uv pip install getitune # CPU-only by default ``` +> ⚠️ **Ultralytics YOLO Models**: The PyPI package does **not** include Ultralytics YOLO26 models. Install from source to use them: +> +> ```bash +> git clone https://github.com/open-edge-platform/training_extensions.git +> cd training_extensions/library +> uv sync --extra cpu --extra ultralytics # CPU + YOLO +> uv sync --extra xpu --extra ultralytics # Intel GPU + YOLO +> uv sync --extra cuda --extra ultralytics # NVIDIA GPU + YOLO +> ``` +> +> See the [library README](library/README.md#installation) for more details. + **Discover available models and train a model in just a few lines of code:** ```python diff --git a/library/README.md b/library/README.md index 555fec0d620..e1e9b5f8e8f 100644 --- a/library/README.md +++ b/library/README.md @@ -82,6 +82,7 @@ Requirements: **Python 3.11–3.14**, **PyTorch 2.10**, **OpenVINO™ 2026.1**, ```bash # With uv (recommended) +# CPU-only by default uv pip install "getitune" # Or with pip @@ -90,6 +91,8 @@ pip install "getitune" # For hardware-specific PyTorch wheels, see "Advanced Installation: Specify Hardware Backend" below. ``` +⚠️ **Note**: PyPi version doesn't support Ultralytics YOLO models. To use Ultralytics YOLO models, you must [install from source](#advanced-installation-install-from-source). +
Advanced Installation: Specify Hardware Backend @@ -114,21 +117,12 @@ uv pip install "getitune[cpu]" > **macOS note**: PyTorch's `+cpu` wheel is only published for Linux and Windows. The `[cpu]` extra resolves this automatically and installs the default `torch==2.10.0` wheel on macOS. -> **Ultralytics YOLO models**: YOLO26 support requires the additional `[ultralytics]` extra. Combine it with any hardware extra: -> -> ```bash -> uv pip install "getitune[xpu,ultralytics]" # Intel GPU + YOLO -> uv pip install "getitune[cuda,ultralytics]" # NVIDIA GPU + YOLO -> uv pip install "getitune[cpu,ultralytics]" # CPU + YOLO -> ``` -> -> ⚠️ **Note**: The PyPI `[ultralytics]` extra includes the Ultralytics framework but **does not bundle YOLO26 model weights**. -> To use YOLO26 models, you must [install from source](#advanced-installation-install-from-source) using `[ultralytics]`, which includes pre-downloaded model weights. - +> **Ultralytics YOLO models**: The PyPI version doesn't include Ultralytics YOLO support. +> To use YOLO26 models, you must [install from source](#advanced-installation-install-from-source).
- Advanced Installation: Install from Source + Advanced Installation: Install from Source with Ultralytics YOLO Support ```bash git clone https://github.com/open-edge-platform/geti.git @@ -158,7 +152,9 @@ pip install -e ".[cuda]" \ > > ```bash > uv sync --extra xpu --extra ultralytics # Intel GPU + YOLO -> pip install -e ".[xpu,ultralytics]" # Intel GPU + YOLO +> +> # or with pip +> pip install -e ".[xpu,ultralytics]" --extra-index-url https://download.pytorch.org/whl/xpu #Intel GPU + YOLO > ```
@@ -373,9 +369,8 @@ engine.train(epochs=50) engine.test() engine.export() -> ⚠️ **Note for PyPI users**: Direct `UltralyticsEngine` instantiation (as shown above) with YOLO models requires -> [installing from source](#advanced-installation-install-from-source) with the `[ultralytics]` extra to access pre-downloaded model weights. -> For PyPI installs, use `create_engine()` which handles model resolution automatically. +> ⚠️ **Note**: Ultralytics YOLO models and the `UltralyticsEngine` backend require [installing from source](#advanced-installation-install-from-source) with the `[ultralytics]` extra. +> The PyPI package does **not** include Ultralytics support. # -- OpenVINO Backend (inference) -- From 9b6b32113622a9b3362c4f36e8f2b4b075066ce8 Mon Sep 17 00:00:00 2001 From: kprokofi Date: Mon, 22 Jun 2026 20:43:39 +0900 Subject: [PATCH 11/15] small fix for README --- README.md | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 982c4cf40d1..fbad9ef5292 100644 --- a/README.md +++ b/README.md @@ -265,9 +265,9 @@ uv pip install getitune # CPU-only by default > ```bash > git clone https://github.com/open-edge-platform/training_extensions.git > cd training_extensions/library -> uv sync --extra cpu --extra ultralytics # CPU + YOLO > uv sync --extra xpu --extra ultralytics # Intel GPU + YOLO > uv sync --extra cuda --extra ultralytics # NVIDIA GPU + YOLO +> uv sync --extra cpu --extra ultralytics # CPU + YOLO > ``` > > See the [library README](library/README.md#installation) for more details. @@ -288,18 +288,27 @@ engine = create_engine( model="efficientnet_b0", # model name, recipe YAML path, or exported IR/ONNX data="/path/to/dataset", # dataset directory or YAML path work_dir="./my_workspace", # checkpoints and logs directory - device="auto", # "auto", "cpu", "gpu", "0", "xpu", etc. + device="auto", # "auto", "cpu", "gpu", "xpu". ) # Train and validate engine.train(max_epochs=50) metrics = engine.test() -# Export to OpenVINO IR for deployment +# Export to OpenVINO IR (default) for deployment exported_model_path = engine.export() -# Inference on dataset or custom inputs -predictions = engine.predict() +# load exported OpenVINO model +ov_engine = create_engine(model=exported_model_path, data=engine.datamodule) + +# optimize the model for edge deployment +optimized_model_path = ov_engine.optimize() + +# test the optimized model +metrics = ov_engine.test() + +# predict with the optimized model +predictions = ov_engine.predict() ``` See the [library README](library/README.md) for the full list of recipes, advanced configuration, dataset support, @@ -341,3 +350,5 @@ Stay tuned for further updates soon! ## Disclaimers FFmpeg is an open source project licensed under LGPL and GPL. See [https://www.ffmpeg.org/legal.html](https://www.ffmpeg.org/legal.html). You are solely responsible for determining if your use of FFmpeg requires any additional licenses. Intel is not responsible for obtaining any such licenses, nor liable for any licensing fees due, in connection with your use of FFmpeg. + +Ultralytics YOLO models are distributed under the AGPL-3.0 license, an OSI approved license ideal for open-source research, academic, and personal projects. For commercial use, enhanced support, and tailored licensing terms, please explore flexible Ultralytics licensing options at https://www.ultralytics.com/license. From f0fa3f7a902c21197ef3ebacb560ff9b16d0f775 Mon Sep 17 00:00:00 2001 From: kprokofi Date: Mon, 22 Jun 2026 20:53:52 +0900 Subject: [PATCH 12/15] resolve merge conflicts: remove duplicate Quick Start block, restore clean structure - Replace old Quick start with getitune content with enhanced version - Remove duplicate Quick Start/Docker section added during merge resolution - Keep community user list from develop under Who uses Geti? - Preserve Migrating from Geti 2.x and Documentation sections --- README.md | 198 +++++++++++++----------------------------------------- 1 file changed, 46 insertions(+), 152 deletions(-) diff --git a/README.md b/README.md index 3bc202516db..700f69bb96f 100644 --- a/README.md +++ b/README.md @@ -116,56 +116,68 @@ Once Geti is up and running, follow the intuitive UI to train your first model. ## Quick start with `getitune` -The Geti™ training engine `getitune` is published on PyPI and can train, optimize, and deploy models. - -To install `getitune`: +Prefer to work programmatically? Geti's training engine is published on PyPI and can train, optimize, and deploy models +from Python. It requires **Python 3.11–3.14**, **PyTorch 2.10**, **OpenVINO™ 2026.1**, and **NumPy ≥ 2.0**. ```bash -# With uv (recommended) -uv pip install "getitune" - -# Or with pip -pip install "getitune" +uv pip install "getitune[xpu]" --extra-index-url https://download.pytorch.org/whl/xpu # for Intel® XPU acceleration +uv pip install "getitune[cuda]" --extra-index-url https://download.pytorch.org/whl/cu128 # for NVIDIA® CUDA acceleration +uv pip install getitune # CPU-only by default ``` -Provide `getitune` with a dataset and fine-tune a model: +> ⚠️ **Ultralytics YOLO Models**: The PyPI package does **not** include Ultralytics YOLO26 models. Install from source to use them: +> +> ```bash +> git clone https://github.com/open-edge-platform/training_extensions.git +> cd training_extensions/library +> uv sync --extra xpu --extra ultralytics # Intel GPU + YOLO +> uv sync --extra cuda --extra ultralytics # NVIDIA GPU + YOLO +> uv sync --extra cpu --extra ultralytics # CPU + YOLO +> ``` +> +> See the [library README](library/README.md#installation) for more details. -```Python -from getitune.utils import list_models +**Discover available models and train a model in just a few lines of code:** + +```python from getitune.engine import create_engine -from getitune.types import ExportFormat, ExportPrecision +from getitune.utils import list_models -# List all available models names -all_models = list_models() +# Explore available models for your task +all_models = list_models() # List all model names +detection_models = list_models(task="DETECTION") # Filter by task +recipes = list_models(return_recipes=True) # Get full recipe YAML paths -# create Engine +# Create an engine using any model name or recipe path engine = create_engine( - model="efficientnet_b0", - data="/path/to/dataset", - work_dir="./my_workspace", + model="efficientnet_b0", # model name, recipe YAML path, or exported IR/ONNX + data="/path/to/dataset", # dataset directory or YAML path + work_dir="./my_workspace", # checkpoints and logs directory + device="auto", # "auto", "cpu", "gpu", "xpu". ) -# train a model -engine.train() +# Train and validate +engine.train(max_epochs=50) +metrics = engine.test() + +# Export to OpenVINO IR (default) for deployment +exported_model_path = engine.export() -# Export to FP32 OpenVINO IR (default) -ov_ir_path = engine.export() +# load exported OpenVINO model +ov_engine = create_engine(model=exported_model_path, data=engine.datamodule) -# validate engine -ov_engine = create_engine( - model="/path/to/exported_model.xml", - data="/path/to/dataset", -) -ov_engine.test() # test on test subset -ov_engine.predict() # predict on test subset +# optimize the model for edge deployment +optimized_model_path = ov_engine.optimize() + +# test the optimized model +metrics = ov_engine.test() -# optimize a model to int8 quantized version via NNCF tool -ov_engine.optimize() +# predict with the optimized model +predictions = ov_engine.predict() ``` -> [!NOTE] -> See the [`getitune` README](library/README.md) for the full list of recipes, advanced configuration, dataset support, -> inference/optimization examples, and hardware-specific PyTorch installation options. +See the [library README](library/README.md) for the full list of recipes, advanced configuration, dataset support, +backend-specific options, and deployment/optimization examples. ## Key Features @@ -336,124 +348,6 @@ Geti™ is a powerful tool to build vision models for a wide range of processes, - [PeopleSense.AI](https://community.intel.com/t5/Blogs/Tech-Innovation/Artificial-Intelligence-AI/Intel-Liftoff-Days-2024-Highlights-from-the-Third-Edition/post/1661265) - [Capgemini](https://www.capgemini.com/insights/expert-perspectives/capgemini-and-intel-corporation-redefining-the-future-of-robotics-and-physical-ai/) -## Quick Start - -### 1. Install Geti™ - -Geti™ ships as a Docker image optimized for each supported hardware target. Use the provided install.sh script for a one-command setup: - -```bash -curl -fsSL https://raw.githubusercontent.com/open-edge-platform/training_extensions/develop/install.sh | bash -``` - -The script auto-detects your hardware and pulls the appropriate Docker image. For manual installation and alternative methods (Python Desktop, Docker Compose, Windows), see the [installation guide](https://docs.geti.intel.com/). - -#### Docker - -Pull a pre-built image for your hardware and launch it: - -```bash -docker pull ghcr.io/open-edge-platform/geti-xpu # modern Intel® CPU/GPU (recommended) -docker pull ghcr.io/open-edge-platform/geti-cuda # NVIDIA® CUDA platforms -docker pull ghcr.io/open-edge-platform/geti-cpu # CPU-only (most lightweight) - -# Retag the pulled image as `geti-{cpu,xpu,cuda}:latest` for using with `just run-image` -docker tag ghcr.io/open-edge-platform/geti-cpu:latest geti-cpu:latest - -just run-image --accelerator xpu # launch the application -``` - -Then open the Geti web application at [**http://localhost:7860**](http://localhost:7860). - -For build-from-source options and advanced setup, see the [installation guide](https://docs.geti.intel.com/) and the -[application README](application/README.md). - -#### Install natively with Ultralytics YOLO26 models (the latest NMS‑free, edge‑optimized models (Nano / Small / Medium) for object detection and instance segmentation. The integration covers the full model lifecycle: training, inference, quantization, and OpenVINO™ model export - -Linux, WSL (In order to run a script you need to have curl & git installed): - -```bash -curl -fsSL https://raw.githubusercontent.com/open-edge-platform/training_extensions/develop/install.sh | bash -``` - -### 2. Train your first model - -Once Geti is running, build your first model directly in the web UI: - -1. **Create a project** — choose a task (object detection, instance segmentation, or classification) and define your labels. -2. **Upload media** — drag in 20–50 representative images to start. -3. **Annotate** — label your media with the built-in manual and AI-assisted tools. -4. **Train** — pick a recommended architecture and start training; watch progress in the Jobs panel. -5. **Deploy** — build an inference pipeline (source → model → sink) and run predictions in real time, or export an - OpenVINO™-optimized bundle for the edge. - -See [Training your first model](https://docs.geti.intel.com/) for the full walkthrough. - -### Use the Geti Library (`getitune`) - -Prefer to work programmatically? Geti's training engine is published on PyPI and can train, optimize, and deploy models -from Python. It requires **Python 3.11–3.14**, **PyTorch 2.10**, **OpenVINO™ 2026.1**, and **NumPy ≥ 2.0**. - -```bash -uv pip install "getitune[xpu]" --extra-index-url https://download.pytorch.org/whl/xpu # for Intel® XPU acceleration -uv pip install "getitune[cuda]" --extra-index-url https://download.pytorch.org/whl/cu128 # for NVIDIA® CUDA acceleration -uv pip install getitune # CPU-only by default -``` - -> ⚠️ **Ultralytics YOLO Models**: The PyPI package does **not** include Ultralytics YOLO26 models. Install from source to use them: -> -> ```bash -> git clone https://github.com/open-edge-platform/training_extensions.git -> cd training_extensions/library -> uv sync --extra xpu --extra ultralytics # Intel GPU + YOLO -> uv sync --extra cuda --extra ultralytics # NVIDIA GPU + YOLO -> uv sync --extra cpu --extra ultralytics # CPU + YOLO -> ``` -> -> See the [library README](library/README.md#installation) for more details. - -**Discover available models and train a model in just a few lines of code:** - -```python -from getitune.engine import create_engine -from getitune.utils import list_models - -# Explore available models for your task -all_models = list_models() # List all model names -detection_models = list_models(task="DETECTION") # Filter by task -recipes = list_models(return_recipes=True) # Get full recipe YAML paths - -# Create an engine using any model name or recipe path -engine = create_engine( - model="efficientnet_b0", # model name, recipe YAML path, or exported IR/ONNX - data="/path/to/dataset", # dataset directory or YAML path - work_dir="./my_workspace", # checkpoints and logs directory - device="auto", # "auto", "cpu", "gpu", "xpu". -) - -# Train and validate -engine.train(max_epochs=50) -metrics = engine.test() - -# Export to OpenVINO IR (default) for deployment -exported_model_path = engine.export() - -# load exported OpenVINO model -ov_engine = create_engine(model=exported_model_path, data=engine.datamodule) - -# optimize the model for edge deployment -optimized_model_path = ov_engine.optimize() - -# test the optimized model -metrics = ov_engine.test() - -# predict with the optimized model -predictions = ov_engine.predict() -``` - -See the [library README](library/README.md) for the full list of recipes, advanced configuration, dataset support, -backend-specific options, and deployment/optimization examples. - ## Migrating from Geti 2.x Geti 3.0 introduces a simplified dataset‑based workflow: datasets must be exported and imported individually, models from 2.x require retraining, project-level migration is replaced by dataset-level transfer, and the REST API and deployment now use the OpenVINO™ Model API — **Please follow the From 9a5cfa4b4eef6164be3e2b24f41bc6f6c9273b5f Mon Sep 17 00:00:00 2001 From: kprokofi Date: Mon, 22 Jun 2026 20:53:52 +0900 Subject: [PATCH 13/15] resolve merge conflicts: remove duplicate Quick Start block, restore clean structure - Replace old Quick start with getitune content with enhanced version - Remove duplicate Quick Start/Docker section added during merge resolution - Keep community user list from develop under Who uses Geti? - Preserve Migrating from Geti 2.x and Documentation sections --- README.md | 207 ++++++++++++------------------------------------------ 1 file changed, 44 insertions(+), 163 deletions(-) diff --git a/README.md b/README.md index 3bc202516db..d67e8d6d78e 100644 --- a/README.md +++ b/README.md @@ -121,51 +121,64 @@ The Geti™ training engine `getitune` is published on PyPI and can train, optim To install `getitune`: ```bash -# With uv (recommended) -uv pip install "getitune" - -# Or with pip -pip install "getitune" +uv pip install "getitune[xpu]" --extra-index-url https://download.pytorch.org/whl/xpu # for Intel® XPU acceleration +uv pip install "getitune[cuda]" --extra-index-url https://download.pytorch.org/whl/cu128 # for NVIDIA® CUDA acceleration +uv pip install getitune # CPU-only by default ``` -Provide `getitune` with a dataset and fine-tune a model: +> ⚠️ **Ultralytics YOLO Models**: The PyPI package does **not** include Ultralytics YOLO26 models. Install from source to use them: +> +> ```bash +> git clone https://github.com/open-edge-platform/training_extensions.git +> cd training_extensions/library +> uv sync --extra xpu --extra ultralytics # Intel GPU + YOLO +> uv sync --extra cuda --extra ultralytics # NVIDIA GPU + YOLO +> uv sync --extra cpu --extra ultralytics # CPU + YOLO +> ``` +> +> See the [library README](library/README.md#installation) for more details. + +**Discover available models and train a model in just a few lines of code:** -```Python -from getitune.utils import list_models +```python from getitune.engine import create_engine -from getitune.types import ExportFormat, ExportPrecision +from getitune.utils import list_models -# List all available models names -all_models = list_models() +# Explore available models for your task +all_models = list_models() # List all model names +detection_models = list_models(task="DETECTION") # Filter by task +recipes = list_models(return_recipes=True) # Get full recipe YAML paths -# create Engine +# Create an engine using any model name or recipe path engine = create_engine( - model="efficientnet_b0", - data="/path/to/dataset", - work_dir="./my_workspace", + model="efficientnet_b0", # model name, recipe YAML path, or exported IR/ONNX + data="/path/to/dataset", # dataset directory or YAML path + work_dir="./my_workspace", # checkpoints and logs directory + device="auto", # "auto", "cpu", "gpu", "xpu". ) -# train a model -engine.train() +# Train and validate +engine.train(max_epochs=50) +metrics = engine.test() -# Export to FP32 OpenVINO IR (default) -ov_ir_path = engine.export() +# Export to OpenVINO IR (default) for deployment +exported_model_path = engine.export() -# validate engine -ov_engine = create_engine( - model="/path/to/exported_model.xml", - data="/path/to/dataset", -) -ov_engine.test() # test on test subset -ov_engine.predict() # predict on test subset +# load exported OpenVINO model +ov_engine = create_engine(model=exported_model_path, data=engine.datamodule) + +# optimize the model for edge deployment +optimized_model_path = ov_engine.optimize() -# optimize a model to int8 quantized version via NNCF tool -ov_engine.optimize() +# test the optimized model +metrics = ov_engine.test() + +# predict with the optimized model +predictions = ov_engine.predict() ``` -> [!NOTE] -> See the [`getitune` README](library/README.md) for the full list of recipes, advanced configuration, dataset support, -> inference/optimization examples, and hardware-specific PyTorch installation options. +See the [library README](library/README.md) for the full list of recipes, advanced configuration, dataset support, +backend-specific options, and deployment/optimization examples. ## Key Features @@ -336,138 +349,6 @@ Geti™ is a powerful tool to build vision models for a wide range of processes, - [PeopleSense.AI](https://community.intel.com/t5/Blogs/Tech-Innovation/Artificial-Intelligence-AI/Intel-Liftoff-Days-2024-Highlights-from-the-Third-Edition/post/1661265) - [Capgemini](https://www.capgemini.com/insights/expert-perspectives/capgemini-and-intel-corporation-redefining-the-future-of-robotics-and-physical-ai/) -## Quick Start - -### 1. Install Geti™ - -Geti™ ships as a Docker image optimized for each supported hardware target. Use the provided install.sh script for a one-command setup: - -```bash -curl -fsSL https://raw.githubusercontent.com/open-edge-platform/training_extensions/develop/install.sh | bash -``` - -The script auto-detects your hardware and pulls the appropriate Docker image. For manual installation and alternative methods (Python Desktop, Docker Compose, Windows), see the [installation guide](https://docs.geti.intel.com/). - -#### Docker - -Pull a pre-built image for your hardware and launch it: - -```bash -docker pull ghcr.io/open-edge-platform/geti-xpu # modern Intel® CPU/GPU (recommended) -docker pull ghcr.io/open-edge-platform/geti-cuda # NVIDIA® CUDA platforms -docker pull ghcr.io/open-edge-platform/geti-cpu # CPU-only (most lightweight) - -# Retag the pulled image as `geti-{cpu,xpu,cuda}:latest` for using with `just run-image` -docker tag ghcr.io/open-edge-platform/geti-cpu:latest geti-cpu:latest - -just run-image --accelerator xpu # launch the application -``` - -Then open the Geti web application at [**http://localhost:7860**](http://localhost:7860). - -For build-from-source options and advanced setup, see the [installation guide](https://docs.geti.intel.com/) and the -[application README](application/README.md). - -#### Install natively with Ultralytics YOLO26 models (the latest NMS‑free, edge‑optimized models (Nano / Small / Medium) for object detection and instance segmentation. The integration covers the full model lifecycle: training, inference, quantization, and OpenVINO™ model export - -Linux, WSL (In order to run a script you need to have curl & git installed): - -```bash -curl -fsSL https://raw.githubusercontent.com/open-edge-platform/training_extensions/develop/install.sh | bash -``` - -### 2. Train your first model - -Once Geti is running, build your first model directly in the web UI: - -1. **Create a project** — choose a task (object detection, instance segmentation, or classification) and define your labels. -2. **Upload media** — drag in 20–50 representative images to start. -3. **Annotate** — label your media with the built-in manual and AI-assisted tools. -4. **Train** — pick a recommended architecture and start training; watch progress in the Jobs panel. -5. **Deploy** — build an inference pipeline (source → model → sink) and run predictions in real time, or export an - OpenVINO™-optimized bundle for the edge. - -See [Training your first model](https://docs.geti.intel.com/) for the full walkthrough. - -### Use the Geti Library (`getitune`) - -Prefer to work programmatically? Geti's training engine is published on PyPI and can train, optimize, and deploy models -from Python. It requires **Python 3.11–3.14**, **PyTorch 2.10**, **OpenVINO™ 2026.1**, and **NumPy ≥ 2.0**. - -```bash -uv pip install "getitune[xpu]" --extra-index-url https://download.pytorch.org/whl/xpu # for Intel® XPU acceleration -uv pip install "getitune[cuda]" --extra-index-url https://download.pytorch.org/whl/cu128 # for NVIDIA® CUDA acceleration -uv pip install getitune # CPU-only by default -``` - -> ⚠️ **Ultralytics YOLO Models**: The PyPI package does **not** include Ultralytics YOLO26 models. Install from source to use them: -> -> ```bash -> git clone https://github.com/open-edge-platform/training_extensions.git -> cd training_extensions/library -> uv sync --extra xpu --extra ultralytics # Intel GPU + YOLO -> uv sync --extra cuda --extra ultralytics # NVIDIA GPU + YOLO -> uv sync --extra cpu --extra ultralytics # CPU + YOLO -> ``` -> -> See the [library README](library/README.md#installation) for more details. - -**Discover available models and train a model in just a few lines of code:** - -```python -from getitune.engine import create_engine -from getitune.utils import list_models - -# Explore available models for your task -all_models = list_models() # List all model names -detection_models = list_models(task="DETECTION") # Filter by task -recipes = list_models(return_recipes=True) # Get full recipe YAML paths - -# Create an engine using any model name or recipe path -engine = create_engine( - model="efficientnet_b0", # model name, recipe YAML path, or exported IR/ONNX - data="/path/to/dataset", # dataset directory or YAML path - work_dir="./my_workspace", # checkpoints and logs directory - device="auto", # "auto", "cpu", "gpu", "xpu". -) - -# Train and validate -engine.train(max_epochs=50) -metrics = engine.test() - -# Export to OpenVINO IR (default) for deployment -exported_model_path = engine.export() - -# load exported OpenVINO model -ov_engine = create_engine(model=exported_model_path, data=engine.datamodule) - -# optimize the model for edge deployment -optimized_model_path = ov_engine.optimize() - -# test the optimized model -metrics = ov_engine.test() - -# predict with the optimized model -predictions = ov_engine.predict() -``` - -See the [library README](library/README.md) for the full list of recipes, advanced configuration, dataset support, -backend-specific options, and deployment/optimization examples. - -## Migrating from Geti 2.x - -Geti 3.0 introduces a simplified dataset‑based workflow: datasets must be exported and imported individually, models from 2.x require retraining, project-level migration is replaced by dataset-level transfer, and the REST API and deployment now use the OpenVINO™ Model API — **Please follow the -[migration guidance](https://docs.geti.intel.com/) in the documentation.** - -## Documentation - -For complete user and developer documentation, visit [**docs.geti.intel.com**](https://docs.geti.intel.com/). - -| Component | README | Documentation | -| ------------------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------- | -| **Geti application** | [application/README.md](application/README.md) | [docs.geti.intel.com](https://docs.geti.intel.com/) | -| **Python API (getitune)** | [library/README.md](library/README.md) | [Docs](https://open-edge-platform.github.io/training_extensions/latest/index.html) | - ## Community - To report a bug or submit a feature request, please open a [GitHub issue](https://github.com/open-edge-platform/geti/issues). From 7820d33f94b63727e554224dd5bd3b825c897bc6 Mon Sep 17 00:00:00 2001 From: kprokofi Date: Mon, 22 Jun 2026 20:59:00 +0900 Subject: [PATCH 14/15] small fix --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 912df4ba18f..89770bb65a4 100644 --- a/README.md +++ b/README.md @@ -114,9 +114,9 @@ Once Geti is up and running, follow the intuitive UI to train your first model. > > Detailed installation guide is available in ["Installation guide"](https://docs.geti.intel.com/docs/user-guide/getting-started/installation/installation-guide) -## Quick start with `getitune` +## Quick start with Geti Library (`getitune`) -Prefer to work programmatically? Geti's training engine is published on PyPI and can train, optimize, and deploy models +Geti's training engine is published on PyPI and can train, optimize, and deploy models from Python. It requires **Python 3.11–3.14**, **PyTorch 2.10**, **OpenVINO™ 2026.1**, and **NumPy ≥ 2.0**. ```bash From e4a2431de5b43f812c68caf93596579f93f6d79b Mon Sep 17 00:00:00 2001 From: kprokofi Date: Mon, 22 Jun 2026 21:18:48 +0900 Subject: [PATCH 15/15] fix: address Copilot review feedback - Glob both *.yaml and *.yml for model name recipe resolution - Unlink best_checkpoint symlink before shutil.copy2 to prevent corruption - Remove stale OpenVINO version requirements from both READMEs --- README.md | 2 +- library/README.md | 2 -- library/src/getitune/backend/lightning/engine.py | 1 + library/src/getitune/engine/utils/create.py | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 89770bb65a4..d4ff44d67b8 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ Once Geti is up and running, follow the intuitive UI to train your first model. ## Quick start with Geti Library (`getitune`) Geti's training engine is published on PyPI and can train, optimize, and deploy models -from Python. It requires **Python 3.11–3.14**, **PyTorch 2.10**, **OpenVINO™ 2026.1**, and **NumPy ≥ 2.0**. +from Python. ```bash uv pip install "getitune[xpu]" --extra-index-url https://download.pytorch.org/whl/xpu # for Intel® XPU acceleration diff --git a/library/README.md b/library/README.md index e1e9b5f8e8f..5aea59a382f 100644 --- a/library/README.md +++ b/library/README.md @@ -76,8 +76,6 @@ Licensing Information: Ultralytics YOLO models are distributed under the AGPL-3. ## Installation -Requirements: **Python 3.11–3.14**, **PyTorch 2.10**, **OpenVINO™ 2026.1**, **NumPy ≥ 2.0**. - ## Quick Install ```bash diff --git a/library/src/getitune/backend/lightning/engine.py b/library/src/getitune/backend/lightning/engine.py index 2fb96b583a8..736a5661fc2 100644 --- a/library/src/getitune/backend/lightning/engine.py +++ b/library/src/getitune/backend/lightning/engine.py @@ -311,6 +311,7 @@ def train( raise TypeError(msg) best_checkpoint = Path(self.work_dir) / "best_checkpoint.pt" + best_checkpoint.unlink(missing_ok=True) shutil.copy2(self.checkpoint, best_checkpoint) return self._build_train_metrics() diff --git a/library/src/getitune/engine/utils/create.py b/library/src/getitune/engine/utils/create.py index 215d8c06744..b66176a9d90 100644 --- a/library/src/getitune/engine/utils/create.py +++ b/library/src/getitune/engine/utils/create.py @@ -51,7 +51,7 @@ def _resolve_recipe(model: MODEL, task: TaskType | str | None) -> Path: # Bare model name — search the recipe tree. name = path.stem if path.suffix else path.name - matches = sorted(_RECIPE_PATH.glob(f"**/{name}.yaml")) + matches = sorted([*_RECIPE_PATH.glob(f"**/{name}.yaml"), *_RECIPE_PATH.glob(f"**/{name}.yml")]) if not matches: msg = (