oracle-ads 2.13.12__py3-none-any.whl → 2.13.13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ads/aqua/model/model.py CHANGED
@@ -16,7 +16,7 @@ from oci.data_science.models import JobRun, Metadata, Model, UpdateModelDetails
16
16
 
17
17
  from ads.aqua import logger
18
18
  from ads.aqua.app import AquaApp
19
- from ads.aqua.common.entities import AquaMultiModelRef, LoraModuleSpec
19
+ from ads.aqua.common.entities import AquaMultiModelRef
20
20
  from ads.aqua.common.enums import (
21
21
  ConfigFolder,
22
22
  CustomInferenceContainerTypeFamily,
@@ -83,10 +83,7 @@ from ads.aqua.model.entities import (
83
83
  ModelValidationResult,
84
84
  )
85
85
  from ads.aqua.model.enums import MultiModelSupportedTaskType
86
- from ads.aqua.model.utils import (
87
- extract_base_model_from_ft,
88
- extract_fine_tune_artifacts_path,
89
- )
86
+ from ads.aqua.model.utils import extract_fine_tune_artifacts_path
90
87
  from ads.common.auth import default_signer
91
88
  from ads.common.oci_resource import SEARCH_TYPE, OCIResource
92
89
  from ads.common.utils import UNKNOWN, get_console_link, is_path_exists, read_file
@@ -242,6 +239,7 @@ class AquaModelApp(AquaApp):
242
239
  compartment_id: Optional[str] = None,
243
240
  freeform_tags: Optional[Dict] = None,
244
241
  defined_tags: Optional[Dict] = None,
242
+ source_models: Optional[Dict[str, DataScienceModel]] = None,
245
243
  **kwargs, # noqa: ARG002
246
244
  ) -> DataScienceModel:
