Skip to content

Commit 1cdd6a3

Browse files
Enable GPU field to be set in CLI when creating new container apps
1 parent 416af7b commit 1cdd6a3

File tree

8 files changed

+121
-41
lines changed

8 files changed

+121
-41
lines changed

src/containerapp/HISTORY.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ upcoming
1212
* 'az containerapp env http-route-config': Add commands for the http-route-config feature area.
1313
* 'az containerapp env java-component': Support more flexible configuration updates with new parameters `--set-configurations`, `--replace-configurations`, `--remove-configurations` and `--remove-all-configurations`.
1414
* 'az containerapp env java-component gateway-for-spring create/update': Support `--bind` and `--unbind`
15+
* 'az containerapp create/update': Add an option to specify GPUs per container using the --gpu feature.
1516

1617
1.1.0b1
1718
++++++

src/containerapp/azext_containerapp/_compose_utils.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
from knack.log import get_logger
88

9+
from azure.cli.command_modules.containerapp._compose_utils import service_deploy_resources_exists
10+
911
logger = get_logger(__name__)
1012

1113

@@ -47,3 +49,14 @@ def validate_memory_and_cpu_setting(cpu, memory, managed_environment):
4749
logger.warning( # pylint: disable=W1203
4850
f"Invalid CPU reservation request of {cpu}. The default resource values will be used.")
4951
return (None, None)
52+
53+
54+
def resolve_gpu_configuration_from_service(service):
55+
gpu = None
56+
if service_deploy_resources_exists(service):
57+
resources = service.deploy.resources
58+
if resources.reservations is not None and resources.reservations.gpu is not None:
59+
gpu = str(resources.reservations.gpu)
60+
elif service.gpu is not None:
61+
gpu = str(service.gpu)
62+
return gpu

src/containerapp/azext_containerapp/_help.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,12 @@
908908
az containerapp create -n my-containerapp -g MyResourceGroup \\
909909
--image my-app:v1.0 --environment MyContainerappEnv \\
910910
--enable-java-agent
911+
- name: Create a container app with resource requirements and GPU defined.
912+
text: |
913+
az containerapp create -n my-containerapp -g MyResourceGroup \\
914+
--image my-gpu-app:v1.0 --environment MyContainerappEnv \\
915+
--cpu 2 --memory 4.0Gi \\
916+
--gpu 1 --workload-profile-name my-gpu-wlp
911917
"""
912918

913919
# containerapp update for preview

src/containerapp/azext_containerapp/_models.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@
6464

6565
ContainerResources = {
6666
"cpu": None,
67-
"memory": None
67+
"memory": None,
68+
"gpu": None,
6869
}
6970

7071
VolumeMount = {

src/containerapp/azext_containerapp/_sdk_models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3900,6 +3900,8 @@ class ContainerResources(_serialization.Model):
39003900
:vartype memory: str
39013901
:ivar ephemeral_storage: Ephemeral Storage, e.g. "1Gi".
39023902
:vartype ephemeral_storage: str
3903+
:ivar gpu: Required GPUs, e.g. 1.
3904+
:vartype gpu: float
39033905
"""
39043906

39053907
_validation = {
@@ -3910,6 +3912,7 @@ class ContainerResources(_serialization.Model):
39103912
"cpu": {"key": "cpu", "type": "float"},
39113913
"memory": {"key": "memory", "type": "str"},
39123914
"ephemeral_storage": {"key": "ephemeralStorage", "type": "str"},
3915+
"gpu": {"key": "gpu", "type": "float"},
39133916
}
39143917

