Skip to content

Commit 5546448

Browse files
authored
[ML] Add Custom Applications to Compute Instances (Azure#28505)
* Added _custom_applications.py
1 parent 8c2282a commit 5546448

File tree

11 files changed

+460
-22
lines changed

11 files changed

+460
-22
lines changed

sdk/ml/azure-ai-ml/CHANGELOG.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44

55
### Features Added
66
- Added dedicated classes for each type of job service and updated the docstrings. The classes added are `JupyterLabJobService, SshJobService, TensorBoardJobService, VsCodeJobService` with a few properties specific to the type.
7+
- Added Custom Applications Support to Compute Instances.
78

89
### Bugs Fixed
9-
- Fixed an issue where the ordering of `.amlignore` and `.gitignore` files are not respected
10-
- Fixed an issue where ignore files weren't considered during upload directory size calculations
10+
- Fixed an issue where the ordering of `.amlignore` and `.gitignore` files are not respected.
11+
- Fixed an issue that attributes with a value of `False` in `PipelineJobSettings` are not respected.
12+
- Fixed an issue where ignore files weren't considered during upload directory size calculations.
1113
- Fixed an issue where symlinks crashed upload directory size calculations.
12-
- Fixed an issue that attributes with a value of `False` in `PipelineJobSettings` are not respected
14+
- Fixes a bug where enable_node_public_ip returned an improper value when fetching a Compute.
1315

1416
### Other Changes
1517
- Update workspace creation to use Log Analytics-Based Application Insights when the user does not specify/bring their own App Insights.

sdk/ml/azure-ai-ml/azure/ai/ml/_schema/compute/compute_instance.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from .compute import ComputeSchema, IdentitySchema, NetworkSettingsSchema
1414
from .schedule import ComputeSchedulesSchema
1515
from .setup_scripts import SetupScriptsSchema
16+
from .custom_applications import CustomApplicationsSchema
1617

1718

1819
class ComputeInstanceSshSettingsSchema(PathAwareSchema):
@@ -68,6 +69,7 @@ class ComputeInstanceSchema(ComputeSchema):
6869
identity = ExperimentalField(NestedField(IdentitySchema))
6970
idle_time_before_shutdown = ExperimentalField(fields.Str())
7071
idle_time_before_shutdown_minutes = ExperimentalField(fields.Int())
72+
custom_applications = ExperimentalField(fields.List(NestedField(CustomApplicationsSchema)))
7173
setup_scripts = ExperimentalField(NestedField(SetupScriptsSchema))
7274
os_image_metadata = ExperimentalField(
7375
NestedField(OsImageMetadataSchema, dump_only=True)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# ---------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# ---------------------------------------------------------
4+
5+
# pylint: disable=unused-argument,no-self-use
6+
from marshmallow import fields
7+
from marshmallow.decorators import post_load
8+
9+
from azure.ai.ml._schema.core.fields import NestedField, StringTransformedEnum, ExperimentalField
10+
from azure.ai.ml._schema.core.schema_meta import PatchedSchemaMeta
11+
from azure.ai.ml.constants._compute import CustomApplicationDefaults
12+
13+
14+
class ImageSettingsSchema(metaclass=PatchedSchemaMeta):
15+
reference = fields.Str()
16+
17+
@post_load
18+
def make(self, data, **kwargs):
19+
from azure.ai.ml.entities._compute._custom_applications import ImageSettings
20+
21+
return ImageSettings(**data)
22+
23+
24+
class EndpointsSettingsSchema(metaclass=PatchedSchemaMeta):
25+
target = fields.Int()
26+
published = fields.Int()
27+
28+
@post_load
29+
def make(self, data, **kwargs):
30+
from azure.ai.ml.entities._compute._custom_applications import EndpointsSettings
31+
32+
return EndpointsSettings(**data)
33+
34+
35+
class VolumeSettingsSchema(metaclass=PatchedSchemaMeta):
36+
source = fields.Str()
37+
target = fields.Str()
38+
39+
@post_load
40+
def make(self, data, **kwargs):
41+
from azure.ai.ml.entities._compute._custom_applications import VolumeSettings
42+
43+
return VolumeSettings(**data)
44+
45+
46+
class CustomApplicationsSchema(metaclass=PatchedSchemaMeta):
47+
48+
name = fields.Str(required=True)
49+
type = StringTransformedEnum(allowed_values=[CustomApplicationDefaults.DOCKER])
50+
image = ExperimentalField(NestedField(ImageSettingsSchema))
51+
endpoints = ExperimentalField(fields.List(NestedField(EndpointsSettingsSchema)))
52+
environment_variables = fields.Dict()
53+
bind_mounts = ExperimentalField(fields.List(NestedField(VolumeSettingsSchema)))
54+
55+
@post_load
56+
def make(self, data, **kwargs):
57+
from azure.ai.ml.entities._compute._custom_applications import (
58+
CustomApplications,
59+
)
60+
61+
return CustomApplications(**data)

sdk/ml/azure-ai-ml/azure/ai/ml/_schema/compute/setup_scripts.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# Copyright (c) Microsoft Corporation. All rights reserved.
33
# ---------------------------------------------------------
44

5+
# pylint: disable=unused-argument,no-self-use
56
from marshmallow import fields
67
from marshmallow.decorators import post_load
78

sdk/ml/azure-ai-ml/azure/ai/ml/constants/_compute.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,14 @@ class ComputeDefaults:
3131
MAX_NODES = 4
3232
IDLE_TIME = 1800
3333
PRIORITY = "Dedicated"
34+
35+
class CustomApplicationDefaults:
36+
TARGET_PORT = "target_port"
37+
PUBLISHED_PORT = "published_port"
38+
PORT_MIN_VALUE = 1025
39+
PORT_MAX_VALUE = 65535
40+
DOCKER = "docker"
41+
ENDPOINT_NAME = "connect"
42+
43+
DUPLICATE_APPLICATION_ERROR = "Value of {} must be unique across all custom applications."
44+
INVALID_VALUE_ERROR = "Value of {} must be between {} and {}."

sdk/ml/azure-ai-ml/azure/ai/ml/entities/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from ._compute._aml_compute_node_info import AmlComputeNodeInfo
2626
from ._compute._image_metadata import ImageMetadata
2727
from ._compute._schedule import ComputePowerAction, ComputeSchedules, ComputeStartStopSchedule, ScheduleState
28+
from ._compute._custom_applications import CustomApplications
2829
from ._compute._usage import Usage, UsageName
2930
from ._compute._vm_size import VmSize
3031
from ._compute.aml_compute import AmlCompute, AmlComputeSshSettings
@@ -238,6 +239,7 @@
238239
"AmlComputeSshSettings",
239240
"AmlComputeNodeInfo",
240241
"ImageMetadata",
242+
"CustomApplications",
241243
"SystemCreatedAcrAccount",
242244
"SystemCreatedStorageAccount",
243245
"ValidationResult",
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
# ---------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# ---------------------------------------------------------
4+
# pylint: disable=protected-access,redefined-builtin
5+
6+
from typing import Dict, List, Optional
7+
from azure.ai.ml._restclient.v2022_10_01_preview.models import (
8+
CustomService,
9+
Docker,
10+
Endpoint as RestEndpoint,
11+
EnvironmentVariable as RestEnvironmentVariable,
12+
EnvironmentVariableType as RestEnvironmentVariableType,
13+
Image as RestImage,
14+
ImageType as RestImageType,
15+
Protocol,
16+
VolumeDefinition as RestVolumeDefinition,
17+
VolumeDefinitionType as RestVolumeDefinitionType,
18+
)
19+
from azure.ai.ml.exceptions import ErrorCategory, ErrorTarget, ValidationException
20+
from azure.ai.ml.constants._compute import (
21+
CustomApplicationDefaults,
22+
DUPLICATE_APPLICATION_ERROR,
23+
INVALID_VALUE_ERROR,
24+
)
25+
from azure.ai.ml._utils._experimental import experimental
26+
27+
28+
@experimental
29+
class ImageSettings:
30+
"""Specifies an image configuration for a Custom Application.
31+
32+
:param reference: Image reference URL.
33+
:type reference: str
34+
"""
35+
36+
def __init__(self, *, reference: str):
37+
self.reference = reference
38+
39+
def _to_rest_object(self) -> RestImage:
40+
return RestImage(type=RestImageType.DOCKER, reference=self.reference)
41+
42+
@classmethod
43+
def _from_rest_object(cls, obj: RestImage) -> "ImageSettings":
44+
return ImageSettings(reference=obj.reference)
45+
46+
47+
@experimental
48+
class EndpointsSettings:
49+
"""Specifies an endpoint configuration for a Custom Application.
50+
51+
:param target: Application port inside the container.
52+
:type target: int
53+
:param published: Port over which the application is exposed from container.
54+
:type published: int
55+
"""
56+
57+
def __init__(self, *, target: int, published: int):
58+
EndpointsSettings._validate_endpoint_settings(
59+
target=target, published=published
60+
)
61+
self.target = target
62+
self.published = published
63+
64+
def _to_rest_object(self) -> RestEndpoint:
65+
return RestEndpoint(
66+
name=CustomApplicationDefaults.ENDPOINT_NAME,
67+
target=self.target,
68+
published=self.published,
69+
protocol=Protocol.HTTP,
70+
)
71+
72+
@classmethod
73+
def _from_rest_object(cls, obj: RestEndpoint) -> "EndpointsSettings":
74+
return EndpointsSettings(target=obj.target, published=obj.published)
75+
76+
@classmethod
77+
def _validate_endpoint_settings(cls, target: int, published: int):
78+
ports = {
79+
CustomApplicationDefaults.TARGET_PORT: target,
80+
CustomApplicationDefaults.PUBLISHED_PORT: published,
81+
}
82+
min_value = CustomApplicationDefaults.PORT_MIN_VALUE
83+
max_value = CustomApplicationDefaults.PORT_MAX_VALUE
84+
85+
for port_name, port in ports.items():
86+
message = INVALID_VALUE_ERROR.format(port_name, min_value, max_value)
87+
if not min_value < port < max_value:
88+
raise ValidationException(
89+
message=message,
90+
target=ErrorTarget.COMPUTE,
91+
no_personal_data_message=message,
92+
error_category=ErrorCategory.USER_ERROR,
93+
)
94+
95+
96+
@experimental
97+
class VolumeSettings:
98+
"""Specifies the Bind Mount settings for a Custom Application.
99+
100+
:param source: The host path of the mount.
101+
:type source: str
102+
:param target: The path in the container for the mount.
103+
:type target: str
104+
"""
105+
106+
def __init__(self, *, source: str, target: str):
107+
self.source = source
108+
self.target = target
109+
110+
def _to_rest_object(self) -> RestVolumeDefinition:
111+
return RestVolumeDefinition(
112+
type=RestVolumeDefinitionType.BIND,
113+
read_only=False,
114+
source=self.source,
115+
target=self.target,
116+
)
117+
118+
@classmethod
119+
def _from_rest_object(cls, obj: RestVolumeDefinition) -> "VolumeSettings":
120+
return VolumeSettings(source=obj.source, target=obj.target)
121+
122+
123+
@experimental
124+
class CustomApplications:
125+
"""Specifies the custom service application configuration.
126+
127+
:param name: Name of the Custom Application.
128+
:type name: str
129+
:param image: Describes the Image Specifications.
130+
:type image: ImageSettings
131+
:param type: Type of the Custom Application.
132+
:type type: Optional[str]
133+
:param endpoints: Configuring the endpoints for the container.
134+
:type endpoints: List[EndpointsSettings]
135+
:param environment_variables: Environment Variables for the container.
136+
:type environment_variables: Optional[Dict[str, str]]
137+
:param bind_mounts: Configuration of the bind mounts for the container.
138+
:type bind_mounts: Optional[List[VolumeSettings]]
139+
"""
140+
141+
def __init__(
142+
self,
143+
*,
144+
name: str,
145+
image: ImageSettings,
146+
type: str = CustomApplicationDefaults.DOCKER,
147+
endpoints: List[EndpointsSettings],
148+
environment_variables: Optional[Dict] = None,
149+
bind_mounts: Optional[List[VolumeSettings]] = None,
150+
**kwargs
151+
):
152+
self.name = name
153+
self.type = type
154+
self.image = image
155+
self.endpoints = endpoints
156+
self.environment_variables = environment_variables
157+
self.bind_mounts = bind_mounts
158+
self.additional_properties = kwargs
159+
160+
def _to_rest_object(self):
161+
endpoints = None
162+
if self.endpoints:
163+
endpoints = [endpoint._to_rest_object() for endpoint in self.endpoints]
164+
165+
environment_variables = None
166+
if self.environment_variables:
167+
environment_variables = {
168+
name: RestEnvironmentVariable(
169+
type=RestEnvironmentVariableType.LOCAL, value=value
170+
)
171+
for name, value in self.environment_variables.items()
172+
}
173+
174+
volumes = None
175+
if self.bind_mounts:
176+
volumes = [volume._to_rest_object() for volume in self.bind_mounts]
177+
178+
return CustomService(
179+
name=self.name,
180+
image=self.image._to_rest_object(),
181+
endpoints=endpoints,
182+
environment_variables=environment_variables,
183+
volumes=volumes,
184+
docker=(
185+
Docker(privileged=True)
186+
if self.type == CustomApplicationDefaults.DOCKER
187+
else None
188+
),
189+
additional_properties=self.additional_properties,
190+
)
191+
192+
@classmethod
193+
def _from_rest_object(cls, obj: CustomService) -> "CustomApplications":
194+
endpoints = []
195+
for endpoint in obj.endpoints:
196+
endpoints.append(EndpointsSettings._from_rest_object(endpoint))
197+
198+
environment_variables = (
199+
{name: value.value for name, value in obj.environment_variables.items()}
200+
if obj.environment_variables
201+
else None
202+
)
203+
204+
bind_mounts = []
205+
if obj.volumes:
206+
for volume in obj.volumes:
207+
bind_mounts.append(VolumeSettings._from_rest_object(volume))
208+
209+
return CustomApplications(
210+
name=obj.name,
211+
image=ImageSettings._from_rest_object(obj.image),
212+
endpoints=endpoints,
213+
environment_variables=environment_variables,
214+
bind_mounts=bind_mounts,
215+
additional_properties=obj.additional_properties,
216+
)
217+
218+
219+
def validate_custom_applications(custom_apps: List[CustomApplications]):
220+
message = DUPLICATE_APPLICATION_ERROR
221+
222+
names = [app.name for app in custom_apps]
223+
if len(set(names)) != len(names):
224+
raise ValidationException(
225+
message=message.format("application_name"),
226+
target=ErrorTarget.COMPUTE,
227+
no_personal_data_message=message.format("application_name"),
228+
error_category=ErrorCategory.USER_ERROR,
229+
)
230+
231+
published_ports = [
232+
endpoint.published for app in custom_apps for endpoint in app.endpoints
233+
]
234+
235+
if len(set(published_ports)) != len(published_ports):
236+
raise ValidationException(
237+
message=message.format("published_port"),
238+
target=ErrorTarget.COMPUTE,
239+
no_personal_data_message=message.format("published_port"),
240+
error_category=ErrorCategory.USER_ERROR,
241+
)

sdk/ml/azure-ai-ml/azure/ai/ml/entities/_compute/aml_compute.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ def _load_from_rest(cls, rest_obj: ComputeResource) -> "AmlCompute":
182182
identity=IdentityConfiguration._from_compute_rest_object(rest_obj.identity) if rest_obj.identity else None,
183183
created_on=prop.additional_properties.get("createdOn", None),
184184
enable_node_public_ip=prop.properties.enable_node_public_ip
185-
if prop.properties.enable_node_public_ip
185+
if prop.properties.enable_node_public_ip is not None
186186
else True,
187187
)
188188
return response

0 commit comments

Comments
 (0)