247
245
  """
@@ -259,6 +257,10 @@ class AquaModelApp(AquaApp):
259
257
  Freeform tags for the model.
260
258
  defined_tags : Optional[Dict]
261
259
  Defined tags for the model.
260
+ source_models: Optional[Dict[str, DataScienceModel]]
261
+ A mapping of model OCIDs to their corresponding `DataScienceModel` objects.
262
+ This dictionary contains metadata for all models involved in the multi-model deployment,
263
+ including both base models and fine-tuned weights.
262
264
 
263
265
  Returns
264
266
  -------
@@ -295,76 +297,127 @@ class AquaModelApp(AquaApp):
295
297
 
296
298
  selected_models_deployment_containers = set()
297
299
 
300
+ if not source_models:
301
+ # Collect all unique model IDs (including fine-tuned models)
302
+ source_model_ids = list(
303
+ {model_id for model in models for model_id in model.all_model_ids()}
304
+ )
305
+ logger.debug(
306
+ "Fetching source model metadata for model IDs: %s", source_model_ids
307
+ )
308
+
309
+ # Fetch source model metadata
310
+ source_models = self.get_multi_source(source_model_ids) or {}
311
+
298
312
  # Process each model in the input list
299
313
  for model in models:
300
- # Retrieve model metadata from the Model Catalog using the model ID
301
- source_model = DataScienceModel.from_id(model.model_id)
302
- display_name = source_model.display_name
303
- model_file_description = source_model.model_file_description
304
- # If model_name is not explicitly provided, use the model's display name
305
- model.model_name = model.model_name or display_name
314
+ # Retrieve base model metadata
315
+ source_model: DataScienceModel = source_models.get(model.model_id)
316
+ if not source_model:
317
+ logger.error(
318
+ "Failed to fetch metadata for base model ID: %s", model.model_id
319
+ )
320
+ raise AquaValueError(
321
+ f"Unable to retrieve metadata for base model ID: {model.model_id}."
322
+ )
323
+
324
+ # Use display name as fallback if model name not provided
325
+ model.model_name = model.model_name or source_model.display_name
306
326
 
327
+ # Validate model file description
328
+ model_file_description = source_model.model_file_description
307
329
  if not model_file_description:
330
+ logger.error(
331
+ "Model '%s' (%s) has no file description.",
332
+ source_model.display_name,
333
+ model.model_id,
334
+ )
308
335
  raise AquaValueError(
309
336
  f"Model '{source_model.display_name}' (ID: {model.model_id}) has no file description. "
310
- "Please register the model first."
337
+ "Please register the model with a file description."
311
338
  )
312
339
 
313
- # Check if the model is a fine-tuned model based on its tags
314
- is_fine_tuned_model = (
315
- Tags.AQUA_FINE_TUNED_MODEL_TAG in source_model.freeform_tags
340
+ # Track model file description in a validated structure
341
+ model_file_description_list.append(
342
+ ModelFileDescription(**model_file_description)
316
343
  )
317
344
 
318
- base_model_artifact_path = ""
319
- fine_tune_path = ""
320
-
321
- if is_fine_tuned_model:
322
- # Extract artifact paths for the base and fine-tuned model
323
- base_model_artifact_path, fine_tune_path = (
324
- extract_fine_tune_artifacts_path(source_model)
345
+ # Ensure base model has a valid artifact
346
+ if not source_model.artifact:
347
+ logger.error(
348
+ "Base model '%s' (%s) has no artifact.",
349
+ model.model_name,
350
+ model.model_id,
351
+ )
352
+ raise AquaValueError(
353
+ f"Model '{model.model_name}' (ID: {model.model_id}) has no registered artifacts. "
354
+ "Please register the model before deployment."
325
355
  )
326
356
 
327
- # Create a single LoRA module specification for the fine-tuned model
328
- # TODO: Support multiple LoRA modules in the future
329
- model.fine_tune_weights = [
330
- LoraModuleSpec(
331
- model_id=model.model_id,
332
- model_name=model.model_name,
333
- model_path=fine_tune_path,
334
- )
335
- ]
357
+ # Set base model artifact path
358
+ model.artifact_location = source_model.artifact
359
+ logger.debug(
360
+ "Model '%s' artifact path set to: %s",
361
+ model.model_name,
362
+ model.artifact_location,
363
+ )
336
364
 
337
- # Use the LoRA module name as the model's display name
338
- display_name = model.model_name
365
+ display_name_list.append(model.model_name)
339
366
 
340
- # Temporarily override model ID and name with those of the base model
341
- # TODO: Revisit this logic once proper base/FT model handling is implemented
342
- model.model_id, model.model_name = extract_base_model_from_ft(
343
- source_model
367
+ # Extract model task metadata from source model
368
+ self._extract_model_task(model, source_model)
369
+
370
+ # Process fine-tuned weights if provided
371
+ for ft_model in model.fine_tune_weights or []:
372
+ fine_tune_source_model: DataScienceModel = source_models.get(
373
+ ft_model.model_id
344
374
  )
345
- else:
346
- # For base models, use the original artifact path
347
- base_model_artifact_path = source_model.artifact
348
- display_name = model.model_name
375
+ if not fine_tune_source_model:
376
+ logger.error(
377
+ "Failed to fetch metadata for fine-tuned model ID: %s",
378
+ ft_model.model_id,
379
+ )
380
+ raise AquaValueError(
381
+ f"Unable to retrieve metadata for fine-tuned model ID: {ft_model.model_id}."
382
+ )
349
383
 
350
- if not base_model_artifact_path:
351
- # Fail if no artifact is found for the base model model
352
- raise AquaValueError(
353
- f"Model '{model.model_name}' (ID: {model.model_id}) has no artifacts. "
354
- "Please register the model first."
384
+ # Validate model file description
385
+ ft_model_file_description = (
386
+ fine_tune_source_model.model_file_description
355
387
  )
388
+ if not ft_model_file_description:
389
+ logger.error(
390
+ "Model '%s' (%s) has no file description.",
391
+ fine_tune_source_model.display_name,
392
+ ft_model.model_id,
393
+ )
394
+ raise AquaValueError(
395
+ f"Model '{fine_tune_source_model.display_name}' (ID: {ft_model.model_id}) has no file description. "
396
+ "Please register the model with a file description."
397
+ )
356
398
 
357
- # Update the artifact path in the model configuration
358
- model.artifact_location = base_model_artifact_path
359
- display_name_list.append(display_name)
399
+ # Track model file description in a validated structure
400
+ model_file_description_list.append(
401
+ ModelFileDescription(**ft_model_file_description)
402
+ )
360
403
 
361
- # Extract model task metadata from source model
362
- self._extract_model_task(model, source_model)
404
+ # Extract fine-tuned model path
405
+ _, fine_tune_path = extract_fine_tune_artifacts_path(
406
+ fine_tune_source_model
407
+ )
408
+ logger.debug(
409
+ "Resolved fine-tuned model path for '%s': %s",
410
+ ft_model.model_id,
411
+ fine_tune_path,
412
+ )
413
+ ft_model.model_path = fine_tune_path
363
414
 
364
- # Track model file description in a validated structure
365
- model_file_description_list.append(
366
- ModelFileDescription(**model_file_description)
367
- )
415
+ # Use fallback name if needed
416
+ ft_model.model_name = (
417
+ ft_model.model_name or fine_tune_source_model.display_name
418
+ )
419
+
420
+ display_name_list.append(ft_model.model_name)
368
421
 
369
422
  # Validate deployment container consistency
370
423
  deployment_container = source_model.custom_metadata_list.get(
@@ -375,9 +428,15 @@ class AquaModelApp(AquaApp):
375
428
  ).value
376
429
 
377
430
  if deployment_container not in supported_container_families:
431
+ logger.error(
432
+ "Unsupported deployment container '%s' for model '%s'. Supported: %s",
433
+ deployment_container,
434
+ source_model.id,
435
+ supported_container_families,
436
+ )
378
437
  raise AquaValueError(
379
438
  f"Unsupported deployment container '{deployment_container}' for model '{source_model.id}'. "
380
- f"Only '{supported_container_families}' are supported for multi-model deployments."
439
+ f"Only {supported_container_families} are supported for multi-model deployments."
381
440
  )
382
441
 
383
442
  selected_models_deployment_containers.add(deployment_container)
@@ -4,6 +4,7 @@
4
4
 
5
5
 
6
6
  import json
7
+ import re
7
8
  import shlex
8
9
  import threading
9
10
  from datetime import datetime, timedelta
@@ -47,7 +48,11 @@ from ads.aqua.constants import (
47
48
  )
48
49
  from ads.aqua.data import AquaResourceIdentifier
49
50
  from ads.aqua.model import AquaModelApp
50
- from ads.aqua.model.constants import AquaModelMetadataKeys, ModelCustomMetadataFields
51
+ from ads.aqua.model.constants import (
52
+ AquaModelMetadataKeys,
53
+ ModelCustomMetadataFields,
54
+ ModelTask,
55
+ )
51
56
  from ads.aqua.model.utils import (
52
57
  extract_base_model_from_ft,
53
58
  extract_fine_tune_artifacts_path,
@@ -214,20 +219,52 @@ class AquaDeploymentApp(AquaApp):
214
219
  freeform_tags=freeform_tags,
215
220
  defined_tags=defined_tags,
216
221
  )
222
+ task_tag = aqua_model.freeform_tags.get(Tags.TASK, UNKNOWN)
223
+ if (
224
+ task_tag == ModelTask.TIME_SERIES_FORECASTING
225
+ or task_tag == ModelTask.TIME_SERIES_FORECASTING.replace("-", "_")
226
+ ):
227
+ create_deployment_details.env_var.update(
228
+ {Tags.TASK.upper(): ModelTask.TIME_SERIES_FORECASTING}
229
+ )
217
230
  return self._create(
218
231
  aqua_model=aqua_model,
219
232
  create_deployment_details=create_deployment_details,
220
233
  container_config=container_config,
221
234
  )
222
235
  else:
223
- model_ids = [model.model_id for model in create_deployment_details.models]
236
+ # Collect all unique model IDs (including fine-tuned models)
237
+ source_model_ids = list(
238
+ {
239
+ model_id
240
+ for model in create_deployment_details.models
241
+ for model_id in model.all_model_ids()
242
+ }
243
+ )
244
+ logger.debug(
245
+ "Fetching source model metadata for model IDs: %s", source_model_ids
246
+ )
247
+ # Fetch source model metadata
248
+ source_models = self.get_multi_source(source_model_ids) or {}
249
+
250
+ try:
251
+ create_deployment_details.validate_input_models(
252
+ model_details=source_models
253
+ )
254
+ except ConfigValidationError as err:
255
+ raise AquaValueError(f"{err}") from err
256
+
257
+ base_model_ids = [
258
+ model.model_id for model in create_deployment_details.models
259
+ ]
224
260
 
225
261
  try:
226
262
  model_config_summary = self.get_multimodel_deployment_config(
227
- model_ids=model_ids, compartment_id=compartment_id
263
+ model_ids=base_model_ids, compartment_id=compartment_id
228
264
  )
229
265
  if not model_config_summary.gpu_allocation:
230
266
  raise AquaValueError(model_config_summary.error_message)
267
+
231
268
  create_deployment_details.validate_multimodel_deployment_feasibility(
232
269
  models_config_summary=model_config_summary
233
270
  )
@@ -298,7 +335,7 @@ class AquaDeploymentApp(AquaApp):
298
335
  )
299
336
 
300
337
  logger.debug(
301
- f"Multi models ({model_ids}) provided. Delegating to multi model creation method."
338
+ f"Multi models ({source_model_ids}) provided. Delegating to multi model creation method."
302
339
  )
303
340
 
304
341
  aqua_model = model_app.create_multi(
@@ -307,6 +344,7 @@ class AquaDeploymentApp(AquaApp):
307
344
  project_id=project_id,
308
345
  freeform_tags=freeform_tags,
309
346
  defined_tags=defined_tags,
347
+ source_models=source_models,
310
348
  )
311
349
  return self._create_multi(
312
350
  aqua_model=aqua_model,
@@ -727,14 +765,16 @@ class AquaDeploymentApp(AquaApp):
727
765
  ).deploy(wait_for_completion=False)
728
766
 
729
767
  deployment_id = deployment.id
768
+
730
769
  logger.info(
731
770
  f"Aqua model deployment {deployment_id} created for model {aqua_model_id}. Work request Id is {deployment.dsc_model_deployment.workflow_req_id}"
732
771
  )
772
+ status_list = []
733
773
 
734
774
  progress_thread = threading.Thread(
735
775
  target=self.get_deployment_status,
736
776
  args=(
737
- deployment_id,
777
+ deployment,
738
778
  deployment.dsc_model_deployment.workflow_req_id,
739
779
  model_type,
740
780
  model_name,
@@ -822,12 +862,22 @@ class AquaDeploymentApp(AquaApp):
822
862
  )
823
863
 
824
864
  if oci_aqua:
865
+ # skipping the AQUA model deployments that are created from model group
866
+ # TODO: remove this checker after AQUA deployment is integrated with model group
867
+ aqua_model_id = model_deployment.freeform_tags.get(
868
+ Tags.AQUA_MODEL_ID_TAG, UNKNOWN
869
+ )
870
+ if (
871
+ "datasciencemodelgroup" in aqua_model_id
872
+ or model_deployment.model_deployment_configuration_details.deployment_type
873
+ == "UNKNOWN_ENUM_VALUE"
874
+ ):
875
+ continue
825
876
  results.append(
826
877
  AquaDeployment.from_oci_model_deployment(
827
878
  model_deployment, self.region
828
879
  )
829
880
  )
830
-
831
881
  # log telemetry if MD is in active or failed state
832
882
  deployment_id = model_deployment.id
833
883
  state = model_deployment.lifecycle_state.upper()
@@ -1233,7 +1283,7 @@ class AquaDeploymentApp(AquaApp):
1233
1283
 
1234
1284
  def get_deployment_status(
1235
1285
  self,
1236
- model_deployment_id: str,
1286
+ deployment: ModelDeployment,
1237
1287
  work_request_id: str,
1238
1288
  model_type: str,
1239
1289
  model_name: str,
@@ -1255,13 +1305,10 @@ class AquaDeploymentApp(AquaApp):
1255
1305
  AquaDeployment
1256
1306
  An Aqua deployment instance.
1257
1307
  """
