sagemaker-hyperpod 3.0.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.
- sagemaker/__init__.py +0 -0
- sagemaker/hyperpod/__init__.py +2 -0
- sagemaker/hyperpod/cli/__init__.py +0 -0
- sagemaker/hyperpod/cli/clients/__init__.py +12 -0
- sagemaker/hyperpod/cli/clients/kubernetes_client.py +365 -0
- sagemaker/hyperpod/cli/commands/__init__.py +0 -0
- sagemaker/hyperpod/cli/commands/cluster.py +627 -0
- sagemaker/hyperpod/cli/commands/inference.py +659 -0
- sagemaker/hyperpod/cli/commands/training.py +347 -0
- sagemaker/hyperpod/cli/constants/__init__.py +0 -0
- sagemaker/hyperpod/cli/constants/command_constants.py +105 -0
- sagemaker/hyperpod/cli/constants/exception_constants.py +13 -0
- sagemaker/hyperpod/cli/constants/hyperpod_instance_types.py +100 -0
- sagemaker/hyperpod/cli/constants/kueue_constants.py +17 -0
- sagemaker/hyperpod/cli/constants/pytorch_constants.py +15 -0
- sagemaker/hyperpod/cli/hyp_cli.py +132 -0
- sagemaker/hyperpod/cli/inference_utils.py +121 -0
- sagemaker/hyperpod/cli/service/__init__.py +12 -0
- sagemaker/hyperpod/cli/service/cancel_training_job.py +70 -0
- sagemaker/hyperpod/cli/service/discover_namespaces.py +108 -0
- sagemaker/hyperpod/cli/service/exec_command.py +75 -0
- sagemaker/hyperpod/cli/service/get_logs.py +136 -0
- sagemaker/hyperpod/cli/service/get_namespaces.py +59 -0
- sagemaker/hyperpod/cli/service/get_training_job.py +110 -0
- sagemaker/hyperpod/cli/service/list_pods.py +138 -0
- sagemaker/hyperpod/cli/service/list_training_jobs.py +178 -0
- sagemaker/hyperpod/cli/service/self_subject_access_review.py +47 -0
- sagemaker/hyperpod/cli/telemetry/__init__.py +12 -0
- sagemaker/hyperpod/cli/telemetry/user_agent.py +36 -0
- sagemaker/hyperpod/cli/templates/__init__.py +12 -0
- sagemaker/hyperpod/cli/templates/k8s_pytorch_job_template.py +74 -0
- sagemaker/hyperpod/cli/training_utils.py +200 -0
- sagemaker/hyperpod/cli/utils.py +183 -0
- sagemaker/hyperpod/cli/validators/__init__.py +12 -0
- sagemaker/hyperpod/cli/validators/cluster_validator.py +52 -0
- sagemaker/hyperpod/cli/validators/job_validator.py +346 -0
- sagemaker/hyperpod/cli/validators/validator.py +75 -0
- sagemaker/hyperpod/common/__init__.py +0 -0
- sagemaker/hyperpod/common/config/__init__.py +1 -0
- sagemaker/hyperpod/common/config/metadata.py +18 -0
- sagemaker/hyperpod/common/utils.py +299 -0
- sagemaker/hyperpod/inference/__init__.py +11 -0
- sagemaker/hyperpod/inference/config/__init__.py +0 -0
- sagemaker/hyperpod/inference/config/constants.py +18 -0
- sagemaker/hyperpod/inference/config/hp_endpoint_config.py +767 -0
- sagemaker/hyperpod/inference/config/hp_jumpstart_endpoint_config.py +572 -0
- sagemaker/hyperpod/inference/hp_endpoint.py +203 -0
- sagemaker/hyperpod/inference/hp_endpoint_base.py +223 -0
- sagemaker/hyperpod/inference/hp_jumpstart_endpoint.py +232 -0
- sagemaker/hyperpod/observability/MonitoringConfig.py +10 -0
- sagemaker/hyperpod/observability/__init__.py +0 -0
- sagemaker/hyperpod/observability/constants.py +2 -0
- sagemaker/hyperpod/observability/utils.py +64 -0
- sagemaker/hyperpod/training/__init__.py +9 -0
- sagemaker/hyperpod/training/config/__init__.py +1 -0
- sagemaker/hyperpod/training/config/hyperpod_pytorch_job_config.py +2977 -0
- sagemaker/hyperpod/training/config/hyperpod_pytorch_job_status.py +3083 -0
- sagemaker/hyperpod/training/hyperpod_pytorch_job.py +253 -0
- sagemaker_hyperpod-3.0.0.dist-info/METADATA +534 -0
- sagemaker_hyperpod-3.0.0.dist-info/RECORD +66 -0
- sagemaker_hyperpod-3.0.0.dist-info/WHEEL +5 -0
- sagemaker_hyperpod-3.0.0.dist-info/entry_points.txt +2 -0
- sagemaker_hyperpod-3.0.0.dist-info/licenses/LICENSE +175 -0
- sagemaker_hyperpod-3.0.0.dist-info/licenses/NOTICE +1 -0
- sagemaker_hyperpod-3.0.0.dist-info/top_level.txt +1 -0
- sagemaker_hyperpod-3.0.0.dist-info/zip-safe +1 -0
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"). You
|
|
4
|
+
# may not use this file except in compliance with the License. A copy of
|
|
5
|
+
# the License is located at
|
|
6
|
+
#
|
|
7
|
+
# http://aws.amazon.com/apache2.0/
|
|
8
|
+
#
|
|
9
|
+
# or in the "license" file accompanying this file. This file is
|
|
10
|
+
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
|
|
11
|
+
# ANY KIND, either express or implied. See the License for the specific
|
|
12
|
+
# language governing permissions and limitations under the License.
|
|
13
|
+
import logging
|
|
14
|
+
import subprocess
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
import botocore.config
|
|
18
|
+
from collections import defaultdict
|
|
19
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
20
|
+
|
|
21
|
+
import boto3
|
|
22
|
+
import click
|
|
23
|
+
from botocore.client import BaseClient
|
|
24
|
+
from kubernetes import client
|
|
25
|
+
from ratelimit import limits, sleep_and_retry
|
|
26
|
+
from tabulate import tabulate
|
|
27
|
+
|
|
28
|
+
from sagemaker.hyperpod.cli.clients.kubernetes_client import (
|
|
29
|
+
KubernetesClient,
|
|
30
|
+
)
|
|
31
|
+
from sagemaker.hyperpod.cli.constants.command_constants import (
|
|
32
|
+
AVAILABLE_ACCELERATOR_DEVICES_KEY,
|
|
33
|
+
DEEP_HEALTH_CHECK_STATUS_LABEL,
|
|
34
|
+
HP_HEALTH_STATUS_LABEL,
|
|
35
|
+
HYPERPOD_NAMESPACE_PREFIX,
|
|
36
|
+
INSTANCE_TYPE_LABEL,
|
|
37
|
+
NVIDIA_GPU_RESOURCE_LIMIT_KEY,
|
|
38
|
+
SAGEMAKER_HYPERPOD_NAME_LABEL,
|
|
39
|
+
SAGEMAKER_MANAGED_CLUSTER_QUEUE_SUFFIX,
|
|
40
|
+
SAGEMAKER_QUOTA_ALLOCATION_LABEL,
|
|
41
|
+
TOTAL_ACCELERATOR_DEVICES_KEY,
|
|
42
|
+
TEMP_KUBE_CONFIG_FILE,
|
|
43
|
+
OutputFormat,
|
|
44
|
+
)
|
|
45
|
+
from sagemaker.hyperpod.cli.telemetry.user_agent import (
|
|
46
|
+
get_user_agent_extra_suffix,
|
|
47
|
+
)
|
|
48
|
+
from sagemaker.hyperpod.cli.service.list_pods import (
|
|
49
|
+
ListPods,
|
|
50
|
+
)
|
|
51
|
+
from sagemaker.hyperpod.cli.utils import (
|
|
52
|
+
get_name_from_arn,
|
|
53
|
+
get_sagemaker_client,
|
|
54
|
+
setup_logger,
|
|
55
|
+
set_logging_level,
|
|
56
|
+
store_current_hyperpod_context,
|
|
57
|
+
)
|
|
58
|
+
from sagemaker.hyperpod.cli.validators.cluster_validator import (
|
|
59
|
+
ClusterValidator,
|
|
60
|
+
)
|
|
61
|
+
from sagemaker.hyperpod.cli.utils import (
|
|
62
|
+
get_eks_cluster_name,
|
|
63
|
+
)
|
|
64
|
+
from sagemaker.hyperpod.common.utils import get_cluster_context as get_cluster_context_util
|
|
65
|
+
from sagemaker.hyperpod.observability.utils import get_monitoring_config, is_observability_addon_enabled
|
|
66
|
+
|
|
67
|
+
RATE_LIMIT = 4
|
|
68
|
+
RATE_LIMIT_PERIOD = 1 # 1 second
|
|
69
|
+
|
|
70
|
+
logger = setup_logger(__name__)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@click.command()
|
|
74
|
+
@click.option(
|
|
75
|
+
"--region",
|
|
76
|
+
type=click.STRING,
|
|
77
|
+
required=False,
|
|
78
|
+
help="Optional. The region that the HyperPod and EKS clusters are located. If not specified, it will be set to the region from the current AWS account credentials.",
|
|
79
|
+
)
|
|
80
|
+
@click.option(
|
|
81
|
+
"--output",
|
|
82
|
+
type=click.Choice([c.value for c in OutputFormat]),
|
|
83
|
+
required=False,
|
|
84
|
+
default=OutputFormat.JSON.value,
|
|
85
|
+
help="Optional. The output format. Available values are `TABLE` and `JSON`. The default value is `JSON`.",
|
|
86
|
+
)
|
|
87
|
+
@click.option(
|
|
88
|
+
"--clusters",
|
|
89
|
+
type=click.STRING,
|
|
90
|
+
required=False,
|
|
91
|
+
help="Optional. A list of HyperPod cluster names that users want to check the capacity for. This is useful for users who know some of their most commonly used clusters and want to check the capacity status of the clusters in the AWS account.",
|
|
92
|
+
)
|
|
93
|
+
@click.option(
|
|
94
|
+
"--debug",
|
|
95
|
+
is_flag=True,
|
|
96
|
+
help="Enable debug mode",
|
|
97
|
+
)
|
|
98
|
+
@click.option(
|
|
99
|
+
"--namespace",
|
|
100
|
+
"-n",
|
|
101
|
+
type=click.STRING,
|
|
102
|
+
required=False,
|
|
103
|
+
multiple=True,
|
|
104
|
+
help="Optional. The namespace that you want to check the capacity for. Only SageMaker managed namespaces are supported.",
|
|
105
|
+
)
|
|
106
|
+
def list_cluster(
|
|
107
|
+
region: Optional[str],
|
|
108
|
+
output: Optional[str],
|
|
109
|
+
clusters: Optional[str],
|
|
110
|
+
debug: bool,
|
|
111
|
+
namespace: Optional[List]
|
|
112
|
+
):
|
|
113
|
+
"""List SageMaker Hyperpod Clusters with cluster metadata.
|
|
114
|
+
|
|
115
|
+
Example Usage:
|
|
116
|
+
1. List clusters with JSON output: hyperpod get-clusters -n hyperpod-ns-test-team
|
|
117
|
+
|
|
118
|
+
Output:
|
|
119
|
+
[
|
|
120
|
+
{
|
|
121
|
+
"Cluster": "hyperpod-eks-cluster-a",
|
|
122
|
+
"InstanceType": "ml.g5.2xlarge",
|
|
123
|
+
"TotalNodes": 2,
|
|
124
|
+
"AcceleratorDevicesAvailable": 1,
|
|
125
|
+
"NodeHealthStatus=Schedulable": 2,
|
|
126
|
+
"DeepHealthCheckStatus=Passed": "N/A",
|
|
127
|
+
"Namespaces": {
|
|
128
|
+
"hyperpod-ns-test-team": {
|
|
129
|
+
"AvailableAcceleratorDevices": 1,
|
|
130
|
+
"TotalAcceleratorDevices": 1
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
2. List clusters with table output: hyperpod get-clusters -n hyperpod-ns-test-team --output table
|
|
137
|
+
|
|
138
|
+
Output:
|
|
139
|
+
Cluster | InstanceType | TotalNodes | AcceleratorDevicesAvailable | NodeHealthStatus=Schedulable | DeepHealthCheckStatus=Passed | hyperpod-ns-test-teamTotalAcceleratorDevices | hyperpod-ns-test-teamAvailableAcceleratorDevices
|
|
140
|
+
-----------------------+----------------+--------------+-------------------------------+--------------------------------+------------------------------+------------------------------------------------+----------------------------------------------------
|
|
141
|
+
hyperpod-eks-cluster-a | ml.g5.2xlarge | 2 | 1| 2 | N/A | 1 | 1
|
|
142
|
+
"""
|
|
143
|
+
if debug:
|
|
144
|
+
set_logging_level(logger, logging.DEBUG)
|
|
145
|
+
validator = ClusterValidator()
|
|
146
|
+
|
|
147
|
+
# Make use of user_agent_extra field of the botocore_config object
|
|
148
|
+
# to append SageMaker Hyperpod CLI specific user_agent suffix
|
|
149
|
+
# to the current User-Agent header value from boto3
|
|
150
|
+
# This config will also make sure that user_agent never fails to log the User-Agent string
|
|
151
|
+
# even if boto User-Agent header format is updated in the future
|
|
152
|
+
# Ref: https://botocore.amazonaws.com/v1/documentation/api/latest/reference/config.html
|
|
153
|
+
botocore_config = botocore.config.Config(
|
|
154
|
+
user_agent_extra=get_user_agent_extra_suffix()
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
session = boto3.Session(region_name=region) if region else boto3.Session()
|
|
158
|
+
if not validator.validate_aws_credential(session):
|
|
159
|
+
logger.error("Failed to list clusters capacity due to invalid AWS credentials.")
|
|
160
|
+
sys.exit(1)
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
sm_client = get_sagemaker_client(session, botocore_config)
|
|
164
|
+
except botocore.exceptions.NoRegionError:
|
|
165
|
+
logger.error(
|
|
166
|
+
f"Please ensure you have configured the AWS default region or use the '--region' argument to specify the region."
|
|
167
|
+
)
|
|
168
|
+
sys.exit(1)
|
|
169
|
+
except Exception as e:
|
|
170
|
+
logger.error(f"Failed to initialize the SageMaker client: {e}")
|
|
171
|
+
sys.exit(1)
|
|
172
|
+
|
|
173
|
+
if clusters:
|
|
174
|
+
cluster_names = clusters.split(",")
|
|
175
|
+
else:
|
|
176
|
+
try:
|
|
177
|
+
cluster_names = _get_hyperpod_clusters(sm_client)
|
|
178
|
+
except Exception as e:
|
|
179
|
+
logger.error(f"Failed to list HyperPod clusters due to an error: {e}")
|
|
180
|
+
sys.exit(1)
|
|
181
|
+
|
|
182
|
+
cluster_capacities: List[List[str]] = []
|
|
183
|
+
|
|
184
|
+
counter = 0
|
|
185
|
+
for cluster_name in cluster_names:
|
|
186
|
+
current_cluster_capacities_size = len(cluster_capacities)
|
|
187
|
+
rate_limited_operation(
|
|
188
|
+
cluster_name=cluster_name,
|
|
189
|
+
validator=validator,
|
|
190
|
+
sm_client=sm_client,
|
|
191
|
+
region=region,
|
|
192
|
+
temp_config_file=TEMP_KUBE_CONFIG_FILE,
|
|
193
|
+
cluster_capacities=cluster_capacities,
|
|
194
|
+
namespace=namespace,
|
|
195
|
+
)
|
|
196
|
+
# cluster_capacities will only be updated when the cluster
|
|
197
|
+
# is a valid Hyperpod EKS cluster. This check avoid
|
|
198
|
+
# we skipped many Hyperpod Slurm clusters and didn't return
|
|
199
|
+
# any Hyperpod EKS clusters.
|
|
200
|
+
if len(cluster_capacities) > current_cluster_capacities_size:
|
|
201
|
+
counter += 1
|
|
202
|
+
# Currently only support list <= 50 clusters
|
|
203
|
+
if counter >= 50:
|
|
204
|
+
logger.debug(
|
|
205
|
+
"The 'get-clusters' command has reached the maximum number of HyperPod clusters that can be listed, which is 50."
|
|
206
|
+
)
|
|
207
|
+
break
|
|
208
|
+
|
|
209
|
+
headers = [
|
|
210
|
+
"Cluster",
|
|
211
|
+
"InstanceType",
|
|
212
|
+
"TotalNodes",
|
|
213
|
+
"AcceleratorDevicesAvailable",
|
|
214
|
+
"NodeHealthStatus=Schedulable",
|
|
215
|
+
"DeepHealthCheckStatus=Passed",
|
|
216
|
+
]
|
|
217
|
+
|
|
218
|
+
if namespace is not None:
|
|
219
|
+
for ns in namespace:
|
|
220
|
+
headers.append(ns + TOTAL_ACCELERATOR_DEVICES_KEY)
|
|
221
|
+
headers.append(ns + AVAILABLE_ACCELERATOR_DEVICES_KEY)
|
|
222
|
+
if output == OutputFormat.TABLE.value:
|
|
223
|
+
print(tabulate(cluster_capacities, headers=headers, tablefmt="presto"))
|
|
224
|
+
elif output == OutputFormat.JSON.value:
|
|
225
|
+
json_list = [dict(zip(headers, value)) for value in cluster_capacities]
|
|
226
|
+
_restructure_output(json_list, namespace)
|
|
227
|
+
print(json.dumps(json_list, indent=4))
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@sleep_and_retry
|
|
231
|
+
@limits(calls=RATE_LIMIT, period=RATE_LIMIT_PERIOD)
|
|
232
|
+
def rate_limited_operation(
|
|
233
|
+
cluster_name: str,
|
|
234
|
+
validator: ClusterValidator,
|
|
235
|
+
sm_client: BaseClient,
|
|
236
|
+
region: Optional[str],
|
|
237
|
+
temp_config_file: str,
|
|
238
|
+
cluster_capacities: List[List[str]],
|
|
239
|
+
namespace: Optional[List[str]],
|
|
240
|
+
) -> None:
|
|
241
|
+
try:
|
|
242
|
+
eks_cluster_arn = validator.validate_cluster_and_get_eks_arn(
|
|
243
|
+
cluster_name, sm_client
|
|
244
|
+
)
|
|
245
|
+
if eks_cluster_arn is None:
|
|
246
|
+
logger.warning(
|
|
247
|
+
f"Cannot find EKS cluster behind {cluster_name}, continue..."
|
|
248
|
+
)
|
|
249
|
+
return
|
|
250
|
+
eks_cluster_name = get_name_from_arn(eks_cluster_arn)
|
|
251
|
+
_update_kube_config(eks_cluster_name, region, temp_config_file)
|
|
252
|
+
k8s_client = KubernetesClient(is_get_capacity=True)
|
|
253
|
+
nodes = k8s_client.list_node_with_temp_config(
|
|
254
|
+
temp_config_file, SAGEMAKER_HYPERPOD_NAME_LABEL
|
|
255
|
+
)
|
|
256
|
+
nodes_info = _aggregate_nodes_info(nodes)
|
|
257
|
+
|
|
258
|
+
ns_nominal_quota = {}
|
|
259
|
+
ns_quota_usage = {}
|
|
260
|
+
|
|
261
|
+
for ns in namespace:
|
|
262
|
+
sm_managed_namespace = k8s_client.get_sagemaker_managed_namespace(ns)
|
|
263
|
+
if sm_managed_namespace:
|
|
264
|
+
quota_allocation_id = sm_managed_namespace.metadata.labels[SAGEMAKER_QUOTA_ALLOCATION_LABEL]
|
|
265
|
+
cluster_queue_name = HYPERPOD_NAMESPACE_PREFIX + quota_allocation_id + SAGEMAKER_MANAGED_CLUSTER_QUEUE_SUFFIX
|
|
266
|
+
cluster_queue = k8s_client.get_cluster_queue(cluster_queue_name)
|
|
267
|
+
nominal_quota = _get_cluster_queue_nominal_quota(cluster_queue)
|
|
268
|
+
quota_usage = _get_cluster_queue_quota_usage(cluster_queue)
|
|
269
|
+
ns_nominal_quota[ns] = nominal_quota
|
|
270
|
+
ns_quota_usage[ns] = quota_usage
|
|
271
|
+
else:
|
|
272
|
+
ns_nominal_quota[ns] = {}
|
|
273
|
+
ns_quota_usage[ns] = {}
|
|
274
|
+
|
|
275
|
+
for instance_type, nodes_summary in nodes_info.items():
|
|
276
|
+
capacities = [
|
|
277
|
+
cluster_name,
|
|
278
|
+
instance_type,
|
|
279
|
+
nodes_summary["total_nodes"],
|
|
280
|
+
nodes_summary["accelerator_devices_available"],
|
|
281
|
+
nodes_summary["schedulable"],
|
|
282
|
+
nodes_summary["deep_health_check_passed"],
|
|
283
|
+
]
|
|
284
|
+
for ns in namespace:
|
|
285
|
+
capacities.append(ns_nominal_quota.get(ns).get(instance_type, {}).get(NVIDIA_GPU_RESOURCE_LIMIT_KEY, "N/A"))
|
|
286
|
+
capacities.append(_get_available_quota(ns_nominal_quota.get(ns), ns_quota_usage.get(ns), instance_type, NVIDIA_GPU_RESOURCE_LIMIT_KEY))
|
|
287
|
+
cluster_capacities.append(capacities)
|
|
288
|
+
except Exception as e:
|
|
289
|
+
logger.error(f"Error processing cluster {cluster_name}: {e}, continue...")
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _get_cluster_queue_nominal_quota(cluster_queue):
|
|
293
|
+
nominal_quota = {}
|
|
294
|
+
resource_groups = cluster_queue.get("spec", {}).get("resourceGroups", [])
|
|
295
|
+
resource_group = resource_groups[0]
|
|
296
|
+
|
|
297
|
+
for flavor in resource_group.get("flavors", []):
|
|
298
|
+
flavor_name = flavor.get("name", "unknown")
|
|
299
|
+
resources = flavor.get("resources", [])
|
|
300
|
+
for resource in resources:
|
|
301
|
+
resource_name = resource.get("name")
|
|
302
|
+
quota = resource.get("nominalQuota")
|
|
303
|
+
if flavor_name not in nominal_quota:
|
|
304
|
+
nominal_quota[flavor_name] = {}
|
|
305
|
+
if resource_name == NVIDIA_GPU_RESOURCE_LIMIT_KEY:
|
|
306
|
+
quota = int(quota)
|
|
307
|
+
nominal_quota[flavor_name][resource_name] = quota
|
|
308
|
+
|
|
309
|
+
return nominal_quota
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _get_cluster_queue_quota_usage(cluster_queue):
|
|
313
|
+
quota_usage = {}
|
|
314
|
+
flavor_usage = cluster_queue.get("status", {}).get("flavorsUsage", [])
|
|
315
|
+
|
|
316
|
+
for flavor in flavor_usage:
|
|
317
|
+
flavor_name = flavor.get("name", "unknown")
|
|
318
|
+
resources = flavor.get("resources", [])
|
|
319
|
+
for resource in resources:
|
|
320
|
+
resource_name = resource.get("name")
|
|
321
|
+
usage = resource.get("total")
|
|
322
|
+
if flavor_name not in quota_usage:
|
|
323
|
+
quota_usage[flavor_name] = {}
|
|
324
|
+
if resource_name == NVIDIA_GPU_RESOURCE_LIMIT_KEY:
|
|
325
|
+
usage = int(usage)
|
|
326
|
+
quota_usage[flavor_name][resource_name] = usage
|
|
327
|
+
|
|
328
|
+
return quota_usage
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _get_available_quota(nominal, usage, flavor, resource_name):
|
|
332
|
+
nominal_quota = nominal.get(flavor, {}).get(resource_name, None)
|
|
333
|
+
usage_quota = usage.get(flavor, {}).get(resource_name, None)
|
|
334
|
+
|
|
335
|
+
# Calculating available quota only supports numeric values right now.
|
|
336
|
+
# Some resources need to be further processed by parsing unit like memory, e.g 10Gi
|
|
337
|
+
if nominal_quota is not None and usage_quota is not None:
|
|
338
|
+
return int(nominal_quota) - int(usage_quota)
|
|
339
|
+
|
|
340
|
+
return "N/A"
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _get_hyperpod_clusters(sm_client: boto3.client) -> List[str]:
|
|
344
|
+
cluster_names: List[str] = []
|
|
345
|
+
response = sm_client.list_clusters()
|
|
346
|
+
if "ClusterSummaries" in response:
|
|
347
|
+
cluster_names = [
|
|
348
|
+
cluster["ClusterName"] for cluster in response["ClusterSummaries"]
|
|
349
|
+
]
|
|
350
|
+
|
|
351
|
+
return cluster_names
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _restructure_output(summary_list, namespaces):
|
|
355
|
+
if not namespaces:
|
|
356
|
+
return
|
|
357
|
+
|
|
358
|
+
for node_summary in summary_list:
|
|
359
|
+
node_summary["Namespaces"] = {}
|
|
360
|
+
for ns in namespaces:
|
|
361
|
+
available_accelerators = node_summary[ns + AVAILABLE_ACCELERATOR_DEVICES_KEY]
|
|
362
|
+
total_accelerators = node_summary[ns + TOTAL_ACCELERATOR_DEVICES_KEY]
|
|
363
|
+
quota_accelerator_info = {
|
|
364
|
+
AVAILABLE_ACCELERATOR_DEVICES_KEY: available_accelerators,
|
|
365
|
+
TOTAL_ACCELERATOR_DEVICES_KEY: total_accelerators,
|
|
366
|
+
}
|
|
367
|
+
node_summary["Namespaces"][ns] = quota_accelerator_info
|
|
368
|
+
node_summary.pop(ns + AVAILABLE_ACCELERATOR_DEVICES_KEY, None)
|
|
369
|
+
node_summary.pop(ns + TOTAL_ACCELERATOR_DEVICES_KEY, None)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _aggregate_nodes_info(
|
|
373
|
+
nodes: List[client.V1Node],
|
|
374
|
+
) -> Dict[str, Dict[str, Any]]:
|
|
375
|
+
list_pods_service = ListPods()
|
|
376
|
+
nodes_resource_allocated_dict = (
|
|
377
|
+
list_pods_service.list_pods_and_get_requested_resources_group_by_node_name()
|
|
378
|
+
)
|
|
379
|
+
nodes_summary: Dict[str, Dict[str, int]] = defaultdict(lambda: defaultdict(int))
|
|
380
|
+
|
|
381
|
+
for node in nodes:
|
|
382
|
+
labels = node.metadata.labels
|
|
383
|
+
node_name = node.metadata.name
|
|
384
|
+
logger.debug(f"node_name is {node_name} and labels are {labels}")
|
|
385
|
+
instance_type = labels[INSTANCE_TYPE_LABEL]
|
|
386
|
+
nodes_summary[instance_type]["total_nodes"] += 1
|
|
387
|
+
if DEEP_HEALTH_CHECK_STATUS_LABEL in labels:
|
|
388
|
+
if labels[DEEP_HEALTH_CHECK_STATUS_LABEL] == "Passed":
|
|
389
|
+
nodes_summary[instance_type]["deep_health_check_passed"] += 1
|
|
390
|
+
else:
|
|
391
|
+
# Resilience feature can only be enabled at InstanceGroup level
|
|
392
|
+
# So for the same instance type in one cluster, all nodes should
|
|
393
|
+
# have deep_health_check_status or none of them have this label
|
|
394
|
+
nodes_summary[instance_type]["deep_health_check_passed"] = "N/A"
|
|
395
|
+
|
|
396
|
+
health_status = labels[HP_HEALTH_STATUS_LABEL]
|
|
397
|
+
if health_status.startswith("Unschedulable"):
|
|
398
|
+
nodes_summary[instance_type]["unschedulable"] += 1
|
|
399
|
+
# Don't need to update accelerator devices information if
|
|
400
|
+
# node is unscheduable
|
|
401
|
+
continue
|
|
402
|
+
elif health_status == "Schedulable":
|
|
403
|
+
nodes_summary[instance_type]["schedulable"] += 1
|
|
404
|
+
else:
|
|
405
|
+
raise ValueError("Unexpected node health status")
|
|
406
|
+
|
|
407
|
+
# Calculate accelerator devices available
|
|
408
|
+
if (
|
|
409
|
+
not instance_type.startswith("ml.g")
|
|
410
|
+
and not instance_type.startswith("ml.p")
|
|
411
|
+
and not instance_type.startswith("ml.trn")
|
|
412
|
+
):
|
|
413
|
+
nodes_summary[instance_type]["accelerator_devices_available"] = "N/A"
|
|
414
|
+
continue
|
|
415
|
+
else:
|
|
416
|
+
if not node.status:
|
|
417
|
+
continue
|
|
418
|
+
gpu_allocatable = node.status.allocatable.get(NVIDIA_GPU_RESOURCE_LIMIT_KEY)
|
|
419
|
+
neuron_allocatable = node.status.allocatable.get(
|
|
420
|
+
"aws.amazon.com/neurondevice"
|
|
421
|
+
)
|
|
422
|
+
nodes_summary[instance_type]["accelerator_devices_available"] += (
|
|
423
|
+
int(gpu_allocatable) if gpu_allocatable else int(neuron_allocatable)
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Accelerator Devices available = Allocatable devices - Allocated devices
|
|
427
|
+
if node_name in nodes_resource_allocated_dict:
|
|
428
|
+
nodes_summary[instance_type]["accelerator_devices_available"] -= (
|
|
429
|
+
nodes_resource_allocated_dict[node_name]
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
logger.debug(f"nodes_summary: {nodes_summary}")
|
|
433
|
+
return nodes_summary
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
@click.command()
|
|
437
|
+
@click.option(
|
|
438
|
+
"--cluster-name",
|
|
439
|
+
type=click.STRING,
|
|
440
|
+
required=True,
|
|
441
|
+
help="Required. The HyperPod cluster name to configure with.",
|
|
442
|
+
)
|
|
443
|
+
@click.option(
|
|
444
|
+
"--region",
|
|
445
|
+
type=click.STRING,
|
|
446
|
+
required=False,
|
|
447
|
+
help="Optional. The region that the HyperPod and EKS clusters are located. If not specified, it will be set to the region from the current AWS account credentials.",
|
|
448
|
+
)
|
|
449
|
+
@click.option(
|
|
450
|
+
"--namespace",
|
|
451
|
+
"-n",
|
|
452
|
+
type=click.STRING,
|
|
453
|
+
required=False,
|
|
454
|
+
help="Optional. The namespace that you want to connect to. If not specified, Hyperpod cli commands will auto discover the accessible namespace.",
|
|
455
|
+
)
|
|
456
|
+
@click.option(
|
|
457
|
+
"--debug",
|
|
458
|
+
is_flag=True,
|
|
459
|
+
help="Enable debug mode",
|
|
460
|
+
)
|
|
461
|
+
def set_cluster_context(
|
|
462
|
+
cluster_name: str,
|
|
463
|
+
region: Optional[str],
|
|
464
|
+
debug: bool,
|
|
465
|
+
namespace: str,
|
|
466
|
+
) -> None:
|
|
467
|
+
"""
|
|
468
|
+
Connect to a HyperPod EKS cluster.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
cluster_name (str): The name of the HyperPod EKS cluster to connect to.
|
|
472
|
+
namespace (str): The namespace connect to. Default as 'default' namespace.
|
|
473
|
+
debug (bool): Enable debug mode.
|
|
474
|
+
region (Optional[str]): The AWS region where the HyperPod EKS cluster resides.
|
|
475
|
+
If not provided, the default region from the AWS credentials will be used.
|
|
476
|
+
|
|
477
|
+
Returns:
|
|
478
|
+
None
|
|
479
|
+
"""
|
|
480
|
+
if debug:
|
|
481
|
+
set_logging_level(logger, logging.DEBUG)
|
|
482
|
+
validator = ClusterValidator()
|
|
483
|
+
botocore_config = botocore.config.Config(
|
|
484
|
+
user_agent_extra=get_user_agent_extra_suffix()
|
|
485
|
+
)
|
|
486
|
+
session = boto3.Session(region_name=region) if region else boto3.Session()
|
|
487
|
+
if not validator.validate_aws_credential(session):
|
|
488
|
+
logger.error("Cannot connect to HyperPod cluster due to aws credentials error")
|
|
489
|
+
sys.exit(1)
|
|
490
|
+
|
|
491
|
+
try:
|
|
492
|
+
sm_client = get_sagemaker_client(session, botocore_config)
|
|
493
|
+
hp_cluster_details = sm_client.describe_cluster(ClusterName=cluster_name)
|
|
494
|
+
logger.debug("Fetched hyperpod cluster details")
|
|
495
|
+
store_current_hyperpod_context(hp_cluster_details)
|
|
496
|
+
eks_cluster_arn = hp_cluster_details["Orchestrator"]["Eks"]["ClusterArn"]
|
|
497
|
+
logger.debug(
|
|
498
|
+
f"hyperpod cluster's EKS orchestrator cluster arn: {eks_cluster_arn}"
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
eks_name = get_name_from_arn(eks_cluster_arn)
|
|
502
|
+
_update_kube_config(eks_name, region, None)
|
|
503
|
+
k8s_client = KubernetesClient()
|
|
504
|
+
k8s_client.set_context(eks_cluster_arn, namespace)
|
|
505
|
+
except botocore.exceptions.NoRegionError:
|
|
506
|
+
logger.error(
|
|
507
|
+
f"Please ensure you configured AWS default region or use '--region' argument to specify the region"
|
|
508
|
+
)
|
|
509
|
+
sys.exit(1)
|
|
510
|
+
except Exception as e:
|
|
511
|
+
logger.error(
|
|
512
|
+
f"Unexpected error happens when try to connect to cluster {cluster_name}. Error: {e}"
|
|
513
|
+
)
|
|
514
|
+
sys.exit(1)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
@click.command()
|
|
518
|
+
@click.option(
|
|
519
|
+
"--debug",
|
|
520
|
+
is_flag=True,
|
|
521
|
+
help="Enable debug mode",
|
|
522
|
+
)
|
|
523
|
+
def get_cluster_context(
|
|
524
|
+
debug: bool,
|
|
525
|
+
) -> Tuple[Any, str]:
|
|
526
|
+
"""
|
|
527
|
+
Get all the context related to the current set Cluster
|
|
528
|
+
|
|
529
|
+
Args:
|
|
530
|
+
debug (bool): Enable debug mode.
|
|
531
|
+
|
|
532
|
+
Returns:
|
|
533
|
+
None
|
|
534
|
+
"""
|
|
535
|
+
if debug:
|
|
536
|
+
set_logging_level(logger, logging.DEBUG)
|
|
537
|
+
|
|
538
|
+
try:
|
|
539
|
+
current_context = get_cluster_context_util()
|
|
540
|
+
print(f"Cluster context:{current_context}")
|
|
541
|
+
except botocore.exceptions.NoRegionError:
|
|
542
|
+
logger.error(
|
|
543
|
+
f"Please ensure you configured AWS default region or use '--region' argument to specify the region"
|
|
544
|
+
)
|
|
545
|
+
sys.exit(1)
|
|
546
|
+
except Exception as e:
|
|
547
|
+
logger.error(
|
|
548
|
+
f"Unexpected error happens when try to fetch cluster context. Error: {e}"
|
|
549
|
+
)
|
|
550
|
+
sys.exit(1)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
@click.command()
|
|
555
|
+
@click.option("--grafana", is_flag=True, help="Returns Grafana Dashboard URL")
|
|
556
|
+
@click.option("--prometheus", is_flag=True, help="Returns Prometheus Workspace URL")
|
|
557
|
+
@click.option("--list", is_flag=True, help="Returns list of available metrics")
|
|
558
|
+
def get_monitoring(grafana: bool, prometheus: bool, list: bool) -> None:
|
|
559
|
+
"""Get monitoring configurations for Hyperpod cluster"""
|
|
560
|
+
try:
|
|
561
|
+
if not any([grafana, prometheus, list]):
|
|
562
|
+
print("Error: Please select at least one option")
|
|
563
|
+
print("Usage : hyp get-monitoring --grafana/--prometheus/--list/--help")
|
|
564
|
+
return
|
|
565
|
+
if not is_observability_addon_enabled(get_eks_cluster_name()):
|
|
566
|
+
print("Observability addon is not enabled for this cluster")
|
|
567
|
+
sys.exit(1)
|
|
568
|
+
monitor_config = get_monitoring_config()
|
|
569
|
+
if prometheus:
|
|
570
|
+
print(f"Prometheus workspace URL: {monitor_config.prometheusURL}")
|
|
571
|
+
if grafana:
|
|
572
|
+
print(f"Grafana dashboard URL: {monitor_config.grafanaURL}")
|
|
573
|
+
if list:
|
|
574
|
+
metrics_data = monitor_config.availableMetrics
|
|
575
|
+
print(tabulate([[k, v.get('level', v.get('enabled'))] for k, v in metrics_data.items()],
|
|
576
|
+
headers=['Metric', 'Level/Enabled'], tablefmt='presto'))
|
|
577
|
+
except Exception as e:
|
|
578
|
+
logger.error(f"Failed to get metrics: {e}")
|
|
579
|
+
sys.exit(1)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def _update_kube_config(
|
|
584
|
+
eks_name: str,
|
|
585
|
+
region: Optional[str],
|
|
586
|
+
config_file: Optional[str],
|
|
587
|
+
) -> None:
|
|
588
|
+
"""
|
|
589
|
+
Update the local kubeconfig with the specified EKS cluster details.
|
|
590
|
+
|
|
591
|
+
Args:
|
|
592
|
+
eks_name (str): The name of the EKS cluster to update in the kubeconfig.
|
|
593
|
+
region (Optional[str]): The AWS region where the EKS cluster resides.
|
|
594
|
+
If not provided, the default region from the AWS credentials will be used.
|
|
595
|
+
config_file (Optional[str]): The path to the kubeconfig file.
|
|
596
|
+
|
|
597
|
+
Raises:
|
|
598
|
+
RuntimeError: If the `aws eks update-kubeconfig` command fails to execute.
|
|
599
|
+
"""
|
|
600
|
+
|
|
601
|
+
# Construct the kubeconfig update command
|
|
602
|
+
# EKS doesn't provide boto3 API for this command
|
|
603
|
+
command = [
|
|
604
|
+
"aws",
|
|
605
|
+
"eks",
|
|
606
|
+
"update-kubeconfig",
|
|
607
|
+
"--name",
|
|
608
|
+
eks_name,
|
|
609
|
+
]
|
|
610
|
+
|
|
611
|
+
if region:
|
|
612
|
+
command.extend(["--region", region])
|
|
613
|
+
|
|
614
|
+
if config_file:
|
|
615
|
+
command.extend(["--kubeconfig", config_file])
|
|
616
|
+
|
|
617
|
+
# Validate command components
|
|
618
|
+
if not all(isinstance(arg, str) and arg.strip() for arg in command):
|
|
619
|
+
raise ValueError("Invalid command arguments")
|
|
620
|
+
|
|
621
|
+
try:
|
|
622
|
+
# Execute the command to update kubeconfig
|
|
623
|
+
subprocess.run(command, check=True)
|
|
624
|
+
except subprocess.CalledProcessError as e:
|
|
625
|
+
raise RuntimeError(f"Failed to update kubeconfig: {e}")
|
|
626
|
+
except (OSError, ValueError) as e:
|
|
627
|
+
raise RuntimeError(f"Invalid command execution: {e}")
|