Skip to content

Commit 36a0df0

Browse files
authored
[AI] Convert Deployment Create/Update to an LRO (#33419)
* add ARM template creation * verify local upload for deployment works
1 parent a819c9e commit 36a0df0

File tree

4 files changed

+160
-35
lines changed

4 files changed

+160
-35
lines changed

sdk/ai/azure-ai-resources/azure/ai/resources/_utils/_deployment_utils.py

Lines changed: 113 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,93 @@
1010
from ._registry_utils import get_registry_model
1111

1212

13+
def get_empty_deployment_arm_template():
14+
return {
15+
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
16+
"contentVersion": "1.0.0.0",
17+
"parameters": {
18+
"onlineEndpointProperties": {
19+
"defaultValue": {
20+
"authMode": "Key",
21+
"publicNetworkAccess": "Enabled",
22+
"properties": {
23+
"enforce_access_to_default_secret_stores": "enabled"
24+
},
25+
},
26+
"type": "Object",
27+
},
28+
"onlineEndpointPropertiesTrafficUpdate": {
29+
"defaultValue": {
30+
"traffic": {"[parameters('onlineDeploymentName')]": 100},
31+
"authMode": "Key",
32+
"publicNetworkAccess": "Enabled",
33+
"properties": {
34+
"enforce_access_to_default_secret_stores": "enabled"
35+
},
36+
},
37+
"type": "Object",
38+
},
39+
},
40+
"resources": [
41+
{
42+
"type": "Microsoft.MachineLearningServices/workspaces/onlineEndpoints",
43+
"apiVersion": "2023-04-01-Preview",
44+
"name": "[concat(parameters('workspaceName'), '/', parameters('onlineEndpointName'))]",
45+
"location": "[parameters('location')]",
46+
"identity": {"type": "SystemAssigned"},
47+
"properties": "[parameters('onlineEndpointProperties')]",
48+
"copy": {"name": "onlineEndpointCopy", "count": 1, "mode": "serial"},
49+
},
50+
{
51+
"type": "Microsoft.MachineLearningServices/workspaces/onlineEndpoints/deployments",
52+
"apiVersion": "2023-04-01-Preview",
53+
"name": "[concat(parameters('workspaceName'), '/', parameters('onlineEndpointName'), '/', parameters('onlineDeploymentName'))]",
54+
"location": "[parameters('location')]",
55+
"dependsOn": [
56+
"onlineEndpointCopy",
57+
],
58+
"sku": {"capacity": "[parameters('deploymentInstanceCount')]", "name": "default"},
59+
"identity": {"type": "None"},
60+
"properties": "[parameters('onlineDeploymentProperties')]",
61+
"copy": {"name": "onlineDeploymentCopy", "count": 1, "mode": "serial"},
62+
},
63+
{
64+
"type": "Microsoft.Resources/deployments",
65+
"apiVersion": "2015-01-01",
66+
"name": "[concat('updateEndpointWithTraffic', '-', parameters('onlineEndpointName'))]",
67+
"dependsOn": ["onlineDeploymentCopy"],
68+
"properties": {
69+
"mode": "Incremental",
70+
"template": {
71+
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
72+
"contentVersion": "1.0.0.0",
73+
"resources": [
74+
{
75+
"type": "Microsoft.MachineLearningServices/workspaces/onlineEndpoints",
76+
"apiVersion": "2023-04-01-Preview",
77+
"location": "[parameters('location')]",
78+
"name": "[concat(parameters('workspaceName'), '/', parameters('onlineEndpointName'))]",
79+
"properties": "[parameters('onlineEndpointPropertiesTrafficUpdate')]",
80+
"identity": {"type": "SystemAssigned"},
81+
}
82+
],
83+
},
84+
},
85+
},
86+
],
87+
"outputs": {
88+
"online_endpoint_name": {
89+
"type": "string",
90+
"value": "[parameters('onlineEndpointName')]",
91+
},
92+
"online_deployment_name": {
93+
"type": "string",
94+
"value": "[parameters('onlineDeploymentName')]",
95+
},
96+
},
97+
}
98+
99+
13100
def get_default_allowed_instance_type_for_hugging_face(
14101
model_details: Model, credential: Any
15102
) -> Tuple[str, str]:
@@ -23,31 +110,46 @@ def get_default_allowed_instance_type_for_hugging_face(
23110
hf_engines = hf_engines.split(",")
24111
if len(hf_engines) > 1:
25112
for engine_id in hf_engines:
26-
instance_type, instance_type_list = get_default_allowed_instance_type_from_model_engine(engine_id, credential)
113+
(
114+
instance_type,
115+
instance_type_list,
116+
) = get_default_allowed_instance_type_from_model_engine(
117+
engine_id, credential
118+
)
27119
if "cpu" in engine_id:
28120
default_instance_type = instance_type
29121
allowed_instance_types.append(instance_type_list)
30122
else:
31123
# if the model has only one engine, we can proceed with that as the default engine for SKU
32124
# selection
33-
default_instance_type, allowed_instance_types = get_default_allowed_instance_type_from_model_engine(hf_engines[0], credential)
125+
(
126+
default_instance_type,
127+
allowed_instance_types,
128+
) = get_default_allowed_instance_type_from_model_engine(
129+
hf_engines[0], credential
130+
)
34131
else:
35-
default_instance_type, allowed_instance_types = parse_deployment_config(deployment_config)
132+
default_instance_type, allowed_instance_types = parse_deployment_config(
133+
deployment_config
134+
)
36135
return (default_instance_type, allowed_instance_types)
37136

38137

39138
def parse_deployment_config(deployment_config: str):
40139
deployment_config = json.loads(deployment_config)
41-
allowed_instance_types = deployment_config["PipelineMetadata"]["PipelineDefinition"]["ec"]["AllowedInstanceTypes"]
42-
default_instance_type = deployment_config["PipelineMetadata"]["PipelineDefinition"]["ec"]["DefaultInstanceType"]
140+
allowed_instance_types = deployment_config["PipelineMetadata"][
141+
"PipelineDefinition"
142+
]["ec"]["AllowedInstanceTypes"]
143+
default_instance_type = deployment_config["PipelineMetadata"]["PipelineDefinition"][
144+
"ec"
145+
]["DefaultInstanceType"]
43146

44147
return (default_instance_type, allowed_instance_types)
45148

46149

47-
def get_default_allowed_instance_type_from_model_engine(engine_id: str, credential: Any):
48-
model_details = get_registry_model(
49-
credential,
50-
id=engine_id
51-
)
150+
def get_default_allowed_instance_type_from_model_engine(
151+
engine_id: str, credential: Any
152+
):
153+
model_details = get_registry_model(credential, id=engine_id)
52154
deployment_config = model_details.properties.get("modelDeploymentConfig", None)
53-
return parse_deployment_config(deployment_config)
155+
return parse_deployment_config(deployment_config)

sdk/ai/azure-ai-resources/azure/ai/resources/entities/models.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,6 @@
77
from typing import Union
88

99

10-
@dataclass
11-
class LangchainModel:
12-
chain: "langchain.chains.Chain"
13-
conda_file: Union[str, os.PathLike]
14-
15-
1610
@dataclass
1711
class PromptflowModel:
1812
path: Union[str, os.PathLike]

sdk/ai/azure-ai-resources/azure/ai/resources/operations/_deployment_operations.py

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,21 @@
88
import shutil
99
import tempfile
1010
from typing import Any, List, Union, Iterable
11+
import uuid
1112

1213

1314
from azure.ai.ml import MLClient
14-
from azure.ai.ml.entities import ManagedOnlineDeployment, ManagedOnlineEndpoint, Model as AzureMLModel, DataCollector, DeploymentCollection, Environment, BuildContext
15+
from azure.ai.ml.entities import ManagedOnlineDeployment, Model as AzureMLModel, DataCollector, DeploymentCollection, Environment, BuildContext
16+
from azure.ai.ml.operations._operation_orchestrator import OperationOrchestrator
17+
from azure.ai.ml._utils._endpoint_utils import upload_dependencies
1518
from azure.core.tracing.decorator import distributed_trace
19+
from azure.mgmt.resource import ResourceManagementClient
20+
from azure.mgmt.resource.resources.models import DeploymentMode
21+
from azure.core.polling import LROPoller
1622

1723
from .._utils._scoring_script_utils import create_chat_scoring_script, create_mlmodel_file
1824
from .._utils._registry_utils import get_registry_model
19-
from .._utils._deployment_utils import get_default_allowed_instance_type_for_hugging_face
25+
from .._utils._deployment_utils import get_default_allowed_instance_type_for_hugging_face, get_empty_deployment_arm_template
2026
from ..entities.deployment import Deployment
2127
from ..entities.deployment_keys import DeploymentKeys
2228
from ..entities.models import Model, PromptflowModel
@@ -32,10 +38,11 @@ def __init__(self, ml_client: MLClient, connections, **kwargs) -> None:
3238
self._ml_client = ml_client
3339
self._connections = connections
3440
ops_logger.update_info(kwargs)
41+
self._resource_management_client = ResourceManagementClient(self._ml_client._credential, self._ml_client.subscription_id)
3542

3643
@distributed_trace
37-
@monitor_with_activity(logger, "Deployment.CreateOrUpdate", ActivityType.PUBLICAPI)
38-
def create_or_update(self, deployment: Deployment) -> Any:
44+
@monitor_with_activity(logger, "Deployment.BeginCreateOrUpdate", ActivityType.PUBLICAPI)
45+
def begin_create_or_update(self, deployment: Deployment) -> LROPoller[Deployment]:
3946
model = deployment.model
4047
endpoint_name = deployment.endpoint_name if deployment.endpoint_name else deployment.name
4148

@@ -53,13 +60,6 @@ def create_or_update(self, deployment: Deployment) -> Any:
5360
sampling_rate=1,
5461
)
5562

56-
v2_endpoint = ManagedOnlineEndpoint(
57-
name=endpoint_name,
58-
properties={
59-
"enforce_access_to_default_secret_stores": "enabled"
60-
}
61-
)
62-
created_endpoint = self._ml_client.begin_create_or_update(v2_endpoint).result()
6363
model = deployment.model
6464
v2_deployment = None
6565
temp_dir = tempfile.TemporaryDirectory()
@@ -227,18 +227,46 @@ def create_or_update(self, deployment: Deployment) -> Any:
227227
app_insights_enabled=deployment.app_insights_enabled,
228228
data_collector=data_collector,
229229
)
230-
230+
if deployment.data_collector_enabled:
231+
self._ml_client.online_deployments._register_collection_data_assets(v2_deployment)
232+
orchestrators = OperationOrchestrator(
233+
operation_container=self._ml_client.online_deployments._all_operations,
234+
operation_scope=self._ml_client.online_deployments._operation_scope,
235+
operation_config=self._ml_client.online_deployments._operation_config,
236+
)
237+
upload_dependencies(v2_deployment, orchestrators)
238+
location = self._ml_client.online_deployments._get_workspace_location()
231239
v2_deployment.tags = deployment.tags
232240
v2_deployment.properties = deployment.properties
233-
create_deployment_poller = self._ml_client.begin_create_or_update(v2_deployment)
234241
shutil.rmtree(temp_dir.name)
235-
created_deployment = create_deployment_poller.result()
242+
243+
template = get_empty_deployment_arm_template()
244+
template["parameters"]["workspaceName"] = {"defaultValue": self._ml_client.workspace_name, "type": "String"}
245+
template["parameters"]["onlineEndpointName"] = {"defaultValue": endpoint_name, "type": "String"}
246+
template["parameters"]["onlineDeploymentName"] = {"defaultValue": deployment.name, "type": "String"}
247+
template["parameters"]["onlineDeploymentProperties"] = {"defaultValue": v2_deployment._to_rest_object(location=location).properties.serialize(), "type": "Object"}
248+
template["parameters"]["location"] = {"defaultValue": location, "type": "String"}
249+
template["parameters"]["deploymentInstanceCount"] = {"defaultValue": deployment.instance_count, "type": "int"}
236250