39153918
def __init__(
@@ -3927,6 +3930,7 @@ def __init__(
39273930
self.cpu = cpu
39283931
self.memory = memory
39293932
self.ephemeral_storage = None
3933+
self.gpu = None
39303934

39313935

39323936
class CookieExpiration(_serialization.Model):

src/containerapp/azext_containerapp/custom.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,7 @@ def create_containerapp(cmd,
455455
env_vars=None,
456456
cpu=None,
457457
memory=None,
458+
gpu=None,
458459
registry_server=None,
459460
registry_user=None,
460461
registry_pass=None,
@@ -537,6 +538,7 @@ def update_containerapp_logic(cmd,
537538
remove_all_env_vars=False,
538539
cpu=None,
539540
memory=None,
541+
gpu=None,
540542
revision_suffix=None,
541543
startup_command=None,
542544
args=None,
@@ -603,6 +605,7 @@ def update_containerapp(cmd,
603605
remove_all_env_vars=False,
604606
cpu=None,
605607
memory=None,
608+
gpu=None,
606609
revision_suffix=None,
607610
startup_command=None,
608611
args=None,
@@ -643,6 +646,7 @@ def update_containerapp(cmd,
643646
remove_all_env_vars=remove_all_env_vars,
644647
cpu=cpu,
645648
memory=memory,
649+
gpu=gpu,
646650
revision_suffix=revision_suffix,
647651
startup_command=startup_command,
648652
args=args,
@@ -902,6 +906,7 @@ def create_containerappsjob(cmd,
902906
env_vars=None,
903907
cpu=None,
904908
memory=None,
909+
gpu=None,
905910
registry_server=None,
906911
registry_user=None,
907912
registry_pass=None,
@@ -958,6 +963,7 @@ def update_containerappsjob(cmd,
958963
remove_all_env_vars=False,
959964
cpu=None,
960965
memory=None,
966+
gpu=None,
961967
startup_command=None,
962968
args=None,
963969
scale_rule_metadata=None,
@@ -1438,7 +1444,7 @@ def create_containerapps_from_compose(cmd, # pylint: disable=R0914
14381444
resolve_replicas_from_service,
14391445
resolve_environment_from_service,
14401446
resolve_secret_from_service)
1441-
from ._compose_utils import validate_memory_and_cpu_setting
1447+
from ._compose_utils import resolve_gpu_configuration_from_service, validate_memory_and_cpu_setting
14421448

14431449
# Validate managed environment
14441450
parsed_managed_env = parse_resource_id(managed_env)
@@ -1483,6 +1489,7 @@ def create_containerapps_from_compose(cmd, # pylint: disable=R0914
14831489
resolve_memory_configuration_from_service(service),
14841490
managed_environment
14851491
)
1492+
gpu = resolve_gpu_configuration_from_service(service)
14861493
replicas = resolve_replicas_from_service(service)
14871494
environment = resolve_environment_from_service(service)
14881495
secret_vars, secret_env_ref = resolve_secret_from_service(service, parsed_compose_file.secrets)
@@ -1529,6 +1536,7 @@ def create_containerapps_from_compose(cmd, # pylint: disable=R0914
15291536
args=startup_args,
15301537
cpu=cpu,
15311538
memory=memory,
1539+
gpu=gpu,
15321540
env_vars=environment,
15331541
secrets=secret_vars,
15341542
min_replicas=replicas,
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
6+
import os
7+
import time
8+
9+
from azure.cli.testsdk.scenario_tests import AllowLargeResponse
10+
from azure.cli.testsdk import (ScenarioTest, ResourceGroupPreparer, JMESPathCheck, live_only)
11+
12+
from azext_containerapp.tests.latest.common import (write_test_file, clean_up_test_file)
13+
from .common import TEST_LOCATION
14+
15+
TEST_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), '..'))
16+
17+
class ContainerAppWorkloadProfilesGPUTest(ScenarioTest):
18+
def __init__(self, *arg, **kwargs):
19+
super().__init__(*arg, random_config_dir=True, **kwargs)
20+
21+
@AllowLargeResponse(8192)
22+
@ResourceGroupPreparer(location="northeurope")
23+
def test_containerapp_create_enable_dedicated_gpu(self, resource_group):
24+
self.cmd('configure --defaults location={}'.format("northeurope"))
25+
env = self.create_random_name(prefix='gpu-env', length=24)
26+
gpu_default_name = "gpu"
27+
gpu_default_type = "NC24-A100"
28+
self.cmd('containerapp env create -g {} -n {} --logs-destination none --enable-dedicated-gpu'.format(
29+
resource_group, env), expect_failure=False, checks=[
30+
JMESPathCheck("name", env),
31+
JMESPathCheck("properties.provisioningState", "Succeeded"),
32+
JMESPathCheck("length(properties.workloadProfiles)", 2),
33+
JMESPathCheck('properties.workloadProfiles[0].name', "Consumption", case_sensitive=False),
34+
JMESPathCheck('properties.workloadProfiles[0].workloadProfileType', "Consumption", case_sensitive=False),
35+
JMESPathCheck('properties.workloadProfiles[1].name', gpu_default_name, case_sensitive=False),
36+
JMESPathCheck('properties.workloadProfiles[1].workloadProfileType', gpu_default_type, case_sensitive=False),
37+
JMESPathCheck('properties.workloadProfiles[1].maximumCount', 1),
38+
JMESPathCheck('properties.workloadProfiles[1].minimumCount', 0),
39+
])
40+
containerapp_env = self.cmd('containerapp env show -g {} -n {}'.format(resource_group, env)).get_output_in_json()
41+
42+
while containerapp_env["properties"]["provisioningState"].lower() == "waiting":
43+
time.sleep(5)
44+
containerapp_env = self.cmd('containerapp env show -g {} -n {}'.format(resource_group, env)).get_output_in_json()
45+
app1 = self.create_random_name(prefix='app1', length=24)
46+
self.cmd(f'containerapp create -n {app1} -g {resource_group} --image mcr.microsoft.com/azuredocs/samples-tf-mnist-demo:gpu --environment {env} -w {gpu_default_name} --min-replicas 1 --cpu 0.1 --memory 0.1', checks=[
47+
JMESPathCheck("properties.provisioningState", "Succeeded"),
48+
JMESPathCheck("properties.workloadProfileName", gpu_default_name),
49+
JMESPathCheck('properties.template.containers[0].resources.cpu', '0.1'),
50+
JMESPathCheck('properties.template.containers[0].resources.memory', '0.1Gi'),
51+
JMESPathCheck('properties.template.containers[0].resources.gpu', '1'),
52+
JMESPathCheck('properties.template.scale.minReplicas', '1'),
53+
JMESPathCheck('properties.template.scale.maxReplicas', '10')
54+
])
55+
56+
@AllowLargeResponse(8192)
57+
@ResourceGroupPreparer(location="eastus2")
58+
def test_containerapp_create_enable_consumption_gpu(self, resource_group):
59+
self.cmd('configure --defaults location={}'.format("northeurope"))
60+
env = self.create_random_name(prefix='consumption-gpu-env', length=24)
61+
self.cmd('containerapp env create -g {} -n {} --logs-destination none --enable-workload-profiles'.format(
62+
resource_group, env), expect_failure=False, checks=[
63+
JMESPathCheck("name", env),
64+
JMESPathCheck("properties.provisioningState", "Succeeded"),
65+
JMESPathCheck("length(properties.workloadProfiles)", 1),
66+
JMESPathCheck('properties.workloadProfiles[0].name', "Consumption", case_sensitive=False),
67+
JMESPathCheck('properties.workloadProfiles[0].workloadProfileType', "Consumption", case_sensitive=False),
68+
])
69+
consumption_gpu_wp_name = "Consumption-T4"
70+
71+
self.cmd("az containerapp env workload-profile set -g {} -n {} --workload-profile-name {consumption_gpu_wp_name} --workload-profile-type Consumption_GPU_NC8as_T4".format(
72+
resource_group, env), expect_failure=False)
73+
74+
containerapp_env = self.cmd('containerapp env show -g {} -n {}'.format(resource_group, env)).get_output_in_json()
75+
76+
while containerapp_env["properties"]["provisioningState"].lower() == "waiting":
77+
time.sleep(5)
78+
containerapp_env = self.cmd('containerapp env show -g {} -n {}'.format(resource_group, env)).get_output_in_json()
79+
app1 = self.create_random_name(prefix='app1', length=24)
80+
self.cmd(f'containerapp create -n {app1} -g {resource_group} --image mcr.microsoft.com/azuredocs/samples-tf-mnist-demo:gpu --environment {env} -w {consumption_gpu_wp_name} --cpu 0.1 --memory 0.1 --gpu 1', checks=[
81+
JMESPathCheck("properties.provisioningState", "Succeeded"),
82+
JMESPathCheck("properties.workloadProfileName", consumption_gpu_wp_name),
83+
JMESPathCheck('properties.template.containers[0].resources.cpu', '0.1'),
84+
JMESPathCheck('properties.template.containers[0].resources.memory', '0.1Gi'),
85+
JMESPathCheck('properties.template.containers[0].resources.gpu', '1'),
86+
])

src/containerapp/azext_containerapp/tests/latest/test_containerapp_workload_profile_commands.py

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -437,42 +437,3 @@ def assertContainerappProperties(self, containerapp_env, rg, app, workload_profi
437437
JMESPathCheck("properties.template.containers[0].resources.cpu", cpu),
438438
JMESPathCheck("properties.template.containers[0].resources.memory", mem)
439439
])
440-
441-
442-
class ContainerAppWorkloadProfilesGPUTest(ScenarioTest):
443-
def __init__(self, *arg, **kwargs):
444-
super().__init__(*arg, random_config_dir=True, **kwargs)
445-
446-
@AllowLargeResponse(8192)
447-
@ResourceGroupPreparer(location="northeurope")
448-
def test_containerapp_create_enable_dedicated_gpu(self, resource_group):
449-
self.cmd('configure --defaults location={}'.format("northeurope"))
450-
env = self.create_random_name(prefix='gpu-env', length=24)
451-
gpu_default_name = "gpu"
452-
gpu_default_type = "NC24-A100"
453-
self.cmd('containerapp env create -g {} -n {} --logs-destination none --enable-dedicated-gpu'.format(
454-
resource_group, env), expect_failure=False, checks=[
455-
JMESPathCheck("name", env),
456-
JMESPathCheck("properties.provisioningState", "Succeeded"),
457-
JMESPathCheck("length(properties.workloadProfiles)", 2),
458-
JMESPathCheck('properties.workloadProfiles[0].name', "Consumption", case_sensitive=False),
459-
JMESPathCheck('properties.workloadProfiles[0].workloadProfileType', "Consumption", case_sensitive=False),
460-
JMESPathCheck('properties.workloadProfiles[1].name', gpu_default_name, case_sensitive=False),
461-
JMESPathCheck('properties.workloadProfiles[1].workloadProfileType', gpu_default_type, case_sensitive=False),
462-
JMESPathCheck('properties.workloadProfiles[1].maximumCount', 1),
463-
JMESPathCheck('properties.workloadProfiles[1].minimumCount', 0),
464-
])
465-
containerapp_env = self.cmd('containerapp env show -g {} -n {}'.format(resource_group, env)).get_output_in_json()
466-
467-
while containerapp_env["properties"]["provisioningState"].lower() == "waiting":
468-
time.sleep(5)
469-
containerapp_env = self.cmd('containerapp env show -g {} -n {}'.format(resource_group, env)).get_output_in_json()
470-
app1 = self.create_random_name(prefix='app1', length=24)
471-
self.cmd(f'containerapp create -n {app1} -g {resource_group} --image mcr.microsoft.com/azuredocs/samples-tf-mnist-demo:gpu --environment {env} -w {gpu_default_name} --min-replicas 1 --cpu 0.1 --memory 0.1', checks=[
472-
JMESPathCheck("properties.provisioningState", "Succeeded"),
473-
JMESPathCheck("properties.workloadProfileName", gpu_default_name),
474-
JMESPathCheck('properties.template.containers[0].resources.cpu', '0.1'),
475-
JMESPathCheck('properties.template.containers[0].resources.memory', '0.1Gi'),
476-
JMESPathCheck('properties.template.scale.minReplicas', '1'),
477-
JMESPathCheck('properties.template.scale.maxReplicas', '10')
478-
])

0 commit comments

Comments
 (0)