1212import shlex
1313import subprocess
1414from collections .abc import Sequence
15- from typing import Any , Optional
15+ from typing import Any , Optional , cast
1616
1717import kubernetes as k8s
1818from absl import flags
2727from axlearn .cloud .gcp .k8s_http_route import LWSHTTPRoute
2828from axlearn .cloud .gcp .k8s_service import LWSService
2929from 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+ )
3035from 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
3642from axlearn .common .config import REQUIRED , ConfigOr , Required , config_class , maybe_instantiate
3743from 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