Skip to content

Commit 41887e1

Browse files
Alexandre Jameschanglan
authored andcommitted
Support Multi Replica Topology Assignment
GitOrigin-RevId: 634bac5
1 parent b17d1af commit 41887e1

5 files changed

Lines changed: 481 additions & 135 deletions

File tree

axlearn/cloud/gcp/job.py

Lines changed: 149 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import shlex
1313
import subprocess
1414
from collections.abc import Sequence
15-
from typing import Any, Optional
15+
from typing import Any, Optional, cast
1616

1717
import kubernetes as k8s
1818
from absl import flags
@@ -27,12 +27,18 @@
2727
from axlearn.cloud.gcp.k8s_http_route import LWSHTTPRoute
2828
from axlearn.cloud.gcp.k8s_service import LWSService
2929
from axlearn.cloud.gcp.lws_utils import BaseLeaderWorkerTemplate
30+
from axlearn.cloud.gcp.system_characteristics import (
31+
_SystemCharacteristics,
32+
get_subblock_characteristics,
33+
get_system_characteristics,
34+
)
3035
from axlearn.cloud.gcp.utils import (
3136
custom_jobset_kwargs,
3237
custom_leaderworkerset_kwargs,
3338
delete_k8s_jobset,
3439
delete_k8s_leaderworkerset,
3540
)
41+
from axlearn.common.compiler_options import infer_tpu_version
3642
from axlearn.common.config import REQUIRED, ConfigOr, Required, config_class, maybe_instantiate
3743
from axlearn.common.utils import Nested
3844

@@ -202,41 +208,160 @@ def _get_topology_assignment(self) -> Optional[list[list[str]]]:
202208
)
203209
return None
204210