1258
- ocid = get_ocid_substring(model_deployment_id, key_len=8)
1259
- telemetry_kwargs = {"ocid": ocid}
1260
-
1308
+ ocid = get_ocid_substring(deployment.id, key_len=8)
1261
1309
  data_science_work_request: DataScienceWorkRequest = DataScienceWorkRequest(
1262
1310
  work_request_id
1263
1311
  )
1264
-
1265
1312
  try:
1266
1313
  data_science_work_request.wait_work_request(
1267
1314
  progress_bar_description="Creating model deployment",
@@ -1269,23 +1316,49 @@ class AquaDeploymentApp(AquaApp):
1269
1316
  poll_interval=DEFAULT_POLL_INTERVAL,
1270
1317
  )
1271
1318
  except Exception:
1319
+ status = ""
1320
+ logs = deployment.show_logs().sort_values(by="time", ascending=False)
1321
+
1322
+ if logs and len(logs) > 0:
1323
+ status = logs.iloc[0]["message"]
1324
+
1325
+ status = re.sub(r"[^a-zA-Z0-9]", " ", status)
1326
+
1272
1327
  if data_science_work_request._error_message:
1273
1328
  error_str = ""
1274
1329
  for error in data_science_work_request._error_message:
1275
1330
  error_str = error_str + " " + error.message
