oracle-ads 2.10.1__py3-none-any.whl → 2.11.0__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/__init__.py +12 -0
- ads/aqua/base.py +324 -0
- ads/aqua/cli.py +19 -0
- ads/aqua/config/deployment_config_defaults.json +9 -0
- ads/aqua/config/resource_limit_names.json +7 -0
- ads/aqua/constants.py +45 -0
- ads/aqua/data.py +40 -0
- ads/aqua/decorator.py +101 -0
- ads/aqua/deployment.py +643 -0
- ads/aqua/dummy_data/icon.txt +1 -0
- ads/aqua/dummy_data/oci_model_deployments.json +56 -0
- ads/aqua/dummy_data/oci_models.json +1 -0
- ads/aqua/dummy_data/readme.md +26 -0
- ads/aqua/evaluation.py +1751 -0
- ads/aqua/exception.py +82 -0
- ads/aqua/extension/__init__.py +40 -0
- ads/aqua/extension/base_handler.py +138 -0
- ads/aqua/extension/common_handler.py +21 -0
- ads/aqua/extension/deployment_handler.py +202 -0
- ads/aqua/extension/evaluation_handler.py +135 -0
- ads/aqua/extension/finetune_handler.py +66 -0
- ads/aqua/extension/model_handler.py +59 -0
- ads/aqua/extension/ui_handler.py +201 -0
- ads/aqua/extension/utils.py +23 -0
- ads/aqua/finetune.py +579 -0
- ads/aqua/job.py +29 -0
- ads/aqua/model.py +819 -0
- ads/aqua/training/__init__.py +4 -0
- ads/aqua/training/exceptions.py +459 -0
- ads/aqua/ui.py +453 -0
- ads/aqua/utils.py +715 -0
- ads/cli.py +37 -6
- ads/common/decorator/__init__.py +7 -3
- ads/common/decorator/require_nonempty_arg.py +65 -0
- ads/common/object_storage_details.py +166 -7
- ads/common/oci_client.py +18 -1
- ads/common/oci_logging.py +2 -2
- ads/common/oci_mixin.py +4 -5
- ads/common/serializer.py +34 -5
- ads/common/utils.py +75 -10
- ads/config.py +40 -1
- ads/jobs/ads_job.py +43 -25
- ads/jobs/builders/infrastructure/base.py +4 -2
- ads/jobs/builders/infrastructure/dsc_job.py +49 -39
- ads/jobs/builders/runtimes/base.py +71 -1
- ads/jobs/builders/runtimes/container_runtime.py +4 -4
- ads/jobs/builders/runtimes/pytorch_runtime.py +10 -63
- ads/jobs/templates/driver_pytorch.py +27 -10
- ads/model/artifact_downloader.py +84 -14
- ads/model/artifact_uploader.py +25 -23
- ads/model/datascience_model.py +388 -38
- ads/model/deployment/model_deployment.py +10 -2
- ads/model/generic_model.py +8 -0
- ads/model/model_file_description_schema.json +68 -0
- ads/model/model_metadata.py +1 -1
- ads/model/service/oci_datascience_model.py +34 -5
- ads/opctl/operator/lowcode/anomaly/README.md +2 -1
- ads/opctl/operator/lowcode/anomaly/__main__.py +10 -4
- ads/opctl/operator/lowcode/anomaly/environment.yaml +2 -1
- ads/opctl/operator/lowcode/anomaly/model/automlx.py +12 -6
- ads/opctl/operator/lowcode/forecast/README.md +3 -2
- ads/opctl/operator/lowcode/forecast/environment.yaml +3 -2
- ads/opctl/operator/lowcode/forecast/model/automlx.py +12 -23
- ads/telemetry/base.py +62 -0
- ads/telemetry/client.py +105 -0
- ads/telemetry/telemetry.py +6 -3
- {oracle_ads-2.10.1.dist-info → oracle_ads-2.11.0.dist-info}/METADATA +37 -7
- {oracle_ads-2.10.1.dist-info → oracle_ads-2.11.0.dist-info}/RECORD +71 -36
- {oracle_ads-2.10.1.dist-info → oracle_ads-2.11.0.dist-info}/LICENSE.txt +0 -0
- {oracle_ads-2.10.1.dist-info → oracle_ads-2.11.0.dist-info}/WHEEL +0 -0
- {oracle_ads-2.10.1.dist-info → oracle_ads-2.11.0.dist-info}/entry_points.txt +0 -0
ads/aqua/deployment.py
ADDED
@@ -0,0 +1,643 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
# Copyright (c) 2024 Oracle and/or its affiliates.
|
4
|
+
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
|
5
|
+
|
6
|
+
import json
|
7
|
+
import logging
|
8
|
+
from dataclasses import dataclass, field, asdict
|
9
|
+
from typing import Dict, List, Union
|
10
|
+
|
11
|
+
import requests
|
12
|
+
from oci.data_science.models import ModelDeployment, ModelDeploymentSummary
|
13
|
+
|
14
|
+
from ads.aqua.base import AquaApp, logger
|
15
|
+
from ads.aqua.exception import AquaRuntimeError, AquaValueError
|
16
|
+
from ads.aqua.model import AquaModelApp, Tags
|
17
|
+
from ads.aqua.utils import (
|
18
|
+
UNKNOWN,
|
19
|
+
MODEL_BY_REFERENCE_OSS_PATH_KEY,
|
20
|
+
load_config,
|
21
|
+
get_container_image,
|
22
|
+
UNKNOWN_DICT,
|
23
|
+
get_resource_name,
|
24
|
+
get_model_by_reference_paths,
|
25
|
+
)
|
26
|
+
from ads.aqua.finetune import FineTuneCustomMetadata
|
27
|
+
from ads.aqua.data import AquaResourceIdentifier
|
28
|
+
from ads.common.utils import get_console_link, get_log_links
|
29
|
+
from ads.common.auth import default_signer
|
30
|
+
from ads.model.deployment import (
|
31
|
+
ModelDeployment,
|
32
|
+
ModelDeploymentContainerRuntime,
|
33
|
+
ModelDeploymentInfrastructure,
|
34
|
+
ModelDeploymentMode,
|
35
|
+
)
|
36
|
+
from ads.common.serializer import DataClassSerializable
|
37
|
+
from ads.config import (
|
38
|
+
AQUA_MODEL_DEPLOYMENT_CONFIG,
|
39
|
+
COMPARTMENT_OCID,
|
40
|
+
AQUA_CONFIG_FOLDER,
|
41
|
+
AQUA_MODEL_DEPLOYMENT_CONFIG_DEFAULTS,
|
42
|
+
AQUA_DEPLOYMENT_CONTAINER_METADATA_NAME,
|
43
|
+
AQUA_SERVED_MODEL_NAME,
|
44
|
+
)
|
45
|
+
from ads.common.object_storage_details import ObjectStorageDetails
|
46
|
+
from ads.telemetry import telemetry
|
47
|
+
|
48
|
+
|
49
|
+
@dataclass
|
50
|
+
class ShapeInfo:
|
51
|
+
instance_shape: str = None
|
52
|
+
instance_count: int = None
|
53
|
+
ocpus: float = None
|
54
|
+
memory_in_gbs: float = None
|
55
|
+
|
56
|
+
|
57
|
+
@dataclass(repr=False)
|
58
|
+
class AquaDeployment(DataClassSerializable):
|
59
|
+
"""Represents an Aqua Model Deployment"""
|
60
|
+
|
61
|
+
id: str = None
|
62
|
+
display_name: str = None
|
63
|
+
aqua_service_model: bool = None
|
64
|
+
state: str = None
|
65
|
+
description: str = None
|
66
|
+
created_on: str = None
|
67
|
+
created_by: str = None
|
68
|
+
endpoint: str = None
|
69
|
+
console_link: str = None
|
70
|
+
lifecycle_details: str = None
|
71
|
+
shape_info: field(default_factory=ShapeInfo) = None
|
72
|
+
tags: dict = None
|
73
|
+
|
74
|
+
@classmethod
|
75
|
+
def from_oci_model_deployment(
|
76
|
+
cls,
|
77
|
+
oci_model_deployment: Union[ModelDeploymentSummary, ModelDeployment],
|
78
|
+
region: str,
|
79
|
+
) -> "AquaDeployment":
|
80
|
+
"""Converts oci model deployment response to AquaDeployment instance.
|
81
|
+
|
82
|
+
Parameters
|
83
|
+
----------
|
84
|
+
oci_model_deployment: Union[ModelDeploymentSummary, ModelDeployment]
|
85
|
+
The instance of either oci.data_science.models.ModelDeployment or
|
86
|
+
oci.data_science.models.ModelDeploymentSummary class.
|
87
|
+
region: str
|
88
|
+
The region of this model deployment.
|
89
|
+
|
90
|
+
Returns
|
91
|
+
-------
|
92
|
+
AquaDeployment:
|
93
|
+
The instance of the Aqua model deployment.
|
94
|
+
"""
|
95
|
+
instance_configuration = (
|
96
|
+
oci_model_deployment.model_deployment_configuration_details.model_configuration_details.instance_configuration
|
97
|
+
)
|
98
|
+
instance_shape_config_details = (
|
99
|
+
instance_configuration.model_deployment_instance_shape_config_details
|
100
|
+
)
|
101
|
+
instance_count = (
|
102
|
+
oci_model_deployment.model_deployment_configuration_details.model_configuration_details.scaling_policy.instance_count
|
103
|
+
)
|
104
|
+
shape_info = ShapeInfo(
|
105
|
+
instance_shape=instance_configuration.instance_shape_name,
|
106
|
+
instance_count=instance_count,
|
107
|
+
ocpus=(
|
108
|
+
instance_shape_config_details.ocpus
|
109
|
+
if instance_shape_config_details
|
110
|
+
else None
|
111
|
+
),
|
112
|
+
memory_in_gbs=(
|
113
|
+
instance_shape_config_details.memory_in_gbs
|
114
|
+
if instance_shape_config_details
|
115
|
+
else None
|
116
|
+
),
|
117
|
+
)
|
118
|
+
|
119
|
+
return AquaDeployment(
|
120
|
+
id=oci_model_deployment.id,
|
121
|
+
display_name=oci_model_deployment.display_name,
|
122
|
+
aqua_service_model=oci_model_deployment.freeform_tags.get(
|
123
|
+
Tags.AQUA_SERVICE_MODEL_TAG.value
|
124
|
+
)
|
125
|
+
is not None,
|
126
|
+
shape_info=shape_info,
|
127
|
+
state=oci_model_deployment.lifecycle_state,
|
128
|
+
lifecycle_details=getattr(
|
129
|
+
oci_model_deployment, "lifecycle_details", UNKNOWN
|
130
|
+
),
|
131
|
+
description=oci_model_deployment.description,
|
132
|
+
created_on=str(oci_model_deployment.time_created),
|
133
|
+
created_by=oci_model_deployment.created_by,
|
134
|
+
endpoint=oci_model_deployment.model_deployment_url,
|
135
|
+
console_link=get_console_link(
|
136
|
+
resource="model-deployments",
|
137
|
+
ocid=oci_model_deployment.id,
|
138
|
+
region=region,
|
139
|
+
),
|
140
|
+
tags=oci_model_deployment.freeform_tags,
|
141
|
+
)
|
142
|
+
|
143
|
+
|
144
|
+
@dataclass(repr=False)
|
145
|
+
class AquaDeploymentDetail(AquaDeployment, DataClassSerializable):
|
146
|
+
"""Represents a details of Aqua deployment."""
|
147
|
+
|
148
|
+
log_group: AquaResourceIdentifier = field(default_factory=AquaResourceIdentifier)
|
149
|
+
log: AquaResourceIdentifier = field(default_factory=AquaResourceIdentifier)
|
150
|
+
|
151
|
+
|
152
|
+
class AquaDeploymentApp(AquaApp):
|
153
|
+
"""Provides a suite of APIs to interact with Aqua model deployments within the Oracle
|
154
|
+
Cloud Infrastructure Data Science service, serving as an interface for deploying
|
155
|
+
machine learning models.
|
156
|
+
|
157
|
+
|
158
|
+
Methods
|
159
|
+
-------
|
160
|
+
create(model_id: str, instance_shape: str, display_name: str,...) -> AquaDeployment
|
161
|
+
Creates a model deployment for Aqua Model.
|
162
|
+
get(model_deployment_id: str) -> AquaDeployment:
|
163
|
+
Retrieves details of an Aqua model deployment by its unique identifier.
|
164
|
+
list(**kwargs) -> List[AquaModelSummary]:
|
165
|
+
Lists all Aqua deployments within a specified compartment and/or project.
|
166
|
+
get_deployment_config(self, model_id: str) -> Dict:
|
167
|
+
Gets the deployment config of given Aqua model.
|
168
|
+
|
169
|
+
Note:
|
170
|
+
Use `ads aqua deployment <method_name> --help` to get more details on the parameters available.
|
171
|
+
This class is designed to work within the Oracle Cloud Infrastructure
|
172
|
+
and requires proper configuration and authentication set up to interact
|
173
|
+
with OCI services.
|
174
|
+
"""
|
175
|
+
|
176
|
+
@telemetry(entry_point="plugin=deployment&action=create", name="aqua")
|
177
|
+
def create(
|
178
|
+
self,
|
179
|
+
model_id: str,
|
180
|
+
instance_shape: str,
|
181
|
+
display_name: str,
|
182
|
+
instance_count: int = None,
|
183
|
+
log_group_id: str = None,
|
184
|
+
access_log_id: str = None,
|
185
|
+
predict_log_id: str = None,
|
186
|
+
compartment_id: str = None,
|
187
|
+
project_id: str = None,
|
188
|
+
description: str = None,
|
189
|
+
bandwidth_mbps: int = None,
|
190
|
+
web_concurrency: int = None,
|
191
|
+
server_port: int = 8080,
|
192
|
+
health_check_port: int = 8080,
|
193
|
+
env_var: Dict = None,
|
194
|
+
) -> "AquaDeployment":
|
195
|
+
"""
|
196
|
+
Creates a new Aqua deployment
|
197
|
+
|
198
|
+
Parameters
|
199
|
+
----------
|
200
|
+
model_id: str
|
201
|
+
The model OCID to deploy.
|
202
|
+
compartment_id: str
|
203
|
+
The compartment OCID
|
204
|
+
project_id: str
|
205
|
+
Target project to list deployments from.
|
206
|
+
display_name: str
|
207
|
+
The name of model deployment.
|
208
|
+
description: str
|
209
|
+
The description of the deployment.
|
210
|
+
instance_count: (int, optional). Defaults to 1.
|
211
|
+
The number of instance used for deployment.
|
212
|
+
instance_shape: (str).
|
213
|
+
The shape of the instance used for deployment.
|
214
|
+
log_group_id: (str)
|
215
|
+
The oci logging group id. The access log and predict log share the same log group.
|
216
|
+
access_log_id: (str).
|
217
|
+
The access log OCID for the access logs. https://docs.oracle.com/en-us/iaas/data-science/using/model_dep_using_logging.htm
|
218
|
+
predict_log_id: (str).
|
219
|
+
The predict log OCID for the predict logs. https://docs.oracle.com/en-us/iaas/data-science/using/model_dep_using_logging.htm
|
220
|
+
bandwidth_mbps: (int). Defaults to 10.
|
221
|
+
The bandwidth limit on the load balancer in Mbps.
|
222
|
+
web_concurrency: str
|
223
|
+
The number of worker processes/threads to handle incoming requests
|
224
|
+
with_bucket_uri(bucket_uri)
|
225
|
+
Sets the bucket uri when uploading large size model.
|
226
|
+
server_port: (int). Defaults to 8080.
|
227
|
+
The server port for docker container image.
|
228
|
+
health_check_port: (int). Defaults to 8080.
|
229
|
+
The health check port for docker container image.
|
230
|
+
env_var : dict, optional
|
231
|
+
Environment variable for the deployment, by default None.
|
232
|
+
Returns
|
233
|
+
-------
|
234
|
+
AquaDeployment
|
235
|
+
An Aqua deployment instance
|
236
|
+
|
237
|
+
"""
|
238
|
+
# todo: revisit error handling and pull deployment image info from config
|
239
|
+
# if not AQUA_MODEL_DEPLOYMENT_IMAGE:
|
240
|
+
# raise AquaValueError(
|
241
|
+
# f"AQUA_MODEL_DEPLOYMENT_IMAGE must be available in environment variables to "
|
242
|
+
# f"continue with Aqua model deployment."
|
243
|
+
# )
|
244
|
+
|
245
|
+
# todo: for fine tuned models, skip model creation.
|
246
|
+
# Create a model catalog entry in the user compartment
|
247
|
+
aqua_model = AquaModelApp().create(
|
248
|
+
model_id=model_id, comparment_id=compartment_id, project_id=project_id
|
249
|
+
)
|
250
|
+
|
251
|
+
tags = {}
|
252
|
+
for tag in [
|
253
|
+
Tags.AQUA_SERVICE_MODEL_TAG.value,
|
254
|
+
Tags.AQUA_FINE_TUNED_MODEL_TAG.value,
|
255
|
+
Tags.AQUA_TAG.value,
|
256
|
+
]:
|
257
|
+
if tag in aqua_model.freeform_tags:
|
258
|
+
tags[tag] = aqua_model.freeform_tags[tag]
|
259
|
+
|
260
|
+
# Set up info to get deployment config
|
261
|
+
config_source_id = model_id
|
262
|
+
model_name = aqua_model.display_name
|
263
|
+
|
264
|
+
is_fine_tuned_model = (
|
265
|
+
Tags.AQUA_FINE_TUNED_MODEL_TAG.value in aqua_model.freeform_tags
|
266
|
+
)
|
267
|
+
|
268
|
+
if is_fine_tuned_model:
|
269
|
+
try:
|
270
|
+
config_source_id = aqua_model.custom_metadata_list.get(
|
271
|
+
FineTuneCustomMetadata.FINE_TUNE_SOURCE.value
|
272
|
+
).value
|
273
|
+
model_name = aqua_model.custom_metadata_list.get(
|
274
|
+
FineTuneCustomMetadata.FINE_TUNE_SOURCE_NAME.value
|
275
|
+
).value
|
276
|
+
except:
|
277
|
+
raise AquaValueError(
|
278
|
+
f"Either {FineTuneCustomMetadata.FINE_TUNE_SOURCE.value} or {FineTuneCustomMetadata.FINE_TUNE_SOURCE_NAME.value} is missing "
|
279
|
+
f"from custom metadata for the model {config_source_id}"
|
280
|
+
)
|
281
|
+
|
282
|
+
deployment_config = self.get_deployment_config(config_source_id)
|
283
|
+
vllm_params = (
|
284
|
+
deployment_config.get("configuration", UNKNOWN_DICT)
|
285
|
+
.get(instance_shape, UNKNOWN_DICT)
|
286
|
+
.get("parameters", UNKNOWN_DICT)
|
287
|
+
.get("VLLM_PARAMS", UNKNOWN)
|
288
|
+
)
|
289
|
+
|
290
|
+
# set up env vars
|
291
|
+
if not env_var:
|
292
|
+
env_var = dict()
|
293
|
+
|
294
|
+
try:
|
295
|
+
model_path_prefix = aqua_model.custom_metadata_list.get(
|
296
|
+
MODEL_BY_REFERENCE_OSS_PATH_KEY
|
297
|
+
).value.rstrip("/")
|
298
|
+
except ValueError:
|
299
|
+
raise AquaValueError(
|
300
|
+
f"{MODEL_BY_REFERENCE_OSS_PATH_KEY} key is not available in the custom metadata field."
|
301
|
+
)
|
302
|
+
|
303
|
+
# todo: remove this after absolute path is removed from env var
|
304
|
+
if ObjectStorageDetails.is_oci_path(model_path_prefix):
|
305
|
+
os_path = ObjectStorageDetails.from_path(model_path_prefix)
|
306
|
+
model_path_prefix = os_path.filepath.rstrip("/")
|
307
|
+
|
308
|
+
env_var.update({"BASE_MODEL": f"{model_path_prefix}"})
|
309
|
+
params = f"--served-model-name {AQUA_SERVED_MODEL_NAME} --seed 42 "
|
310
|
+
if vllm_params:
|
311
|
+
params += vllm_params
|
312
|
+
env_var.update({"PARAMS": params})
|
313
|
+
env_var.update({"MODEL_DEPLOY_PREDICT_ENDPOINT": "/v1/completions"})
|
314
|
+
env_var.update({"MODEL_DEPLOY_ENABLE_STREAMING": "true"})
|
315
|
+
|
316
|
+
if is_fine_tuned_model:
|
317
|
+
_, fine_tune_output_path = get_model_by_reference_paths(
|
318
|
+
aqua_model.model_file_description
|
319
|
+
)
|
320
|
+
|
321
|
+
if not fine_tune_output_path:
|
322
|
+
raise AquaValueError(
|
323
|
+
f"Fine tuned output path is not available in the model artifact."
|
324
|
+
)
|
325
|
+
|
326
|
+
os_path = ObjectStorageDetails.from_path(fine_tune_output_path)
|
327
|
+
fine_tune_output_path = os_path.filepath.rstrip("/")
|
328
|
+
|
329
|
+
env_var.update({"FT_MODEL": f"{fine_tune_output_path}"})
|
330
|
+
|
331
|
+
logging.info(f"Env vars used for deploying {aqua_model.id} :{env_var}")
|
332
|
+
|
333
|
+
try:
|
334
|
+
container_type_key = aqua_model.custom_metadata_list.get(
|
335
|
+
AQUA_DEPLOYMENT_CONTAINER_METADATA_NAME
|
336
|
+
).value
|
337
|
+
except ValueError:
|
338
|
+
raise AquaValueError(
|
339
|
+
f"{AQUA_DEPLOYMENT_CONTAINER_METADATA_NAME} key is not available in the custom metadata field for model {aqua_model.id}"
|
340
|
+
)
|
341
|
+
|
342
|
+
# fetch image name from config
|
343
|
+
container_image = get_container_image(
|
344
|
+
container_type=container_type_key,
|
345
|
+
)
|
346
|
+
logging.info(
|
347
|
+
f"Aqua Image used for deploying {aqua_model.id} : {container_image}"
|
348
|
+
)
|
349
|
+
|
350
|
+
# Start model deployment
|
351
|
+
# configure model deployment infrastructure
|
352
|
+
# todo : any other infrastructure params needed?
|
353
|
+
infrastructure = (
|
354
|
+
ModelDeploymentInfrastructure()
|
355
|
+
.with_project_id(project_id)
|
356
|
+
.with_compartment_id(compartment_id)
|
357
|
+
.with_shape_name(instance_shape)
|
358
|
+
.with_bandwidth_mbps(bandwidth_mbps)
|
359
|
+
.with_replica(instance_count)
|
360
|
+
.with_web_concurrency(web_concurrency)
|
361
|
+
.with_access_log(
|
362
|
+
log_group_id=log_group_id,
|
363
|
+
log_id=access_log_id,
|
364
|
+
)
|
365
|
+
.with_predict_log(
|
366
|
+
log_group_id=log_group_id,
|
367
|
+
log_id=predict_log_id,
|
368
|
+
)
|
369
|
+
)
|
370
|
+
# configure model deployment runtime
|
371
|
+
# todo : any other runtime params needed?
|
372
|
+
container_runtime = (
|
373
|
+
ModelDeploymentContainerRuntime()
|
374
|
+
.with_image(container_image)
|
375
|
+
.with_server_port(server_port)
|
376
|
+
.with_health_check_port(health_check_port)
|
377
|
+
.with_env(env_var)
|
378
|
+
.with_deployment_mode(ModelDeploymentMode.HTTPS)
|
379
|
+
.with_model_uri(aqua_model.id)
|
380
|
+
.with_region(self.region)
|
381
|
+
.with_overwrite_existing_artifact(True)
|
382
|
+
.with_remove_existing_artifact(True)
|
383
|
+
)
|
384
|
+
# configure model deployment and deploy model on container runtime
|
385
|
+
# todo : any other deployment params needed?
|
386
|
+
deployment = (
|
387
|
+
ModelDeployment()
|
388
|
+
.with_display_name(display_name)
|
389
|
+
.with_description(description)
|
390
|
+
.with_freeform_tags(**tags)
|
391
|
+
.with_infrastructure(infrastructure)
|
392
|
+
.with_runtime(container_runtime)
|
393
|
+
).deploy(wait_for_completion=False)
|
394
|
+
|
395
|
+
if is_fine_tuned_model:
|
396
|
+
# tracks unique deployments that were created in the user compartment
|
397
|
+
self.telemetry.record_event_async(
|
398
|
+
category="aqua/custom/deployment", action="create", detail=model_name
|
399
|
+
)
|
400
|
+
# tracks the shape used for deploying the custom models
|
401
|
+
self.telemetry.record_event_async(
|
402
|
+
category="aqua/custom/deployment/create",
|
403
|
+
action="shape",
|
404
|
+
detail=instance_shape,
|
405
|
+
)
|
406
|
+
# tracks the shape used for deploying the custom models by name
|
407
|
+
self.telemetry.record_event_async(
|
408
|
+
category=f"aqua/custom/{model_name}/deployment/create",
|
409
|
+
action="shape",
|
410
|
+
detail=instance_shape,
|
411
|
+
)
|
412
|
+
else:
|
413
|
+
# tracks unique deployments that were created in the user compartment
|
414
|
+
self.telemetry.record_event_async(
|
415
|
+
category="aqua/service/deployment", action="create", detail=model_name
|
416
|
+
)
|
417
|
+
# tracks the shape used for deploying the service models
|
418
|
+
self.telemetry.record_event_async(
|
419
|
+
category="aqua/service/deployment/create",
|
420
|
+
action="shape",
|
421
|
+
detail=instance_shape,
|
422
|
+
)
|
423
|
+
# tracks the shape used for deploying the service models by name
|
424
|
+
self.telemetry.record_event_async(
|
425
|
+
category=f"aqua/service/{model_name}/deployment/create",
|
426
|
+
action="shape",
|
427
|
+
detail=instance_shape,
|
428
|
+
)
|
429
|
+
|
430
|
+
return AquaDeployment.from_oci_model_deployment(
|
431
|
+
deployment.dsc_model_deployment, self.region
|
432
|
+
)
|
433
|
+
|
434
|
+
@telemetry(entry_point="plugin=deployment&action=list", name="aqua")
|
435
|
+
def list(self, **kwargs) -> List["AquaDeployment"]:
|
436
|
+
"""List Aqua model deployments in a given compartment and under certain project.
|
437
|
+
|
438
|
+
Parameters
|
439
|
+
----------
|
440
|
+
kwargs
|
441
|
+
Keyword arguments, such as compartment_id and project_id,
|
442
|
+
for `list_call_get_all_results <https://docs.oracle.com/en-us/iaas/tools/python/2.118.1/api/pagination.html#oci.pagination.list_call_get_all_results>`_
|
443
|
+
|
444
|
+
Returns
|
445
|
+
-------
|
446
|
+
List[AquaDeployment]:
|
447
|
+
The list of the Aqua model deployments.
|
448
|
+
"""
|
449
|
+
compartment_id = kwargs.pop("compartment_id", COMPARTMENT_OCID)
|
450
|
+
|
451
|
+
model_deployments = self.list_resource(
|
452
|
+
self.ds_client.list_model_deployments,
|
453
|
+
compartment_id=compartment_id,
|
454
|
+
**kwargs,
|
455
|
+
)
|
456
|
+
|
457
|
+
results = []
|
458
|
+
for model_deployment in model_deployments:
|
459
|
+
oci_aqua = (
|
460
|
+
(
|
461
|
+
Tags.AQUA_TAG.value in model_deployment.freeform_tags
|
462
|
+
or Tags.AQUA_TAG.value.lower() in model_deployment.freeform_tags
|
463
|
+
)
|
464
|
+
if model_deployment.freeform_tags
|
465
|
+
else False
|
466
|
+
)
|
467
|
+
|
468
|
+
if oci_aqua:
|
469
|
+
results.append(
|
470
|
+
AquaDeployment.from_oci_model_deployment(
|
471
|
+
model_deployment, self.region
|
472
|
+
)
|
473
|
+
)
|
474
|
+
|
475
|
+
# tracks number of times deployment listing was called
|
476
|
+
self.telemetry.record_event_async(category="aqua/deployment", action="list")
|
477
|
+
|
478
|
+
return results
|
479
|
+
|
480
|
+
@telemetry(entry_point="plugin=deployment&action=get", name="aqua")
|
481
|
+
def get(self, model_deployment_id: str, **kwargs) -> "AquaDeploymentDetail":
|
482
|
+
"""Gets the information of Aqua model deployment.
|
483
|
+
|
484
|
+
Parameters
|
485
|
+
----------
|
486
|
+
model_deployment_id: str
|
487
|
+
The OCID of the Aqua model deployment.
|
488
|
+
kwargs
|
489
|
+
Keyword arguments, for `get_model_deployment
|
490
|
+
<https://docs.oracle.com/en-us/iaas/tools/python/2.119.1/api/data_science/client/oci.data_science.DataScienceClient.html#oci.data_science.DataScienceClient.get_model_deployment>`_
|
491
|
+
|
492
|
+
Returns
|
493
|
+
-------
|
494
|
+
AquaDeploymentDetail:
|
495
|
+
The instance of the Aqua model deployment details.
|
496
|
+
"""
|
497
|
+
model_deployment = self.ds_client.get_model_deployment(
|
498
|
+
model_deployment_id=model_deployment_id, **kwargs
|
499
|
+
).data
|
500
|
+
|
501
|
+
oci_aqua = (
|
502
|
+
(
|
503
|
+
Tags.AQUA_TAG.value in model_deployment.freeform_tags
|
504
|
+
or Tags.AQUA_TAG.value.lower() in model_deployment.freeform_tags
|
505
|
+
)
|
506
|
+
if model_deployment.freeform_tags
|
507
|
+
else False
|
508
|
+
)
|
509
|
+
|
510
|
+
if not oci_aqua:
|
511
|
+
raise AquaRuntimeError(
|
512
|
+
f"Target deployment {model_deployment_id} is not Aqua deployment."
|
513
|
+
)
|
514
|
+
|
515
|
+
log_id = ""
|
516
|
+
log_group_id = ""
|
517
|
+
log_name = ""
|
518
|
+
log_group_name = ""
|
519
|
+
|
520
|
+
logs = (
|
521
|
+
model_deployment.category_log_details.access
|
522
|
+
or model_deployment.category_log_details.predict
|
523
|
+
)
|
524
|
+
if logs:
|
525
|
+
log_id = logs.log_id
|
526
|
+
log_group_id = logs.log_group_id
|
527
|
+
if log_id:
|
528
|
+
log_name = get_resource_name(log_id)
|
529
|
+
if log_group_id:
|
530
|
+
log_group_name = get_resource_name(log_group_id)
|
531
|
+
|
532
|
+
log_group_url = get_log_links(region=self.region, log_group_id=log_group_id)
|
533
|
+
log_url = get_log_links(
|
534
|
+
region=self.region,
|
535
|
+
log_group_id=log_group_id,
|
536
|
+
log_id=log_id,
|
537
|
+
compartment_id=model_deployment.compartment_id,
|
538
|
+
source_id=model_deployment.id
|
539
|
+
)
|
540
|
+
|
541
|
+
return AquaDeploymentDetail(
|
542
|
+
**vars(
|
543
|
+
AquaDeployment.from_oci_model_deployment(model_deployment, self.region)
|
544
|
+
),
|
545
|
+
log_group=AquaResourceIdentifier(
|
546
|
+
log_group_id, log_group_name, log_group_url
|
547
|
+
),
|
548
|
+
log=AquaResourceIdentifier(log_id, log_name, log_url),
|
549
|
+
)
|
550
|
+
|
551
|
+
@telemetry(
|
552
|
+
entry_point="plugin=deployment&action=get_deployment_config", name="aqua"
|
553
|
+
)
|
554
|
+
def get_deployment_config(self, model_id: str) -> Dict:
|
555
|
+
"""Gets the deployment config of given Aqua model.
|
556
|
+
|
557
|
+
Parameters
|
558
|
+
----------
|
559
|
+
model_id: str
|
560
|
+
The OCID of the Aqua model.
|
561
|
+
|
562
|
+
Returns
|
563
|
+
-------
|
564
|
+
Dict:
|
565
|
+
A dict of allowed deployment configs.
|
566
|
+
"""
|
567
|
+
config = self.get_config(model_id, AQUA_MODEL_DEPLOYMENT_CONFIG)
|
568
|
+
if not config:
|
569
|
+
logger.info(f"Fetching default deployment config for model: {model_id}")
|
570
|
+
config = load_config(
|
571
|
+
AQUA_CONFIG_FOLDER,
|
572
|
+
config_file_name=AQUA_MODEL_DEPLOYMENT_CONFIG_DEFAULTS,
|
573
|
+
)
|
574
|
+
return config
|
575
|
+
|
576
|
+
|
577
|
+
@dataclass
|
578
|
+
class ModelParams:
|
579
|
+
max_tokens: int = None
|
580
|
+
temperature: float = None
|
581
|
+
top_k: float = None
|
582
|
+
top_p: float = None
|
583
|
+
model: str = None
|
584
|
+
|
585
|
+
|
586
|
+
@dataclass
|
587
|
+
class MDInferenceResponse(AquaApp):
|
588
|
+
"""Contains APIs for Aqua Model deployments Inference.
|
589
|
+
|
590
|
+
Attributes
|
591
|
+
----------
|
592
|
+
|
593
|
+
model_params: Dict
|
594
|
+
prompt: string
|
595
|
+
|
596
|
+
Methods
|
597
|
+
-------
|
598
|
+
get_model_deployment_response(self, **kwargs) -> "String"
|
599
|
+
Creates an instance of model deployment via Aqua
|
600
|
+
"""
|
601
|
+
|
602
|
+
prompt: str = None
|
603
|
+
model_params: field(default_factory=ModelParams) = None
|
604
|
+
|
605
|
+
@telemetry(entry_point="plugin=inference&action=get_response", name="aqua")
|
606
|
+
def get_model_deployment_response(self, endpoint):
|
607
|
+
"""
|
608
|
+
Returns MD inference response
|
609
|
+
|
610
|
+
Parameters
|
611
|
+
----------
|
612
|
+
endpoint: str
|
613
|
+
MD predict url
|
614
|
+
prompt: str
|
615
|
+
User prompt.
|
616
|
+
|
617
|
+
model_params: (Dict, optional)
|
618
|
+
Model parameters to be associated with the message.
|
619
|
+
Currently supported VLLM+OpenAI parameters.
|
620
|
+
|
621
|
+
--model-params '{
|
622
|
+
"max_tokens":500,
|
623
|
+
"temperature": 0.5,
|
624
|
+
"top_k": 10,
|
625
|
+
"top_p": 0.5,
|
626
|
+
"model": "/opt/ds/model/deployed_model",
|
627
|
+
...}'
|
628
|
+
|
629
|
+
Returns
|
630
|
+
-------
|
631
|
+
model_response_content
|
632
|
+
"""
|
633
|
+
|
634
|
+
params_dict = asdict(self.model_params)
|
635
|
+
params_dict = {
|
636
|
+
key: value for key, value in params_dict.items() if value is not None
|
637
|
+
}
|
638
|
+
body = {"prompt": self.prompt, **params_dict}
|
639
|
+
request_kwargs = {"json": body, "headers": {"Content-Type": "application/json"}}
|
640
|
+
response = requests.post(
|
641
|
+
endpoint, auth=default_signer()["signer"], **request_kwargs
|
642
|
+
)
|
643
|
+
return json.loads(response.content)
|
@@ -0,0 +1 @@
|
|
1
|
+
data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MiA1MiI+PHBhdGggZD0iTTQ2Ljk0IDUySDUuMDZDMi4yNyA1MiAwIDQ5LjczIDAgNDYuOTRWNS4wNkMwIDIuMjcgMi4yNyAwIDUuMDYgMGg0MS44N0M0OS43MyAwIDUyIDIuMjcgNTIgNS4wNnY0MS44N2MwIDIuOC0yLjI3IDUuMDctNS4wNiA1LjA3eiIgZmlsbD0iI2I5ZGFjNCIvPjxwYXRoIGQ9Ik00NC4yNSAxOC4yM3YtMy41Yy4xNi0uMDQuMzItLjA4LjQ3LS4xNWEyLjY5IDIuNjkgMCAwMC0xLjA0LTUuMTdjLTEuNDggMC0yLjY5IDEuMjEtMi42OSAyLjY5IDAgLjUzLjE2IDEuMDIuNDIgMS40NGwtNS4xMiA1LjA3Yy0uMjctLjE3LS41Ny0uMy0uODktLjM3di0zLjVjLjE2LS4wNC4zMi0uMDguNDctLjE1YTIuNjkgMi42OSAwIDEwLTMuNzMtMi40OGMwIC41My4xNiAxLjAyLjQyIDEuNDRsLTUuMTIgNS4wN2MtLjI3LS4xNy0uNTctLjMtLjg5LS4zN3YtMy41Yy4xNi0uMDQuMzItLjA4LjQ3LS4xNWEyLjY5IDIuNjkgMCAwMC0xLjA0LTUuMTdjLTEuNDggMC0yLjY5IDEuMjEtMi42OSAyLjY5YTIuNyAyLjcgMCAwMDIuMTIgMi42M3YzLjVhMi42OSAyLjY5IDAgMDAuNTcgNS4zMiAyLjY5MyAyLjY5MyAwIDAwMi43LTIuNjljMC0uNTMtLjE2LTEuMDItLjQyLTEuNDRsNS4xMi01LjA3Yy4yNy4xNy41Ny4zLjg5LjM3djMuNWEyLjY5IDIuNjkgMCAwMC41NyA1LjMyIDIuNjkzIDIuNjkzIDAgMDAyLjctMi42OWMwLS41My0uMTYtMS4wMi0uNDItMS40NGw1LjEyLTUuMDdjLjI3LjE3LjU3LjMuODkuMzd2My41YTIuNjkgMi42OSAwIDAwLjU3IDUuMzIgMi42ODQgMi42ODQgMCAwMDIuNjktMi42OSAyLjczIDIuNzMgMCAwMC0yLjE0LTIuNjN6bS0uNTgtNy42OGExLjU0IDEuNTQgMCAxMS0uMDAxIDMuMDgxIDEuNTQgMS41NCAwIDAxLjAwMS0zLjA4MXptLTguODUgMGExLjU0IDEuNTQgMCAxMS0uMDAxIDMuMDgxIDEuNTQgMS41NCAwIDAxLjAwMS0zLjA4MXpNMjQuNDEgMTIuMWExLjU0IDEuNTQgMCAxMTEuNTQgMS41NGMtLjg0IDAtMS41NC0uNjktMS41NC0xLjU0em0yLjE1IDEwLjE4YTEuNTQgMS41NCAwIDExLTEuMTk5LTIuODQxIDEuNTQgMS41NCAwIDAxMS4xOTkgMi44NDF6bTguODYgMGExLjU0IDEuNTQgMCAxMS0xLjE5OS0yLjg0MSAxLjU0IDEuNTQgMCAwMTEuMTk5IDIuODQxem04Ljg1IDBhMS41NCAxLjU0IDAgMTEuOTQtMS40MmMuMDEuNjItLjM2IDEuMTgtLjk0IDEuNDJ6TTI1LjMzIDM3LjE2Yy0uODUgMC0xLjUxLS4yNi0xLjk3LS43Ny0uNDYtLjUyLS42OS0xLjI1LS42OS0yLjE5cy4yMy0xLjY2LjctMi4xOGMuNDctLjUyIDEuMTItLjc4IDEuOTctLjc4Ljg1IDAgMS41LjI2IDEuOTcuNzguNDYuNTIuNyAxLjI1LjcgMi4xOCAwIC45NS0uMjMgMS42OC0uNjkgMi4xOS0uNDguNTEtMS4xNC43Ny0xLjk5Ljc3em0wLTEuMDRjLjQ4IDAgLjgzLS4xNiAxLjA2LS40OS4yMy0uMzMuMzUtLjguMzUtMS40MyAwLS42Mi0uMTItMS4xLS4zNS0xLjQyLS4yMy0uMzMtLjU5LS40OS0xLjA2LS40OS0uNDggMC0uODMuMTYtMS4wNi40OS0uMjMuMzMtLjM1LjgtLjM1IDEuNDIgMCAuNjIuMTIgMS4xLjM1IDEuNDMuMjIuMzIuNTguNDkgMS4wNi40OXptNC43My45M2gtMS4xOVYzMi45aC43OGwuMTcuNjFjLjE0LS4yLjMxLS4zNS41MS0uNDdzLjQ0LS4xNy43MS0uMTdjLjEyIDAgLjIxLjAxLjMuMDJ2MS4wOGgtLjI1Yy0uMjcgMC0uNDkuMDctLjY3LjIxLS4xOC4xNC0uMjkuMzItLjM0LjU1djIuMzJ6bTQuNDcgMGwtLjE2LS41OGMtLjE0LjIxLS4zMy4zNy0uNTUuNDlzLS40OC4xOC0uNzYuMThjLS40MyAwLS43Ny0uMTItMS4wMi0uMzUtLjI1LS4yNC0uMzctLjU2LS4zNy0uOTcgMC0uNDMuMTctLjc4LjQ5LTEuMDMuMzMtLjI1Ljc3LS4zOCAxLjMzLS4zOGguNjh2LS4xNWMwLS4yMy0uMDYtLjM4LS4xNy0uNDUtLjEyLS4wNy0uMy0uMS0uNTQtLjEtLjQ0IDAtLjkxLjEtMS40Mi4zdi0uOWMuMjEtLjA5LjQ1LS4xNi43My0uMjIuMjgtLjA2LjU2LS4wOS44NS0uMDkuNTQgMCAuOTYuMTIgMS4yNC4zNy4yOS4yNS40My42MS40MyAxLjA3djIuOGgtLjc2em0tMS4xNi0uNzhjLjE3IDAgLjMzLS4wNC40OC0uMTMuMTUtLjA5LjI3LS4yMS4zNi0uMzZ2LS42OGgtLjU2Yy0uMjcgMC0uNDguMDUtLjYzLjE2LS4xNS4xLS4yMi4yNS0uMjIuNDUgMCAuMzguMTkuNTYuNTcuNTZ6bTYuMDMtLjI4di45M2MtLjE2LjA2LS4zNS4xMi0uNTcuMTYtLjIyLjA0LS40NS4wNi0uNjcuMDYtMS4zOCAwLTIuMDYtLjc0LTIuMDYtMi4yMSAwLS42Ni4xOS0xLjE4LjU3LTEuNTYuMzgtLjM4LjktLjU3IDEuNTYtLjU3LjM2IDAgLjcxLjA2IDEuMDYuMTh2LjkyYy0uMjYtLjEzLS41NC0uMi0uODUtLjItLjM3IDAtLjY1LjEtLjg1LjMtLjIuMi0uMy41MS0uMy45MiAwIC40LjA5LjcxLjI2Ljk1LjE4LjIzLjQzLjM1Ljc3LjM1LjE3IDAgLjM0LS4wMi41Mi0uMDZzLjM3LS4xLjU2LS4xN3ptLjg0IDEuMDZWMzJsLS40My0uMzZ2LS40OGgxLjYydjUuODloLTEuMTl6bTUuNjctLjJjLS40LjE5LS44OC4yOS0xLjQ0LjI5LS43MSAwLTEuMjYtLjE5LTEuNjQtLjU2LS4zOC0uMzctLjU3LS45MS0uNTctMS42MiAwLS42Ny4xOC0xLjIuNTUtMS41OC4zNy0uMzguODgtLjU3IDEuNTMtLjU3LjU4IDAgMS4wMS4xNyAxLjMyLjUuMy4zMy40Ni44LjQ2IDEuMzl2LjZoLTIuNjdjLjA1LjMyLjE2LjU2LjM1LjcxLjE5LjE1LjQ1LjIyLjguMjIuMjIgMCAuNDMtLjAyLjYzLS4wNi4yLS4wNC40My0uMTEuNjktLjIxdi44OXptLTEuNi0zLjE4Yy0uNTYgMC0uODYuMjktLjkuODZoMS42M2MtLjAxLS4yNy0uMDgtLjQ4LS4yMS0uNjNhLjY1My42NTMgMCAwMC0uNTItLjIzek0xMi4xOCA0NS4wNWwyLjE3LTUuN2gxLjE4bDIuMTggNS43aC0xLjM3bC0uMzgtMS4xN2gtMi4xbC0uMzggMS4xN2gtMS4zem0yLjAxLTIuMmgxLjQ0bC0uNzItMi4yNC0uNzIgMi4yNHptMy45My42VjQwLjloMS4xOXYyLjUxYzAgLjUxLjIuNzYuNi43Ni4xOCAwIC4zNC0uMDYuNDktLjE3LjE1LS4xMS4yNy0uMjcuMzUtLjQ2VjQwLjloMS4xOXY0LjE1aC0uNzhsLS4xNi0uNjNjLS4zOC40OS0uODcuNzMtMS40NS43My0uNDYgMC0uODEtLjE1LTEuMDYtLjQ0LS4yNC0uMy0uMzctLjcyLS4zNy0xLjI2em01LjA3LjE3VjQxLjhoLS43M3YtLjZsLjc4LS4zMi4zNS0xLjE3aC43OHYxLjE5aC44OXYuODloLS44OXYxLjc2YzAgLjIuMDUuMzUuMTUuNDQuMS4wOS4yNS4xMy40NC4xM2guMTdjLjA2IDAgLjEyLS4wMS4xNy0uMDF2Ljk1Yy0uMTMuMDItLjI0LjAzLS4zNC4wNC0uMS4wMS0uMjEuMDEtLjM0LjAxLS40OCAwLS44My0uMTItMS4wOC0uMzZzLS4zNS0uNjItLjM1LTEuMTN6bTQuNjYgMS41MmMtLjY1IDAtMS4xNS0uMTktMS41Mi0uNTctLjM2LS4zOC0uNTUtLjkyLS41NS0xLjYgMC0uNjkuMTgtMS4yMi41NS0xLjYuMzYtLjM4Ljg3LS41NyAxLjUyLS41N3MxLjE2LjE5IDEuNTIuNTdjLjM2LjM4LjU0LjkxLjU0IDEuNiAwIC42OC0uMTggMS4yMi0uNTQgMS42LS4zNi4zOC0uODcuNTctMS41Mi41N3ptMC0uOWMuNTggMCAuODYtLjQyLjg2LTEuMjcgMC0uODQtLjI5LTEuMjYtLjg2LTEuMjZzLS44Ni40Mi0uODYgMS4yNmMwIC44NS4yOSAxLjI3Ljg2IDEuMjd6bTIuOTMuODF2LTUuN2gxLjI4bDEuODEgMy43NiAxLjc5LTMuNzZoMS4yOHY1LjdoLTEuMjF2LTMuNjlsLTEuNTEgMy4xNWgtLjc1bC0xLjUxLTMuMXYzLjY1aC0xLjE4em03LjI3IDB2LTUuN2gxLjIydjQuNjFoMi41MXYxLjA5aC0zLjczem00LjAzIDBsMS40NC0yLjEzLTEuMzgtMi4wMmgxLjM0bC43NyAxLjI0Ljc5LTEuMjRoMS4yMmwtMS4zOCAyLjAzIDEuNDQgMi4xMmgtMS4zNGwtLjgyLTEuMzYtLjg2IDEuMzZoLTEuMjJ6IiBmaWxsPSIjMzEyZTJjIi8+PHBhdGggZmlsbD0iI2M5NDYzNSIgZD0iTTAgNS44aDE4LjMzdjE4LjMzSDB6Ii8+PHBhdGggZD0iTTYuMjEgMTYuOTZ2Ljc5Yy0uMjEuMTEtLjQ1LjItLjcxLjI2LS4yNi4wNi0uNTQuMDktLjgzLjA5LS44NCAwLTEuNDgtLjI1LTEuOTMtLjc0LS40NS0uNS0uNjctMS4yMS0uNjctMi4xNSAwLS41OC4xMS0xLjA3LjMyLTEuNDguMjEtLjQxLjUxLS43My45MS0uOTUuNC0uMjIuODctLjMzIDEuNDEtLjMzLjI0IDAgLjQ4LjAzLjcxLjA4LjI0LjA2LjQ0LjEzLjYxLjIzdi43OWMtLjI2LS4xMS0uNDktLjE4LS42OC0uMjNzLS4zOS0uMDctLjU4LS4wN2MtLjU2IDAtLjk5LjE3LTEuMy41MS0uMy4zNC0uNDYuODItLjQ2IDEuNDQgMCAuNjcuMTUgMS4xOC40NSAxLjUzcy43NC41NCAxLjMuNTRjLjIyIDAgLjQ0LS4wMy42Ny0uMDhzLjUtLjEyLjc4LS4yM3ptLjgxIDEuMDN2LTUuNDVoMS44N2MxLjMxIDAgMS45Ni42IDEuOTYgMS43OSAwIDEuMi0uNjYgMS43OS0xLjk2IDEuNzloLS45NHYxLjg3aC0uOTN6TTguNyAxMy4zaC0uNzV2Mi4wN2guNzVjLjQzIDAgLjc0LS4wOC45My0uMjQuMTktLjE2LjI5LS40My4yOS0uNzkgMC0uMzctLjEtLjYzLS4yOS0uNzktLjE5LS4xNy0uNS0uMjUtLjkzLS4yNXptMi44NiAyLjE3di0yLjkzaC45NHYyLjkyYzAgLjYzLjEgMS4wOS4zIDEuMzhzLjUyLjQ0Ljk2LjQ0Yy40NCAwIC43Ni0uMTUuOTYtLjQ0LjItLjI5LjMtLjc1LjMtMS4zOHYtMi45MmguOTR2Mi45M2MwIC44OS0uMTggMS41NS0uNTQgMS45OHMtLjkxLjY0LTEuNjUuNjRjLS43NCAwLTEuMjktLjIxLTEuNjUtLjY0LS4zOC0uNDItLjU2LTEuMDgtLjU2LTEuOTh6IiBmaWxsPSIjZmZmIi8+PC9zdmc+
|