205-
def _get_tpu_job_name_from_replicated_jobs(
206-
self, replicated_jobs: Sequence[Nested[Any]]
207-
) -> Optional[str]:
208-
"""Extracts the name of the replicated job that has TPU node selectors.
211+
def _lookup_system_by_node_selectors(
212+
self, node_selector: dict[str, str]
213+
) -> Optional[tuple[str, _SystemCharacteristics]]:
214+
"""Looks up system characteristics from node selectors.
215+
216+
Args:
217+
node_selector: Kubernetes node selector dict
218+
219+
Returns:
220+
Tuple of tpu type and _SystemCharacteristics object if TPU selectors found
221+
None otherwise.
222+
"""
223+
gke_accelerator = node_selector.get("cloud.google.com/gke-tpu-accelerator")
224+
topology = node_selector.get("cloud.google.com/gke-tpu-topology")
225+
226+
if not (gke_accelerator and topology):
227+
return None
228+
229+
return get_system_characteristics(gke_accelerator, topology)
209230

210-
Iterates through the replicated jobs and finds the one that contains
211-
cloud.google.com/gke-tpu-accelerator and cloud.google.com/gke-tpu-topology
212-
node selectors, which indicate it's a TPU job. Returns that jobs name.
231+
def _get_tpu_replicated_job_topology_selection(
232+
self,
233+
replicated_jobs: Sequence[Nested[Any]],
234+
topology_assignments: list[list[str]],
235+
) -> dict[str, list[list[str]]]:
236+
"""Builds topology selection mapping from replicated jobs to subblock assignments.
237+
238+
This method analyzes TPU replicated jobs to determine their subblock requirements
239+
and distributes topology assignments accordingly. It's used for TPU slice
240+
auto-provisioning with super slicing support.
213241
214242
Args:
215243
replicated_jobs: List of replicated job specs from the JobSet.
244+
topology_assignments: List of subblock ID lists, where each inner list
245+
represents subblocks for one replica. Format: [["sb-1", "sb-2"], ["sb-3"]].
216246
217247
Returns:
218-
The name of the replicated job that contains TPU node selectors,
219-
or None if no such job is found.
248+
A dict mapping replicated job name to its topology assignment. The topology
249+
assignment is a list of lists, where each inner list contains subblock IDs
250+
for a specific replica of that job.
251+
252+
Example return value:
253+
{
254+
"tpu-worker": [["sb-1", "sb-2"], ["sb-3", "sb-4"]]
255+
}
256+
257+
Raises:
258+
ValueError: If TPU version doesn't support subblock super slicing.
259+
ValueError: If requested resources don't match topology assignments.
260+
ValueError: If insufficient topology assignments are provided.
261+
ValueError: If system lookup fails for job's node selectors.
220262
"""
263+
result = {}
264+
used_indices = set() # Track which assignments have been used
265+
221266
for job in replicated_jobs:
222-
# Navigate to the node selector in the job spec
223-
# Structure: job -> template -> spec -> template -> spec -> nodeSelector
224-
node_selector = (
267+
# Extract node selector
268+
node_selector: dict[str, str] = cast(
269+
dict[str, str],
225270
job.get("template", {})
226271
.get("spec", {})
227272
.get("template", {})
228273
.get("spec", {})
229-
.get("nodeSelector", {})
274+
.get("nodeSelector", {}),
230275
)
231276

232-
# Check if TPU node selectors are present
233-
has_tpu_accelerator = "cloud.google.com/gke-tpu-accelerator" in node_selector
234-
has_tpu_topology = "cloud.google.com/gke-tpu-topology" in node_selector
277+
# Look up system characteristics
278+
gke_accelerator = node_selector.get("cloud.google.com/gke-tpu-accelerator")
279+
topology = node_selector.get("cloud.google.com/gke-tpu-topology")
280+
281+
if not (gke_accelerator and topology):
282+
# Not a TPU job, skip
283+
continue
284+
285+
maybe_system_chars = self._lookup_system_by_node_selectors(node_selector)
286+
287+
if maybe_system_chars is None:
288+
job_name = str(job.get("name"))
289+
raise ValueError(
290+
f"Could not find system characteristics for job '{job_name}' with "
291+
f"accelerator='{gke_accelerator}' and topology='{topology}'. "
292+
f"This combination is not defined in "
293+
"USER_FACING_NAME_TO_SYSTEM_CHARACTERISTICS."
294+
)
295+
296+
tpu_type, system_chars = maybe_system_chars
297+
job_name = str(job.get("name"))
298+
num_replicas = int(job.get("replicas", 1))
299+
300+
# Get TPU version and check for subblock support
301+
tpu_version = infer_tpu_version(tpu_type)
302+
subblock_chars = get_subblock_characteristics(tpu_version)
303+
304+
if subblock_chars is None:
305+
raise ValueError(
306+
f"TPU version '{tpu_version}' (from device type '{system_chars.device_type}') "
307+
f"does not support subblock super slicing. Only TPU versions with configured "
308+
f"subblock mappings are supported for slice auto-provisioning."
309+
)
310+
311+
# Calculate Subblocks
312+
job_vms = system_chars.vms_per_slice
313+
subblock_vms = subblock_chars.vms_per_slice
314+
315+
if job_vms % subblock_vms != 0:
316+
raise ValueError(
317+
f"Job '{job_name}' requires {job_vms} VMs, which is not "
318+
f"evenly divisible by subblock size {subblock_vms} VMs. "
319+
f"Job topology: {topology}, Subblock topology: {subblock_chars.topology}"
320+
)
235321

236-
if has_tpu_accelerator and has_tpu_topology:
237-
return str(job.get("name"))
322+
# Calculate subblocks needed per replica
323+
subblocks_per_replica = job_vms // subblock_vms
324+
325+
# Find matching assignments for each replica
326+
job_assignments = []
327+
for replica_idx in range(num_replicas):
328+
# Find first unused assignment with correct number of subblocks
329+
found_assignment = None
330+
for idx, assignment in enumerate(topology_assignments):
331+
if idx not in used_indices and len(assignment) == subblocks_per_replica:
332+
found_assignment = assignment
333+
used_indices.add(idx)
334+
break
335+
336+
if found_assignment is None:
337+
raise ValueError(
338+
f"Could not find unused topology assignment with {subblocks_per_replica} "
339+
f"subblock(s) for job '{job_name}' replica {replica_idx}. "
340+
f"Total assignments: {len(topology_assignments)}, "
341+
f"already used: {len(used_indices)}."
342+
)
343+
344+
job_assignments.append(found_assignment)
345+
346+
result[job_name] = job_assignments
347+
logging.info(
348+
"Job '%s' (%s, topology=%s): %d replica(s) × %d subblock(s)",
349+
job_name,
350+
system_chars.device_type,
351+
topology,
352+
num_replicas,
353+
subblocks_per_replica,
354+
)
238355

239-
return None
356+
# Check if there are unused topology assignments
357+
if len(used_indices) < len(topology_assignments):
358+
logging.warning(
359+
"Not all topology assignments were consumed. Used %d out of %d assignment(s).",
360+
len(used_indices),
361+
len(topology_assignments),
362+
)
363+
364+
return result
240365

241366
def _build_jobset(self) -> Nested[Any]:
242367
"""Builds a config for a JobSet, which is a set of Jobs.
@@ -259,14 +384,10 @@ def _build_jobset(self) -> Nested[Any]:
259384
# Try to parse the env var and get the topology assignments.
260385
topology_assignment = self._get_topology_assignment()
261386
if cfg.enable_tpu_slice_auto_provisioning and topology_assignment:
262-
# Get the TPU job name from the replicated jobs
263-
tpu_job_name = self._get_tpu_job_name_from_replicated_jobs(replicated_jobs)
264-
if tpu_job_name is None:
265-
logging.warning(
266-
"TPU slice auto-provisioning enabled but no TPU job found in replicated jobs"
267-
)
268-
tpu_job_name = self._builder.config.job_name # Fallback to builder job name
269-
slice_selection = json.dumps({tpu_job_name: topology_assignment})
387+
slice_selection_dict = self._get_tpu_replicated_job_topology_selection(
388+
replicated_jobs, topology_assignment
389+
)
390+
slice_selection = json.dumps(slice_selection_dict)
270391
logging.info("Adding slice selection: %s to job set", slice_selection)
271392
labels.update({"tpu-provisioner.cloud.google.com/slice-autoprovisioning": "sync"})
272393
annotations.update(

0 commit comments

Comments
 (0)