1276
1331
 
1277
- self.telemetry.record_event(
1278
- category=f"aqua/{model_type}/deployment/status",
1279
- action="FAILED",
1280
- detail=error_str,
1281
- value=model_name,
1282
- **telemetry_kwargs,
1283
- )
1332
+ error_str = re.sub(r"[^a-zA-Z0-9]", " ", error_str)
1333
+ telemetry_kwargs = {
1334
+ "ocid": ocid,
1335
+ "model_name": model_name,
1336
+ "work_request_error": error_str,
1337
+ "status": status,
1338
+ }
1339
+
1340
+ self.telemetry.record_event(
1341
+ category=f"aqua/{model_type}/deployment/status",
1342
+ action="FAILED",
1343
+ **telemetry_kwargs,
1344
+ )
1345
+ else:
1346
+ telemetry_kwargs = {
1347
+ "ocid": ocid,
1348
+ "model_name": model_name,
1349
+ "status": status,
1350
+ }
1351
+
1352
+ self.telemetry.record_event(
1353
+ category=f"aqua/{model_type}/deployment/status",
1354
+ action="FAILED",
1355
+ **telemetry_kwargs,
1356
+ )
1284
1357
 
1285
1358
  else:
1286
- self.telemetry.record_event_async(
1359
+ telemetry_kwargs = {"ocid": ocid, "model_name": model_name}
1360
+ self.telemetry.record_event(
1287
1361
  category=f"aqua/{model_type}/deployment/status",
1288
1362
  action="SUCCEEDED",
1289
- value=model_name,
1290
1363
  **telemetry_kwargs,
1291
1364
  )
@@ -13,12 +13,15 @@ from ads.aqua.common.enums import Tags
13
13
  from ads.aqua.config.utils.serializer import Serializable
14
14
  from ads.aqua.constants import UNKNOWN_DICT
15
15
  from ads.aqua.data import AquaResourceIdentifier
16
+ from ads.aqua.finetuning.constants import FineTuneCustomMetadata
16
17
  from ads.aqua.modeldeployment.config_loader import (
17
18
  ConfigurationItem,
18
19
  ModelDeploymentConfigSummary,
19
20
  )
20
21
  from ads.common.serializer import DataClassSerializable
21
22
  from ads.common.utils import UNKNOWN, get_console_link
23
+ from ads.model.datascience_model import DataScienceModel
24
+ from ads.model.model_metadata import ModelCustomMetadataItem
22
25
 
23
26
 
24
27
  class ConfigValidationError(Exception):
@@ -474,6 +477,149 @@ class CreateModelDeploymentDetails(BaseModel):
474
477
  logger.error(error_message)
475
478
  raise ConfigValidationError(error_message)
476
479
 
480
+ def validate_input_models(self, model_details: Dict[str, DataScienceModel]) -> None:
481
+ """
482
+ Validates the input models for a multi-model deployment configuration.
483
+
484
+ Validation Criteria:
485
+ - The base model must be explicitly provided.
486
+ - The base model must be in 'ACTIVE' state.
487
+ - Fine-tuned model IDs must refer to valid, tagged fine-tuned models.
488
+ - Fine-tuned models must refer back to the same base model.
489
+ - All model names (including fine-tuned variants) must be unique.
490
+
491
+ Parameters
492
+ ----------
493
+ model_details : Dict[str, DataScienceModel]
494
+ Dictionary mapping model OCIDs to DataScienceModel instances.
495
+ Includes the all models to validate including fine-tuned models.
496
+
497
+ Raises
498
+ ------
499
+ ConfigValidationError
500
+ If any of the above conditions are violated.
501
+ """
502
+ if not self.models:
503
+ logger.error("Validation failed: No models specified in the model group.")
504
+ raise ConfigValidationError(
505
+ "Multi-model deployment requires at least one model entry. "
506
+ "Please provide a base model in the `models` list."
507
+ )
508
+
509
+ seen_names = set()
510
+ duplicate_names = set()
511
+
512
+ for model in self.models:
513
+ base_model_id = model.model_id
514
+ base_model = model_details.get(base_model_id)
515
+
516
+ if not base_model:
517
+ logger.error(
518
+ "Validation failed: Base model ID '%s' not found.", base_model_id
519
+ )
520
+ raise ConfigValidationError(f"Model not found: '{base_model_id}'.")
521
+
522
+ if Tags.AQUA_FINE_TUNED_MODEL_TAG in (base_model.freeform_tags or {}):
523
+ logger.error(
524
+ "Validation failed: Base model ID '%s' is a fine-tuned model.",
525
+ base_model_id,
526
+ )
527
+ raise ConfigValidationError(
528
+ f"Invalid base model ID '{base_model_id}'. "
529
+ "Specify a base model OCID in the `models` input, not a fine-tuned model."
530
+ )
531
+
532
+ if base_model.lifecycle_state != "ACTIVE":
533
+ logger.error(
534
+ "Validation failed: Base model '%s' is in state '%s'.",
535
+ base_model_id,
536
+ base_model.lifecycle_state,
537
+ )
538
+ raise ConfigValidationError(
539
+ f"Invalid base model ID '{base_model_id}': must be in ACTIVE state."
540
+ )
541
+
542
+ # Normalize and validate model name uniqueness
543
+ model_name = model.model_name or base_model.display_name
544
+ if model_name in seen_names:
545
+ duplicate_names.add(model_name)
546
+ else:
547
+ seen_names.add(model_name)
548
+
549
+ for lora_module in model.fine_tune_weights or []:
550
+ ft_model_id = lora_module.model_id
551
+ ft_model = model_details.get(ft_model_id)
552
+
553
+ if not ft_model:
554
+ logger.error(
555
+ "Validation failed: Fine-tuned model ID '%s' not found.",
556
+ ft_model_id,
557
+ )
558
+ raise ConfigValidationError(
559
+ f"Fine-tuned model not found: '{ft_model_id}'."
560
+ )
561
+
562
+ if ft_model.lifecycle_state != "ACTIVE":
563
+ logger.error(
564
+ "Validation failed: Fine-tuned model '%s' is in state '%s'.",
565
+ ft_model_id,
566
+ ft_model.lifecycle_state,
567
+ )
568
+ raise ConfigValidationError(
569
+ f"Invalid Fine-tuned model ID '{ft_model_id}': must be in ACTIVE state."
570
+ )
571
+
572
+ if Tags.AQUA_FINE_TUNED_MODEL_TAG not in (ft_model.freeform_tags or {}):
573
+ logger.error(
574
+ "Validation failed: Model ID '%s' is missing tag '%s'.",
575
+ ft_model_id,
576
+ Tags.AQUA_FINE_TUNED_MODEL_TAG,
577
+ )
578
+ raise ConfigValidationError(
579
+ f"Invalid fine-tuned model ID '{ft_model_id}': missing tag '{Tags.AQUA_FINE_TUNED_MODEL_TAG}'."
580
+ )
581
+
582
+ ft_base_model_id = ft_model.custom_metadata_list.get(
583
+ FineTuneCustomMetadata.FINE_TUNE_SOURCE,
584
+ ModelCustomMetadataItem(
585
+ key=FineTuneCustomMetadata.FINE_TUNE_SOURCE
586
+ ),
587
+ ).value
588
+
589
+ if ft_base_model_id != base_model_id:
590
+ logger.error(
591
+ "Validation failed: Fine-tuned model '%s' is linked to base model '%s' (expected '%s').",
592
+ ft_model_id,
593
+ ft_base_model_id,
594
+ base_model_id,
595
+ )
596
+ raise ConfigValidationError(
597
+ f"Fine-tuned model '{ft_model_id}' belongs to base model '{ft_base_model_id}', "
598
+ f"but was included under base model '{base_model_id}'."
599
+ )
600
+
601
+ # Validate fine-tuned model name uniqueness
602
+ lora_model_name = lora_module.model_name or ft_model.display_name
603
+ if lora_model_name in seen_names:
604
+ duplicate_names.add(lora_model_name)
605
+ else:
606
+ seen_names.add(lora_model_name)
607
+
608
+ logger.debug(
609
+ "Validated fine-tuned model '%s' under base model '%s'.",
610
+ ft_model_id,
611
+ base_model_id,
612
+ )
613
+
614
+ if duplicate_names:
615
+ logger.error(
616
+ "Duplicate model names detected: %s", ", ".join(sorted(duplicate_names))
617
+ )
618
+ raise ConfigValidationError(
619
+ f"The following model names are duplicated across base and fine-tuned models: "
620
+ f"{', '.join(sorted(duplicate_names))}. Model names must be unique for proper routing in multi-model deployments."
621
+ )
622
+
477
623
  class Config:
478
624
  extra = "allow"
479
625
  protected_namespaces = ()
@@ -81,14 +81,12 @@ class BaseModelSpec(BaseModel):
81
81
  unique_modules: List[LoraModuleSpec] = []
82
82
 
83
83
  for module in fine_tune_weights or []:
84
- name = getattr(module, "model_name", None)
85
- if not name:
86
- logger.warning("Fine-tuned model in AquaMultiModelRef is missing model_name.")
87
- continue
88
- if name in seen:
89
- logger.warning(f"Duplicate LoRA Module detected: {name!r} (skipping duplicate).")
84
+ if module.model_name and module.model_name in seen:
85
+ logger.warning(
86
+ f"Duplicate LoRA Module detected: {module.model_name!r} (skipping duplicate)."
87
+ )
90
88
  continue
91
- seen.add(name)
89
+ seen.add(module.model_name)
92
90
  unique_modules.append(module)
93
91
 
94
92
  return unique_modules
@@ -169,17 +167,12 @@ class ModelGroupConfig(Serializable):
169
167
  model, container_params, container_type_key
170
168
  )
171
169
 
172
- model_id = (
173
- model.fine_tune_weights[0].model_id
174
- if model.fine_tune_weights
175
- else model.model_id
176
- )
177
-
178
170
  deployment_config = model_config_summary.deployment_config.get(
179
- model_id, AquaDeploymentConfig()
171
+ model.model_id, AquaDeploymentConfig()
180
172
  ).configuration.get(
181
173
  create_deployment_details.instance_shape, ConfigurationItem()
182
174
  )
175
+
183
176
  params_found = False
184
177
  for item in deployment_config.multi_model_deployment:
185
178
  if model.gpu_count and item.gpu_count and item.gpu_count == model.gpu_count:
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*--
3
+
4
+ # Copyright (c) 2024 Oracle and/or its affiliates.
5
+ # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
6
+ from ads.aqua.verify_policies.verify import AquaVerifyPoliciesApp
7
+
8
+ __all__ = ["AquaVerifyPoliciesApp"]