237-
v2_endpoint.traffic = {deployment.name: 100}
238-
update_endpoint_poller = self._ml_client.begin_create_or_update(v2_endpoint)
239-
updated_endpoint = update_endpoint_poller.result()
240251

241-
return Deployment._from_v2_endpoint_deployment(updated_endpoint, deployment)
252+
def lro_callback(raw_response, deserialized, headers):
253+
outputs = deserialized.properties.outputs
254+
return Deployment._from_v2_endpoint_deployment(
255+
self._ml_client.online_endpoints.get(outputs["online_endpoint_name"]["value"]),
256+
self._ml_client.online_deployments.get(outputs["online_deployment_name"]["value"], outputs["online_endpoint_name"]["value"])
257+
)
258+
259+
return self._resource_management_client.deployments.begin_create_or_update(
260+
self._ml_client.resource_group_name,
261+
str(uuid.uuid4()),
262+
{
263+
"properties": {
264+
"template": template,
265+
"mode": DeploymentMode.incremental,
266+
}
267+
},
268+
cls=lro_callback,
269+
)
242270

243271
@distributed_trace
244272
@monitor_with_activity(logger, "Deployment.Get", ActivityType.PUBLICAPI)

sdk/ai/azure-ai-resources/setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"azure-ai-ml>=1.12.0",
7070
"mlflow-skinny<3",
7171
"azureml-telemetry~=1.0,>=1.51.0",
72+
"azure-mgmt-resource<23.0.0,>=22.0.0",
7273
],
7374
project_urls={
7475
"Bug Reports": "https://github.com/Azure/azure-sdk-for-python/issues",

0 commit comments

Comments
 (0)