pathwaysutils 0.1.7__tar.gz → 0.1.8__tar.gz
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.
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/PKG-INFO +1 -1
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/__init__.py +1 -1
- pathwaysutils-0.1.8/pathwaysutils/experimental/concatenate_by_mesh_axis.py +152 -0
- pathwaysutils-0.1.8/pathwaysutils/experimental/shared_pathways_service/deploy_pathways_service.py +261 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/experimental/shared_pathways_service/gke_utils.py +26 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/experimental/shared_pathways_service/isc_pathways.py +123 -8
- pathwaysutils-0.1.8/pathwaysutils/experimental/shared_pathways_service/metrics_collector.py +296 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/experimental/shared_pathways_service/run_connect_example.py +6 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/experimental/shared_pathways_service/run_workload.py +13 -0
- pathwaysutils-0.1.8/pathwaysutils/experimental/shared_pathways_service/yamls/pw-service.yaml +152 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/jax/__init__.py +20 -2
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/profiling.py +10 -1
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/test/profiling_test.py +52 -0
- pathwaysutils-0.1.7/pathwaysutils/experimental/shared_pathways_service/yamls/pw-service-example.yaml +0 -18
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/LICENSE +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/README.md +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/_initialize.py +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/collect_profile.py +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/debug/__init__.py +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/debug/timing.py +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/debug/watchdog.py +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/elastic/__init__.py +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/elastic/elastic.py +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/elastic/manager.py +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/experimental/__init__.py +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/experimental/profiling.py +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/experimental/reshard.py +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/experimental/shared_pathways_service/README.md +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/experimental/shared_pathways_service/__init__.py +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/experimental/shared_pathways_service/validators.py +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/experimental/shared_pathways_service/yamls/pw-proxy.yaml +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/experimental/split_by_mesh_axis.py +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/lru_cache.py +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/persistence/__init__.py +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/persistence/helper.py +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/persistence/orbax_handler.py +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/plugin_executable.py +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/proxy_backend.py +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/reshard.py +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/test/__init__.py +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/test/debug/__init__.py +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/test/debug/timing_test.py +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/test/debug/watchdog_test.py +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/test/experimental/reshard_test.py +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/test/initialize_test.py +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/test/lru_cache_test.py +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/test/persistence_test.py +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/test/plugin_executable_test.py +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/test/proxy_backend_test.py +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pathwaysutils/test/reshard_test.py +0 -0
- {pathwaysutils-0.1.7 → pathwaysutils-0.1.8}/pyproject.toml +0 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
2
|
+
# you may not use this file except in compliance with the License.
|
|
3
|
+
# You may obtain a copy of the License at
|
|
4
|
+
#
|
|
5
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
#
|
|
7
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
8
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
9
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
10
|
+
# See the License for the specific language governing permissions and
|
|
11
|
+
# limitations under the License.
|
|
12
|
+
|
|
13
|
+
"""Pathwaysutils JAX concatenate_by_mesh_axis."""
|
|
14
|
+
|
|
15
|
+
from collections.abc import Sequence
|
|
16
|
+
import itertools
|
|
17
|
+
from typing import Any
|
|
18
|
+
import jax
|
|
19
|
+
import numpy as np
|
|
20
|
+
from pathwaysutils import jax as pw_jax
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def concatenate_by_mesh_axis(
|
|
24
|
+
array_trees: Sequence[Any],
|
|
25
|
+
mesh_axis: str,
|
|
26
|
+
) -> Any:
|
|
27
|
+
"""Concatenates meshes by an axis. Returns arrays on the concatenated mesh.
|
|
28
|
+
|
|
29
|
+
Note: This API always donates the given arrays.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
array_trees: Sequence of PyTrees of JAX arrays with `NamedSharding`. All
|
|
33
|
+
PyTrees in the sequence have the same structure. All arrays in each PyTree
|
|
34
|
+
are sharded/replicated on the same mesh. The input arrays are always
|
|
35
|
+
donated.
|
|
36
|
+
mesh_axis: Mesh axis to concatenate.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
A PyTree with the same structure as `array_trees[i]`. It has arrays with
|
|
40
|
+
their shards concatenated to match a concatenated mesh.
|
|
41
|
+
"""
|
|
42
|
+
if not array_trees:
|
|
43
|
+
return array_trees
|
|
44
|
+
|
|
45
|
+
def _get_named_sharding(array: jax.Array) -> jax.sharding.NamedSharding:
|
|
46
|
+
if not isinstance(array, jax.Array):
|
|
47
|
+
raise ValueError(f"Elements must be jax.Array. Got {type(array)}")
|
|
48
|
+
sharding = array.sharding
|
|
49
|
+
if not isinstance(sharding, jax.sharding.NamedSharding):
|
|
50
|
+
raise ValueError(f"Expected NamedSharding. Got {type(sharding)}")
|
|
51
|
+
return sharding
|
|
52
|
+
|
|
53
|
+
flats_and_defs = [jax.tree_util.tree_flatten(at) for at in array_trees]
|
|
54
|
+
flattened_arrays = [fd[0] for fd in flats_and_defs]
|
|
55
|
+
treedefs = [fd[1] for fd in flats_and_defs]
|
|
56
|
+
input_treedef = treedefs[0]
|
|
57
|
+
for td in treedefs[1:]:
|
|
58
|
+
if td != input_treedef:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
"All array trees must have the same treedef. Got"
|
|
61
|
+
f" {td} vs. {input_treedef}"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Convert to have the output array structure in the outer list, and each entry
|
|
65
|
+
# be a list of arrays from each shard for the concatenated output array.
|
|
66
|
+
input_flat_arrays = list(zip(*flattened_arrays))
|
|
67
|
+
|
|
68
|
+
if not flattened_arrays[0]:
|
|
69
|
+
return array_trees[0]
|
|
70
|
+
|
|
71
|
+
# Extract the shared mesh from each PyTree (from an arbitrary array in each).
|
|
72
|
+
meshes_to_concatenate = [
|
|
73
|
+
_get_named_sharding(array).mesh for array in input_flat_arrays[0]
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
# Validate that the meshes are compatible.
|
|
77
|
+
reference_mesh = meshes_to_concatenate[0]
|
|
78
|
+
if mesh_axis not in reference_mesh.axis_names:
|
|
79
|
+
raise ValueError(
|
|
80
|
+
f"mesh_axis '{mesh_axis}' not found in mesh axis names:"
|
|
81
|
+
f" {reference_mesh.axis_names}"
|
|
82
|
+
)
|
|
83
|
+
mesh_axis_idx = reference_mesh.axis_names.index(mesh_axis)
|
|
84
|
+
for mesh in meshes_to_concatenate:
|
|
85
|
+
if mesh.axis_names != reference_mesh.axis_names:
|
|
86
|
+
raise ValueError(
|
|
87
|
+
"Meshes must have the same axis names. Got"
|
|
88
|
+
f" {mesh} vs. {reference_mesh}."
|
|
89
|
+
)
|
|
90
|
+
if (
|
|
91
|
+
mesh.axis_sizes[:mesh_axis_idx]
|
|
92
|
+
!= reference_mesh.axis_sizes[:mesh_axis_idx]
|
|
93
|
+
or mesh.axis_sizes[mesh_axis_idx + 1 :]
|
|
94
|
+
!= reference_mesh.axis_sizes[mesh_axis_idx + 1 :]
|
|
95
|
+
):
|
|
96
|
+
raise ValueError(
|
|
97
|
+
"Arrays must have the same mesh axis sizes for all axes except"
|
|
98
|
+
f" {mesh_axis}. Got {mesh} vs. {reference_mesh}."
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Construct list of the mesh axis section boundaries.
|
|
102
|
+
devices = [mesh.devices for mesh in meshes_to_concatenate]
|
|
103
|
+
mesh_axis_sections = list(
|
|
104
|
+
itertools.accumulate(
|
|
105
|
+
mesh.axis_sizes[mesh_axis_idx] for mesh in meshes_to_concatenate
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
concatenated_mesh = jax.sharding.Mesh(
|
|
110
|
+
np.concatenate(devices, mesh_axis_idx),
|
|
111
|
+
axis_names=reference_mesh.axis_names,
|
|
112
|
+
axis_types=reference_mesh.axis_types,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def _get_output_sharding(
|
|
116
|
+
arrays: Sequence[jax.Array],
|
|
117
|
+
) -> jax.sharding.NamedSharding:
|
|
118
|
+
reference_sharding = _get_named_sharding(arrays[0])
|
|
119
|
+
reference_spec = reference_sharding.spec
|
|
120
|
+
return jax.sharding.NamedSharding(
|
|
121
|
+
concatenated_mesh,
|
|
122
|
+
reference_spec,
|
|
123
|
+
memory_kind=reference_sharding.memory_kind,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def _sharded_dim_idx_for_sharding(
|
|
127
|
+
sharding: jax.sharding.NamedSharding,
|
|
128
|
+
) -> int:
|
|
129
|
+
sharded_dim_idx = -1
|
|
130
|
+
for dim_idx, dim_spec in enumerate(sharding.spec):
|
|
131
|
+
flat_dim_spec, _ = jax.tree_util.tree_flatten(dim_spec)
|
|
132
|
+
if mesh_axis in flat_dim_spec:
|
|
133
|
+
sharded_dim_idx = dim_idx
|
|
134
|
+
break
|
|
135
|
+
return sharded_dim_idx
|
|
136
|
+
|
|
137
|
+
out_shardings = [_get_output_sharding(arrays) for arrays in input_flat_arrays]
|
|
138
|
+
sharded_dim_idxs = [
|
|
139
|
+
_sharded_dim_idx_for_sharding(sharding) for sharding in out_shardings
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
flat_output_arrays = pw_jax.concatenate_by_mesh_axis(
|
|
143
|
+
arrays=input_flat_arrays,
|
|
144
|
+
sharded_dim_idxs=sharded_dim_idxs,
|
|
145
|
+
mesh_axis_sizes=concatenated_mesh.axis_sizes,
|
|
146
|
+
mesh_axis_idx=mesh_axis_idx,
|
|
147
|
+
mesh_axis_sections=mesh_axis_sections,
|
|
148
|
+
out_shardings=out_shardings,
|
|
149
|
+
donate=True,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return jax.tree_util.tree_unflatten(input_treedef, flat_output_arrays)
|
pathwaysutils-0.1.8/pathwaysutils/experimental/shared_pathways_service/deploy_pathways_service.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""Deploys Pathways service to a Kubernetes cluster using a JobSet template."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Sequence
|
|
4
|
+
import dataclasses
|
|
5
|
+
import logging
|
|
6
|
+
import math
|
|
7
|
+
import os
|
|
8
|
+
import string
|
|
9
|
+
from typing import Any
|
|
10
|
+
from absl import app
|
|
11
|
+
from absl import flags
|
|
12
|
+
from kubernetes import client
|
|
13
|
+
from kubernetes import config
|
|
14
|
+
import yaml
|
|
15
|
+
|
|
16
|
+
_logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
# Flag definitions
|
|
19
|
+
FLAGS = flags.FLAGS
|
|
20
|
+
_JOBSET_NAME = flags.DEFINE_string(
|
|
21
|
+
"jobset_name", "pathways-service", "Name of the JobSet"
|
|
22
|
+
)
|
|
23
|
+
_JAX_VERSION = flags.DEFINE_string(
|
|
24
|
+
"jax_version", "0.9.0", "JAX version (e.g., 0.9.0)"
|
|
25
|
+
)
|
|
26
|
+
_SERVER_IMAGE = flags.DEFINE_string(
|
|
27
|
+
"server_image", None, "Full path to the server Docker image"
|
|
28
|
+
)
|
|
29
|
+
_TPU_TYPE = flags.DEFINE_enum(
|
|
30
|
+
"tpu_type", "v6e", ["v5e", "v5p", "v6e", "tpu7x"], "TPU type"
|
|
31
|
+
)
|
|
32
|
+
_TOPOLOGY = flags.DEFINE_string(
|
|
33
|
+
"topology", "2x2", "TPU topology (e.g., 4x8, 2x2x2)"
|
|
34
|
+
)
|
|
35
|
+
_NUM_SLICES = flags.DEFINE_integer(
|
|
36
|
+
"num_slices", 2, "Number of TPU slices"
|
|
37
|
+
)
|
|
38
|
+
_GCS_BUCKET = flags.DEFINE_string(
|
|
39
|
+
"gcs_bucket",
|
|
40
|
+
"gs://pathways-test-bucket",
|
|
41
|
+
"GCS bucket name for scratch space",
|
|
42
|
+
)
|
|
43
|
+
_TEMPLATE_FILE = flags.DEFINE_string(
|
|
44
|
+
"template_file",
|
|
45
|
+
os.path.join(
|
|
46
|
+
os.path.dirname(__file__), "yamls/pw-service.yaml",
|
|
47
|
+
),
|
|
48
|
+
"Path to the JobSet YAML template file",
|
|
49
|
+
)
|
|
50
|
+
_DRY_RUN = flags.DEFINE_boolean(
|
|
51
|
+
"dry_run",
|
|
52
|
+
False,
|
|
53
|
+
"If true, only print the generated YAML without deploying.",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclasses.dataclass(frozen=True)
|
|
58
|
+
class TPUConfig:
|
|
59
|
+
"""Holds configuration details for a specific TPU type."""
|
|
60
|
+
machine_type: str
|
|
61
|
+
chips_per_vm: int
|
|
62
|
+
accelerator_label: str
|
|
63
|
+
instance_prefix: str
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _validate_topology(topology):
|
|
67
|
+
"""Validates the topology flag format."""
|
|
68
|
+
try:
|
|
69
|
+
dims = topology.split("x")
|
|
70
|
+
if not (2 <= len(dims) <= 3):
|
|
71
|
+
return False
|
|
72
|
+
for dim in dims:
|
|
73
|
+
if not dim.isdigit():
|
|
74
|
+
return False
|
|
75
|
+
if int(dim) <= 0:
|
|
76
|
+
return False
|
|
77
|
+
return True
|
|
78
|
+
except ValueError:
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
flags.register_validator(
|
|
83
|
+
"topology",
|
|
84
|
+
_validate_topology,
|
|
85
|
+
message=(
|
|
86
|
+
"--topology must be in the format like 'AxB' or 'AxBxC', where A, B, C"
|
|
87
|
+
" are positive integers."
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_tpu_config(tpu_type: str) -> TPUConfig:
|
|
93
|
+
"""Returns a TPUConfig object containing TPU configuration details."""
|
|
94
|
+
tpu_configs = {
|
|
95
|
+
"v5e": TPUConfig(
|
|
96
|
+
machine_type="ct5lp-hightpu-4t",
|
|
97
|
+
chips_per_vm=4,
|
|
98
|
+
accelerator_label="tpu-v5-lite-podslice",
|
|
99
|
+
instance_prefix="tpuv5e",
|
|
100
|
+
),
|
|
101
|
+
"v5p": TPUConfig(
|
|
102
|
+
machine_type="ct5p-hightpu-4t",
|
|
103
|
+
chips_per_vm=4,
|
|
104
|
+
accelerator_label="tpu-v5p-slice",
|
|
105
|
+
instance_prefix="tpuv5p",
|
|
106
|
+
),
|
|
107
|
+
"v6e": TPUConfig(
|
|
108
|
+
machine_type="ct6e-standard-4t",
|
|
109
|
+
chips_per_vm=4,
|
|
110
|
+
accelerator_label="tpu-v6e-slice",
|
|
111
|
+
instance_prefix="tpuv6e",
|
|
112
|
+
),
|
|
113
|
+
"tpu7x": TPUConfig(
|
|
114
|
+
machine_type="tpu7x-standard-4t",
|
|
115
|
+
chips_per_vm=4,
|
|
116
|
+
accelerator_label="tpu-v7-slice",
|
|
117
|
+
instance_prefix="tpu7x",
|
|
118
|
+
),
|
|
119
|
+
}
|
|
120
|
+
if tpu_type not in tpu_configs:
|
|
121
|
+
raise ValueError(
|
|
122
|
+
f"Unsupported TPU type: {tpu_type}. Supported types are:"
|
|
123
|
+
f" {list(tpu_configs.keys())}"
|
|
124
|
+
)
|
|
125
|
+
return tpu_configs[tpu_type]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def calculate_vms_per_slice(topology: str, chips_per_vm: int) -> int:
|
|
129
|
+
"""Calculates the number of VMs per slice based on the topology."""
|
|
130
|
+
try:
|
|
131
|
+
dims = [int(d) for d in topology.split("x")]
|
|
132
|
+
total_chips = math.prod(dims)
|
|
133
|
+
if total_chips % chips_per_vm != 0:
|
|
134
|
+
raise ValueError(
|
|
135
|
+
f"Total chips ({total_chips}) in topology {topology} is not divisible"
|
|
136
|
+
f" by chips_per_vm ({chips_per_vm})"
|
|
137
|
+
)
|
|
138
|
+
return total_chips // chips_per_vm
|
|
139
|
+
except ValueError as e:
|
|
140
|
+
raise ValueError(
|
|
141
|
+
f"Invalid topology format: {topology}. Expected format like 'AxB' or"
|
|
142
|
+
f" 'AxBxC'. {e}"
|
|
143
|
+
) from e
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def load_and_substitute_template(
|
|
147
|
+
template_path: str, context: dict[str, Any]
|
|
148
|
+
) -> dict[str, Any]:
|
|
149
|
+
"""Loads and substitutes the string.Template from the given path."""
|
|
150
|
+
try:
|
|
151
|
+
with open(template_path, "r") as f:
|
|
152
|
+
template_str = f.read()
|
|
153
|
+
except OSError as err:
|
|
154
|
+
raise ValueError(
|
|
155
|
+
f"Could not read template file: {template_path}: {err}"
|
|
156
|
+
) from err
|
|
157
|
+
|
|
158
|
+
_logger.info("Template file: %s", template_path)
|
|
159
|
+
_logger.info("Context: %s", context)
|
|
160
|
+
template = string.Template(template_str)
|
|
161
|
+
_logger.info("Template: %s", template)
|
|
162
|
+
substituted_yaml = template.substitute(context)
|
|
163
|
+
return yaml.safe_load(substituted_yaml)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def deploy_jobset(jobset_yaml: dict[str, Any]) -> None:
|
|
167
|
+
"""Deploys the JobSet to the current Kubernetes cluster."""
|
|
168
|
+
try:
|
|
169
|
+
config.load_kube_config()
|
|
170
|
+
api = client.CustomObjectsApi()
|
|
171
|
+
api.create_namespaced_custom_object(
|
|
172
|
+
group="jobset.x-k8s.io",
|
|
173
|
+
version="v1alpha2",
|
|
174
|
+
namespace=jobset_yaml["metadata"]["namespace"],
|
|
175
|
+
body=jobset_yaml,
|
|
176
|
+
plural="jobsets",
|
|
177
|
+
)
|
|
178
|
+
_logger.info(
|
|
179
|
+
"JobSet '%s' created successfully.", jobset_yaml["metadata"]["name"]
|
|
180
|
+
)
|
|
181
|
+
except client.rest.ApiException:
|
|
182
|
+
_logger.exception("Error creating JobSet")
|
|
183
|
+
except config.ConfigException:
|
|
184
|
+
_logger.exception("Error loading Kubernetes configuration")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def run_deployment(
|
|
188
|
+
tpu_type,
|
|
189
|
+
topology,
|
|
190
|
+
num_slices,
|
|
191
|
+
jobset_name,
|
|
192
|
+
gcs_bucket,
|
|
193
|
+
server_image,
|
|
194
|
+
template_file,
|
|
195
|
+
dry_run,
|
|
196
|
+
deploy_func: Callable[[dict[str, Any]], None] = deploy_jobset,
|
|
197
|
+
) -> None:
|
|
198
|
+
"""Executes the deployment logic."""
|
|
199
|
+
tpu_config = get_tpu_config(tpu_type)
|
|
200
|
+
vms_per_slice = calculate_vms_per_slice(topology, tpu_config.chips_per_vm)
|
|
201
|
+
|
|
202
|
+
context = {
|
|
203
|
+
"JOBSET_NAME": jobset_name,
|
|
204
|
+
"SERVER_IMAGE": server_image,
|
|
205
|
+
"GCS_SCRATCH_LOCATION": gcs_bucket,
|
|
206
|
+
"NUM_SLICES": num_slices,
|
|
207
|
+
"INSTANCE_TYPE": f"{tpu_config.instance_prefix}:{topology}",
|
|
208
|
+
"VMS_PER_SLICE": vms_per_slice,
|
|
209
|
+
"CHIPS_PER_VM": tpu_config.chips_per_vm,
|
|
210
|
+
"ACCELERATOR_LABEL": tpu_config.accelerator_label,
|
|
211
|
+
"TOPOLOGY": topology,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
jobset_config = load_and_substitute_template(template_file, context)
|
|
215
|
+
|
|
216
|
+
_logger.info("--- Generated JobSet YAML ---")
|
|
217
|
+
_logger.info("\n%s", yaml.dump(jobset_config))
|
|
218
|
+
|
|
219
|
+
if not dry_run:
|
|
220
|
+
_logger.info("Deploying JobSet...")
|
|
221
|
+
deploy_func(jobset_config)
|
|
222
|
+
else:
|
|
223
|
+
_logger.info("Dry run mode, not deploying.")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def main(argv: Sequence[str]) -> None:
|
|
227
|
+
if len(argv) > 1:
|
|
228
|
+
raise app.UsageError("Too many command-line arguments.")
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
if (
|
|
232
|
+
flags.FLAGS["jax_version"].present
|
|
233
|
+
and flags.FLAGS["server_image"].present
|
|
234
|
+
):
|
|
235
|
+
raise ValueError("Cannot provide both --jax_version and --server_image")
|
|
236
|
+
|
|
237
|
+
if _SERVER_IMAGE.value:
|
|
238
|
+
server_image = _SERVER_IMAGE.value
|
|
239
|
+
else:
|
|
240
|
+
server_image = f"us-docker.pkg.dev/cloud-tpu-v2-images/pathways/server:jax-{_JAX_VERSION.value}"
|
|
241
|
+
|
|
242
|
+
run_deployment(
|
|
243
|
+
tpu_type=_TPU_TYPE.value,
|
|
244
|
+
topology=_TOPOLOGY.value,
|
|
245
|
+
num_slices=_NUM_SLICES.value,
|
|
246
|
+
jobset_name=_JOBSET_NAME.value,
|
|
247
|
+
gcs_bucket=_GCS_BUCKET.value,
|
|
248
|
+
server_image=server_image,
|
|
249
|
+
template_file=_TEMPLATE_FILE.value,
|
|
250
|
+
dry_run=_DRY_RUN.value,
|
|
251
|
+
)
|
|
252
|
+
except ValueError as e:
|
|
253
|
+
_logger.exception("Error: %s", e)
|
|
254
|
+
except FileNotFoundError:
|
|
255
|
+
_logger.exception(
|
|
256
|
+
"Error: Template file not found at %s", _TEMPLATE_FILE.value
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
if __name__ == "__main__":
|
|
261
|
+
app.run(main)
|
|
@@ -298,6 +298,32 @@ def enable_port_forwarding(
|
|
|
298
298
|
return (port_available, port_forward_process)
|
|
299
299
|
|
|
300
300
|
|
|
301
|
+
def stream_pod_logs(pod_name: str) -> subprocess.Popen[str]:
|
|
302
|
+
"""Streams logs from the given pod.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
pod_name: The name of the pod.
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
The process for streaming the logs.
|
|
309
|
+
|
|
310
|
+
Raises:
|
|
311
|
+
Exception: If the log streaming fails.
|
|
312
|
+
"""
|
|
313
|
+
command = ["kubectl", "logs", "-f", pod_name]
|
|
314
|
+
try:
|
|
315
|
+
return subprocess.Popen(
|
|
316
|
+
command,
|
|
317
|
+
stdout=subprocess.PIPE,
|
|
318
|
+
stderr=subprocess.STDOUT,
|
|
319
|
+
text=True,
|
|
320
|
+
bufsize=1, # Line buffered
|
|
321
|
+
)
|
|
322
|
+
except Exception as _:
|
|
323
|
+
_logger.exception("Error streaming logs for pod %s", pod_name)
|
|
324
|
+
raise
|
|
325
|
+
|
|
326
|
+
|
|
301
327
|
def delete_gke_job(job_name: str) -> None:
|
|
302
328
|
"""Deletes the given job from the GKE cluster.
|
|
303
329
|
|