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.
Files changed (66) hide show
  1. sagemaker/__init__.py +0 -0
  2. sagemaker/hyperpod/__init__.py +2 -0
  3. sagemaker/hyperpod/cli/__init__.py +0 -0
  4. sagemaker/hyperpod/cli/clients/__init__.py +12 -0
  5. sagemaker/hyperpod/cli/clients/kubernetes_client.py +365 -0
  6. sagemaker/hyperpod/cli/commands/__init__.py +0 -0
  7. sagemaker/hyperpod/cli/commands/cluster.py +627 -0
  8. sagemaker/hyperpod/cli/commands/inference.py +659 -0
  9. sagemaker/hyperpod/cli/commands/training.py +347 -0
  10. sagemaker/hyperpod/cli/constants/__init__.py +0 -0
  11. sagemaker/hyperpod/cli/constants/command_constants.py +105 -0
  12. sagemaker/hyperpod/cli/constants/exception_constants.py +13 -0
  13. sagemaker/hyperpod/cli/constants/hyperpod_instance_types.py +100 -0
  14. sagemaker/hyperpod/cli/constants/kueue_constants.py +17 -0
  15. sagemaker/hyperpod/cli/constants/pytorch_constants.py +15 -0
  16. sagemaker/hyperpod/cli/hyp_cli.py +132 -0
  17. sagemaker/hyperpod/cli/inference_utils.py +121 -0
  18. sagemaker/hyperpod/cli/service/__init__.py +12 -0
  19. sagemaker/hyperpod/cli/service/cancel_training_job.py +70 -0
  20. sagemaker/hyperpod/cli/service/discover_namespaces.py +108 -0
  21. sagemaker/hyperpod/cli/service/exec_command.py +75 -0
  22. sagemaker/hyperpod/cli/service/get_logs.py +136 -0
  23. sagemaker/hyperpod/cli/service/get_namespaces.py +59 -0
  24. sagemaker/hyperpod/cli/service/get_training_job.py +110 -0
  25. sagemaker/hyperpod/cli/service/list_pods.py +138 -0
  26. sagemaker/hyperpod/cli/service/list_training_jobs.py +178 -0
  27. sagemaker/hyperpod/cli/service/self_subject_access_review.py +47 -0
  28. sagemaker/hyperpod/cli/telemetry/__init__.py +12 -0
  29. sagemaker/hyperpod/cli/telemetry/user_agent.py +36 -0
  30. sagemaker/hyperpod/cli/templates/__init__.py +12 -0
  31. sagemaker/hyperpod/cli/templates/k8s_pytorch_job_template.py +74 -0
  32. sagemaker/hyperpod/cli/training_utils.py +200 -0
  33. sagemaker/hyperpod/cli/utils.py +183 -0
  34. sagemaker/hyperpod/cli/validators/__init__.py +12 -0
  35. sagemaker/hyperpod/cli/validators/cluster_validator.py +52 -0
  36. sagemaker/hyperpod/cli/validators/job_validator.py +346 -0
  37. sagemaker/hyperpod/cli/validators/validator.py +75 -0
  38. sagemaker/hyperpod/common/__init__.py +0 -0
  39. sagemaker/hyperpod/common/config/__init__.py +1 -0
  40. sagemaker/hyperpod/common/config/metadata.py +18 -0
  41. sagemaker/hyperpod/common/utils.py +299 -0
  42. sagemaker/hyperpod/inference/__init__.py +11 -0
  43. sagemaker/hyperpod/inference/config/__init__.py +0 -0
  44. sagemaker/hyperpod/inference/config/constants.py +18 -0
  45. sagemaker/hyperpod/inference/config/hp_endpoint_config.py +767 -0
  46. sagemaker/hyperpod/inference/config/hp_jumpstart_endpoint_config.py +572 -0
  47. sagemaker/hyperpod/inference/hp_endpoint.py +203 -0
  48. sagemaker/hyperpod/inference/hp_endpoint_base.py +223 -0
  49. sagemaker/hyperpod/inference/hp_jumpstart_endpoint.py +232 -0
  50. sagemaker/hyperpod/observability/MonitoringConfig.py +10 -0
  51. sagemaker/hyperpod/observability/__init__.py +0 -0
  52. sagemaker/hyperpod/observability/constants.py +2 -0
  53. sagemaker/hyperpod/observability/utils.py +64 -0
  54. sagemaker/hyperpod/training/__init__.py +9 -0
  55. sagemaker/hyperpod/training/config/__init__.py +1 -0
  56. sagemaker/hyperpod/training/config/hyperpod_pytorch_job_config.py +2977 -0
  57. sagemaker/hyperpod/training/config/hyperpod_pytorch_job_status.py +3083 -0
  58. sagemaker/hyperpod/training/hyperpod_pytorch_job.py +253 -0
  59. sagemaker_hyperpod-3.0.0.dist-info/METADATA +534 -0
  60. sagemaker_hyperpod-3.0.0.dist-info/RECORD +66 -0
  61. sagemaker_hyperpod-3.0.0.dist-info/WHEEL +5 -0
  62. sagemaker_hyperpod-3.0.0.dist-info/entry_points.txt +2 -0
  63. sagemaker_hyperpod-3.0.0.dist-info/licenses/LICENSE +175 -0
  64. sagemaker_hyperpod-3.0.0.dist-info/licenses/NOTICE +1 -0
  65. sagemaker_hyperpod-3.0.0.dist-info/top_level.txt +1 -0
  66. 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}")