77from dataclasses import dataclass
88from datetime import UTC , datetime
99from pathlib import Path
10- from typing import cast
10+ from typing import Any , cast
1111from uuid import UUID
1212
1313import polars as pl
1414import yaml
1515from datumaro .experimental import Dataset
1616from datumaro .experimental .fields import Subset
1717from getitune import TaskType
18- from getitune .backend .lightning .engine import LightningEngine
1918from getitune .backend .lightning .models .base import DataInputParams , LightningModel
2019from getitune .backend .openvino .engine import OVEngine
20+ from getitune .backend .ultralytics .models .base import UltralyticsModel
2121from getitune .config .data import SamplerConfig , SubsetConfig
2222from getitune .data .dataset .base import VisionDataset
2323from getitune .data .entity .utils import detect_storage_dtype
2424from getitune .data .factory import TransformLibFactory
2525from getitune .data .module import DataModule
26+ from getitune .engine import create_engine
27+ from getitune .engine .engine import Engine
2628from getitune .types .device import DeviceType as GetiTuneDeviceType
2729from getitune .types .export import ExportFormat
2830from getitune .types .precision import Precision
@@ -171,7 +173,12 @@ def prepare_weights(self, training_params: TrainingJobParams) -> Path:
171173 "Can't start training - the parent revision has no variants (it may have failed). "
172174 "Review the previous revision and retry."
173175 )
174- parent_pytorch_variant = next (v for v in parent_variants if v .format == ModelFormat .PYTORCH )
176+ parent_pytorch_variant = next ((v for v in parent_variants if v .format == ModelFormat .PYTORCH ), None )
177+ if parent_pytorch_variant is None :
178+ raise ExecutionErr (
179+ "Can't start training - the parent revision has no PyTorch variant. "
180+ "Review the previous revision and retry."
181+ )
175182 weights_path = self .__build_model_weights_path (
176183 self ._data_dir , project_id , parent_model_revision_id , parent_pytorch_variant .id
177184 )
@@ -384,20 +391,17 @@ def train_model(
384391 weights_path : Path ,
385392 model_id : UUID ,
386393 device : DeviceInfo ,
387- has_parent_revision : bool ,
388- ) -> tuple [Path , LightningEngine ]:
389- """Execute model training."""
390- # TODO use weights path to initialize model from pre-downloaded weights
391- # after resolving https://github.com/open-edge-platform/training_extensions/issues/5100
392- logger .warning (
393- "Argument 'weights_path' (value='{}') is not used in model training yet; "
394- "the weights location will be determined internally by getitune" ,
395- weights_path ,
396- )
397-
398- # Build the DataModule
394+ ) -> tuple [Path , Engine ]:
395+ """Execute model training.
396+
397+ Instantiates the model from *training_config* with jsonargparse and
398+ creates the engine through ``create_engine`` from the library.
399+ Lightning callbacks are built when a ``callbacks`` section is present
400+ in the config; backends that do not support them simply ignore the
401+ parameter.
402+ """
399403 logger .info ("Preparing the DataModule for training (model_id={})" , model_id )
400- getitune_datamodule = DataModule .from_vision_datasets (
404+ datamodule = DataModule .from_vision_datasets (
401405 train_dataset = dataset_info .getitune_training_dataset ,
402406 val_dataset = dataset_info .getitune_validation_dataset ,
403407 test_dataset = dataset_info .getitune_testing_dataset ,
@@ -406,36 +410,36 @@ def train_model(
406410 test_subset = dataset_info .getitune_testing_subset_config ,
407411 )
408412
409- # Create the LightningModel according to the training configuration
410- logger .info ("Instantiating the LightningModel for training (model_id={})" , model_id )
413+ logger .info ("Instantiating model for training (model_id={})" , model_id )
411414 model_cfg = training_config ["model" ]
412- model_cfg ["init_args" ]["label_info" ] = getitune_datamodule .label_info .label_names
415+ model_cfg ["init_args" ]["label_info" ] = datamodule .label_info .label_names
413416 model_cfg ["init_args" ]["data_input_params" ] = DataInputParams (
414- input_size = cast (tuple [int , int ], getitune_datamodule .input_size ),
415- mean = getitune_datamodule .input_mean if getitune_datamodule .input_mean is not None else (0.0 , 0.0 , 0.0 ),
416- std = getitune_datamodule .input_std if getitune_datamodule .input_std is not None else (1.0 , 1.0 , 1.0 ),
417- intensity_config = getitune_datamodule .input_intensity_config ,
417+ input_size = cast (tuple [int , int ], datamodule .input_size ),
418+ mean = datamodule .input_mean if datamodule .input_mean is not None else (0.0 , 0.0 , 0.0 ),
419+ std = datamodule .input_std if datamodule .input_std is not None else (1.0 , 1.0 , 1.0 ),
420+ intensity_config = datamodule .input_intensity_config ,
418421 ).as_dict ()
419- model_parser = ArgumentParser ()
420- model_parser .add_subclass_arguments (LightningModel , "model" , required = False , fail_untyped = False )
421- getitune_model : LightningModel = model_parser .instantiate_classes (Namespace (model = model_cfg )).get ("model" )
422- if hasattr (getitune_model , "tile_config" ):
423- getitune_model .tile_config = getitune_datamodule .tile_config
424-
425- # Set up the LightningEngine
426- logger .info ("Initializing the LightningEngine for training (model_id={})" , model_id )
422+ logger .info ("Initializing engine for training (model_id={})" , model_id )
427423 getitune_device_type = (
428424 GetiTuneDeviceType .gpu if device .type is DeviceType .CUDA else GetiTuneDeviceType (device .type )
429425 )
430- getitune_engine = LightningEngine (
431- model = getitune_model ,
432- data = getitune_datamodule ,
433- checkpoint = weights_path if has_parent_revision else None ,
434- work_dir = self ._data_dir / f"getitune-workspace-{ model_id } " ,
435- device = getitune_device_type ,
436- )
426+ engine_kwargs : dict [str , Any ] = {
427+ "work_dir" : self ._data_dir / f"getitune-workspace-{ model_id } " ,
428+ "device" : getitune_device_type ,
429+ "checkpoint" : weights_path ,
430+ }
431+
432+ model_parser = ArgumentParser ()
433+ class_path = model_cfg .get ("class_path" , "" )
434+ model_type = UltralyticsModel if "ultralytics" in class_path else LightningModel
435+ model_parser .add_argument ("--model" , type = model_type )
436+ getitune_model = model_parser .instantiate_classes (Namespace (model = model_cfg )).get ("model" )
437+
438+ if hasattr (getitune_model , "tile_config" ):
439+ getitune_model .tile_config = datamodule .tile_config
440+
441+ getitune_engine = create_engine (model = getitune_model , data = datamodule , ** engine_kwargs )
437442
438- # Set up the callbacks
439443 callbacks_cfg = training_config .get ("callbacks" , [])
440444 for cb_cfg in callbacks_cfg :
441445 if "init_args" in cb_cfg and "dirpath" in cb_cfg ["init_args" ]:
@@ -446,25 +450,35 @@ def train_model(
446450 callbacks_list = parser .instantiate_classes (parsed_callbacks_cfg ).get ("callbacks" , [])
447451 callbacks_list .append (TrainingProgressCallback (self .update_progress , min_p = 10 , max_p = 80 ))
448452
449- # Start training
450- logger .info ("Starting the training loop (model_id={})" , model_id )
451- train_kwargs = {
453+ logger .info ("Starting training loop (model_id={})" , model_id )
454+ train_kwargs : dict [str , Any ] = {
452455 "max_epochs" : training_config ["max_epochs" ],
453456 "callbacks" : callbacks_list ,
454457 }
455- if device .type is not DeviceType .CPU and device .index :
458+ if device .type is not DeviceType .CPU and device .index is not None :
456459 train_kwargs ["devices" ] = [device .index ]
457460 if "precision" in training_config :
458461 train_kwargs ["precision" ] = training_config ["precision" ]
462+ # Forward backend-specific training args (e.g. patience for Ultralytics).
463+ if "training" in training_config :
464+ train_kwargs .update (training_config ["training" ])
459465 getitune_engine .train (** train_kwargs ) # pyrefly: ignore[bad-argument-type]
460- trained_model_path = Path (getitune_engine .work_dir ) / "best_checkpoint.ckpt"
466+
467+ trained_model_path = getitune_engine .best_checkpoint
468+ if trained_model_path is None :
469+ raise RuntimeError (
470+ f"Training completed but no best checkpoint was produced "
471+ f"by the engine ({ type (getitune_engine ).__name__ } )."
472+ )
473+ if not trained_model_path .exists ():
474+ raise FileNotFoundError (f"Trained checkpoint not found at { trained_model_path } " )
461475 logger .info ("Model training completed. Trained model saved at {}" , trained_model_path )
462476 return trained_model_path , getitune_engine
463477
464478 @step ("Evaluate Model" , 95 )
465479 def evaluate_model (
466480 self ,
467- getitune_engine : LightningEngine ,
481+ getitune_engine : Engine ,
468482 task : Task ,
469483 model_revision_id : UUID ,
470484 model_variants : list [ModelVariantDescriptor ],
@@ -533,7 +547,7 @@ def _save_evaluation_result(
533547 )
534548
535549 @step ("Export Model" )
536- def export_model (self , getitune_engine : LightningEngine , model_checkpoint_path : Path ) -> ExportedModels :
550+ def export_model (self , getitune_engine : Engine , model_checkpoint_path : Path ) -> ExportedModels :
537551 """Export the trained model to desired OpenVINO and ONNX formats"""
538552 logger .info ("Exporting the model to OpenVINO format (FP16 precision)..." )
539553 exported_ov_model_path = getitune_engine .export (
@@ -578,10 +592,9 @@ def store_model_artifacts(
578592 variants_dir = model_dir / "variants"
579593 variants_dir .mkdir (parents = True , exist_ok = True )
580594
581- # Copy PyTorch checkpoint
582595 pytorch_variant_dir = variants_dir / str (created_variants [ModelFormat .PYTORCH ])
583596 pytorch_variant_dir .mkdir (parents = True , exist_ok = True )
584- shutil .copyfile (trained_model_path , pytorch_variant_dir / "model.ckpt " )
597+ shutil .copyfile (trained_model_path , pytorch_variant_dir / "model.pt " )
585598 logger .info ("Stored PyTorch variant at {}" , pytorch_variant_dir )
586599
587600 # Copy OpenVINO IR files
@@ -595,6 +608,9 @@ def store_model_artifacts(
595608 exported_model_paths .openvino_model_path .with_suffix (".bin" ),
596609 openvino_variant_dir / "model.bin" ,
597610 )
611+ ov_metadata = exported_model_paths .openvino_model_path .parent / "metadata.yaml"
612+ if ov_metadata .exists ():
613+ shutil .copyfile (ov_metadata , openvino_variant_dir / "metadata.yaml" )
598614 logger .info ("Stored OpenVINO variant at {}" , openvino_variant_dir )
599615
600616 # Copy ONNX file
@@ -604,14 +620,16 @@ def store_model_artifacts(
604620 exported_model_paths .onnx_model_path .with_suffix (".onnx" ),
605621 onnx_variant_dir / "model.onnx" ,
606622 )
623+ onnx_metadata = exported_model_paths .onnx_model_path .parent / "metadata.yaml"
624+ if onnx_metadata .exists ():
625+ shutil .copyfile (onnx_metadata , onnx_variant_dir / "metadata.yaml" )
607626 logger .info ("Stored ONNX variant at {}" , onnx_variant_dir )
608627
609628 # Store the metrics
610629 metrics_source_path = getitune_work_dir / "csv"
611630 metrics_dest_path = model_dir / "metrics"
612631 if metrics_source_path .exists ():
613632 shutil .move (metrics_source_path , metrics_dest_path )
614- logger .info ("Stored training metrics at {}" , metrics_dest_path )
615633
616634 def create_model_variants (self , model_revision_id : UUID ) -> dict [ModelFormat , UUID ]:
617635 """Create variant records in the database for all exported formats.
@@ -680,7 +698,6 @@ def execute(self, params: TrainingJobParams) -> None:
680698 weights_path = weights_path ,
681699 model_id = params .model_id ,
682700 device = params .device ,
683- has_parent_revision = params .parent_model_revision_id is not None ,
684701 )
685702 exported_model_paths = self .export_model (
686703 getitune_engine = getitune_engine , model_checkpoint_path = trained_model_path
@@ -849,9 +866,10 @@ def __base_model_path(data_dir: Path, project_id: UUID, model_id: UUID) -> Path:
849866
850867 @classmethod
851868 def __build_model_weights_path (cls , data_dir : Path , project_id : UUID , model_id : UUID , model_variant : UUID ) -> Path :
852- """Get the path to the PyTorch checkpoint from a model's variants directory ."""
869+ """Get the path to the stored PyTorch checkpoint."""
853870 model_dir = cls .__base_model_path (data_dir , project_id , model_id )
854- return model_dir / "variants" / str (model_variant ) / "model.ckpt"
871+ variant_dir = model_dir / "variants" / str (model_variant )
872+ return variant_dir / "model.pt"
855873
856874 @classmethod
857875 def __build_model_config_path (cls , data_dir : Path , project_id : UUID , model_id : UUID ) -> Path :
0 commit comments