Skip to content

Commit 57e7174

Browse files
authored
improvement(job submission): Enhance topology validation for 3D topologies (#5644)
1 parent 5231ccc commit 57e7174

6 files changed

Lines changed: 239 additions & 129 deletions

File tree

cmd/job/submit.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,6 @@ func init() {
155155
}
156156

157157
func runSubmitCmd(cmd *cobra.Command, args []string) error {
158-
if err := config.ValidateHardwareRequest(computeType, topology, placementPolicy); err != nil {
159-
return err
160-
}
161158
ttlSeconds, err := parseDurationToSeconds(ttlAfterFinished, "--gke-ttl-after-finished")
162159
if err != nil {
163160
return err

pkg/config/hardware.go

Lines changed: 113 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ var tpuFamilyDefaults = map[string]int{
3232

3333
var tpuRegex = regexp.MustCompile(`^v[4-6][ep]?(-\d+)?$`)
3434

35+
type tpu3DConstraints struct {
36+
maxCubes int
37+
}
38+
39+
var tpu3DConstraintsMap = map[string]tpu3DConstraints{
40+
"ct4": {maxCubes: 64},
41+
"ct5p": {maxCubes: 140},
42+
"tpu7x": {maxCubes: 144},
43+
}
44+
3545
var valid2DTPUFamilies = []string{"ct6e", "ct5lp", "v5litepod", "v5-lite", "v6e"}
3646
var valid3DTPUFamilies = []string{"v4", "v5p", "ct4p", "ct5p", "tpu7"}
3747

@@ -92,8 +102,8 @@ var ValidGPUAccelerators = map[string]bool{
92102
"nvidia-h100-mega-80gb": true,
93103
}
94104

95-
// 3D topologies for v4, v5p, tpu7x
96-
var allowed3DTopologies = map[int]string{
105+
// 3D topologies for v4, v5p
106+
var common3DTopologies = map[int]string{
97107
4: "2x2x1",
98108
8: "2x2x2",
99109
16: "2x2x4",
@@ -118,13 +128,9 @@ var allowed2DTopologies = map[int]string{
118128
256: "16x16",
119129
}
120130

121-
var valid3DShapes = make(map[string]bool)
122131
var valid2DShapes = make(map[string]bool)
123132

124133
func init() {
125-
for _, v := range allowed3DTopologies {
126-
valid3DShapes[v] = true
127-
}
128134
for _, v := range allowed2DTopologies {
129135
valid2DShapes[v] = true
130136
}
@@ -237,9 +243,6 @@ func CalculateAcceleratorNodes(machineType, topology string, acceleratorsPerVM i
237243

238244
// 2. Identify Accelerators per VM if not provided
239245
if acceleratorsPerVM <= 0 {
240-
// Resolve shorthand to full machine type if necessary
241-
machineType = ResolveMachineType(machineType)
242-
243246
acceleratorsPerVM = func() int {
244247
// Check for explicit accelerators count in machine_type (e.g., "-4t")
245248
if idx := strings.LastIndex(machineType, "-"); idx != -1 && strings.HasSuffix(machineType, "t") {
@@ -278,11 +281,9 @@ func ResolveMachineType(acceleratorType string) string {
278281
}
279282

280283
// matchesTPUFamily returns true if the accelerator type matches any of the given families.
281-
func matchesTPUFamily(acceleratorType string, families []string) bool {
282-
resolved := ResolveMachineType(acceleratorType)
283-
resolvedLower := strings.ToLower(resolved)
284+
func matchesTPUFamily(machineType string, families []string) bool {
284285
for _, f := range families {
285-
if strings.Contains(resolvedLower, f) {
286+
if strings.Contains(machineType, f) {
286287
return true
287288
}
288289
}
@@ -321,65 +322,81 @@ func GetCandidatesForShorthand(shorthand string) []string {
321322
return candidates
322323
}
323324

324-
// ResolveTopologyForChips returns the default topology for a given accelerator and total chips.
325-
func ResolveTopologyForChips(accelaratorType string) (string, error) {
326-
idx := strings.LastIndex(accelaratorType, "-")
327-
if idx == -1 {
328-
return "", fmt.Errorf("invalid accelerator type format for topology resolution: %s (expected prefix-suffix)", accelaratorType)
329-
}
330-
accelType := accelaratorType[:idx]
331-
suffix := accelaratorType[idx+1:]
332-
totalChips, err := strconv.Atoi(suffix)
325+
func parseShorthand(accelaratorType string) (size int, err error) {
326+
if strings.Count(accelaratorType, "-") != 1 {
327+
return 0, fmt.Errorf("invalid accelerator type format for topology resolution: %s (expected prefix-suffix)", accelaratorType)
328+
}
329+
if strings.HasPrefix(accelaratorType, "tpu7x") {
330+
return 0, fmt.Errorf("for TPU7x, topology should be passed explicitly via --topology flag. It cannot be passed as chip count via compute-type as a suffix (%q). Please use --topology=AxBxC", accelaratorType)
331+
}
332+
333+
parts := strings.Split(accelaratorType, "-")
334+
suffix := parts[1]
335+
size, err = strconv.Atoi(suffix)
333336
if err != nil {
334-
return "", fmt.Errorf("invalid chips value for accelerator type %s: %w", accelaratorType, err)
337+
if strings.Contains(suffix, "x") {
338+
return 0, fmt.Errorf("only total chip count can be passed via compute-type as a suffix (%q). Use --topology flag with full topology string instead", accelaratorType)
339+
}
340+
return 0, fmt.Errorf("invalid chips value for accelerator type %s: %w", accelaratorType, err)
335341
}
336-
resolved := ResolveMachineType(accelType)
337-
resolvedLower := strings.ToLower(resolved)
338342

339343
// For TPU v4 (v4-8), the shorthand suffix represents cores. We need to divide by 2 to get chips.
340344
if accelaratorType == "v4-8" {
341-
totalChips = totalChips / 2
345+
size = size / 2
342346
}
343347

344-
// 3D topologies for v4, v5p, tpu7x
345-
if strings.HasPrefix(resolvedLower, "v4") || strings.HasPrefix(resolvedLower, "v5p") || strings.HasPrefix(resolvedLower, "ct4") || strings.HasPrefix(resolvedLower, "ct5p") || strings.HasPrefix(resolvedLower, "tpu7") {
346-
if shape, exists := allowed3DTopologies[totalChips]; exists {
347-
return shape, nil
348+
return size, nil
349+
}
350+
351+
// ResolveTopologyForChips returns the default topology for a given accelerator and total chips.
352+
func ResolveTopologyForChips(computeType string, machineType string) (string, error) {
353+
totalChips, err := parseShorthand(computeType)
354+
if err != nil {
355+
return "", err
356+
}
357+
358+
// Validate size based on family
359+
isPowerOf2 := totalChips >= 1 && (totalChips&(totalChips-1)) == 0 && totalChips != 2
360+
if matchesTPUFamily(machineType, valid2DTPUFamilies) {
361+
if !(isPowerOf2) {
362+
return "", fmt.Errorf("invalid size %d in compute-type %q. Valid sizes are powers of 2, except 2", totalChips, computeType)
348363
}
349-
} else {
350-
// 2D topologies for v5litepod and v6e (default)
351364
if shape, exists := allowed2DTopologies[totalChips]; exists {
352365
return shape, nil
353366
}
367+
} else if matchesTPUFamily(machineType, valid3DTPUFamilies) {
368+
isMultipleOf64 := totalChips >= 64 && totalChips%64 == 0
369+
if !(isPowerOf2 || isMultipleOf64) {
370+
return "", fmt.Errorf("invalid size %d in compute-type %q. Valid sizes are powers of 2, or multiples of 64", totalChips, computeType)
371+
}
372+
if shape, exists := common3DTopologies[totalChips]; exists {
373+
return shape, nil
374+
}
354375
}
355376

356-
return "", fmt.Errorf("could not find a valid topology shape for %d chips with accelerator %s", totalChips, accelType)
377+
return "", fmt.Errorf("could not find a valid topology shape for %d chips with accelerator %s", totalChips, computeType)
357378
}
358379

359380
// ValidateHardwareRequest validates hardware requests for TPUs.
360-
func ValidateHardwareRequest(machineType, topology, placementPolicy string) error {
381+
func ValidateHardwareRequest(machineType, topology string) error {
361382
if !IsTPU(machineType) {
362383
return nil // Skip for non-TPUs
363384
}
364385

365-
if topology != "" {
366-
if err := validateTopologyMeshFormat(topology, machineType); err != nil {
367-
return err
368-
}
386+
if err := validateTopologyMeshFormat(topology, machineType); err != nil {
387+
return err
388+
}
369389

370-
if matchesTPUFamily(machineType, valid2DTPUFamilies) {
371-
if !valid2DShapes[topology] {
372-
return fmt.Errorf("requested carve footprint layout %s is not an authorized topology subset layout for %s", topology, machineType)
373-
}
374-
} else if matchesTPUFamily(machineType, valid3DTPUFamilies) {
375-
if !valid3DShapes[topology] {
376-
return fmt.Errorf("requested carve footprint layout %s is not an authorized topology subset layout for %s", topology, machineType)
377-
}
378-
} else {
379-
return fmt.Errorf("TPU type %q is recognized but its topology family is unknown; please report a bug to the toolkit maintainers.", machineType)
390+
if matchesTPUFamily(machineType, valid2DTPUFamilies) {
391+
if !valid2DShapes[topology] {
392+
return fmt.Errorf("requested carve footprint layout %s is not an authorized topology subset layout for %s", topology, machineType)
380393
}
394+
return nil
395+
} else if matchesTPUFamily(machineType, valid3DTPUFamilies) {
396+
return validate3DTopology(topology, machineType)
397+
} else {
398+
return fmt.Errorf("TPU type %q is recognized but its topology family is unknown; please report a bug to the toolkit maintainers.", machineType)
381399
}
382-
return nil
383400
}
384401

385402
func validateTopologyMeshFormat(requested string, accelType string) error {
@@ -401,6 +418,53 @@ func validateTopologyMeshFormat(requested string, accelType string) error {
401418
return nil
402419
}
403420

421+
func isValid3DDimension(x int) bool {
422+
return x >= 4 && x%4 == 0
423+
}
424+
425+
func validate3DTopology(topology string, machineType string) error {
426+
for _, v := range common3DTopologies {
427+
if topology == v {
428+
return nil
429+
}
430+
}
431+
432+
dims := strings.Split(topology, "x")
433+
a, _ := strconv.Atoi(dims[0])
434+
b, _ := strconv.Atoi(dims[1])
435+
c, _ := strconv.Atoi(dims[2])
436+
437+
if !isValid3DDimension(a) || !isValid3DDimension(b) || !isValid3DDimension(c) {
438+
return fmt.Errorf("topology %s dimensions must be >= 4 and multiples of 4 (unless it is a common base topology)", topology)
439+
}
440+
441+
maxCubes := 0
442+
found := false
443+
444+
for prefix, constraints := range tpu3DConstraintsMap {
445+
if strings.HasPrefix(machineType, prefix) {
446+
maxCubes = constraints.maxCubes
447+
found = true
448+
break
449+
}
450+
}
451+
452+
if !found {
453+
return fmt.Errorf("unknown 3D TPU family for machine type %s", machineType)
454+
}
455+
456+
cubes := (a / 4) * (b / 4) * (c / 4)
457+
if cubes > maxCubes {
458+
return fmt.Errorf("topology %s exceeds max cubes limit of %d (current: %d)", topology, maxCubes, cubes)
459+
}
460+
461+
if !(a <= b && b <= c) {
462+
return fmt.Errorf("topology %s dimensions must be in non-decreasing order (A <= B <= C)", topology)
463+
}
464+
465+
return nil
466+
}
467+
404468
// CheckTopologyContainment returns true if the requested topology fits within the container topology.
405469
func CheckTopologyContainment(requested, container string, accelType string) (bool, error) {
406470
reqDims := strings.Split(requested, "x")
@@ -422,16 +486,5 @@ func CheckTopologyContainment(requested, container string, accelType string) (bo
422486
}
423487
}
424488

425-
if requested != container {
426-
if matchesTPUFamily(accelType, valid2DTPUFamilies) {
427-
if !valid2DShapes[requested] {
428-
return false, fmt.Errorf("requested carve footprint layout %s is not an authorized topology subset layout", requested)
429-
}
430-
} else if matchesTPUFamily(accelType, valid3DTPUFamilies) {
431-
if !valid3DShapes[requested] {
432-
return false, fmt.Errorf("requested carve footprint layout %s is not an authorized topology subset layout", requested)
433-
}
434-
}
435-
}
436489
return true, nil
437490
}

pkg/config/hardware_test.go

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -312,8 +312,8 @@ func TestResolveTopologyForChips(t *testing.T) {
312312
name: "tpu7x 2048 chips",
313313
prefix: "tpu7x",
314314
totalChips: 2048,
315-
wantShape: "8x16x16",
316-
wantErr: false,
315+
wantShape: "",
316+
wantErr: true,
317317
},
318318
{
319319
name: "v6e 1 chip",
@@ -347,7 +347,8 @@ func TestResolveTopologyForChips(t *testing.T) {
347347

348348
for _, tt := range tests {
349349
t.Run(tt.name, func(t *testing.T) {
350-
got, err := ResolveTopologyForChips(fmt.Sprintf("%s-%d", tt.prefix, tt.totalChips))
350+
resolvedMachineType := ResolveMachineType(tt.prefix)
351+
got, err := ResolveTopologyForChips(fmt.Sprintf("%s-%d", tt.prefix, tt.totalChips), resolvedMachineType)
351352
if (err != nil) != tt.wantErr {
352353
t.Fatalf("ResolveTopologyForChips() error = %v, wantErr %v", err, tt.wantErr)
353354
}
@@ -449,25 +450,29 @@ func TestGetCandidatesForShorthand(t *testing.T) {
449450

450451
func TestValidateHardwareRequest(t *testing.T) {
451452
tests := []struct {
452-
name string
453-
machineType string
454-
topology string
455-
placementPolicy string
456-
wantErr bool
453+
name string
454+
machineType string
455+
topology string
456+
wantErr bool
457457
}{
458-
{"Valid TPU v4", "v4-8", "2x2x1", "", false},
459-
{"Valid TPU v6e", "v6e-8", "2x2", "", false},
460-
{"Invalid TPU v6e shape", "v6e-8", "3x3", "", true},
461-
{"Valid TPU v5litepod", "v5litepod-16", "4x4", "", false},
462-
{"Invalid TPU v5litepod shape", "v5litepod-16", "3x3", "", true},
463-
{"Invalid TPU v4 dimensions", "v4-8", "2x2", "", true}, // Needs 3D
464-
{"Invalid TPU v4 shape", "v4-8", "3x3x3", "", true},
465-
{"Unknown TPU family fails", "tpu-v7-8", "2x2x2", "", true},
466-
{"Non-TPU skips validation", "l4-1", "invalid", "", false},
458+
{"Valid TPU v4", "ct4p-hightpu-4t", "2x2x1", false},
459+
{"Valid TPU v6e", "v6e-8", "2x2", false},
460+
{"Invalid TPU v6e shape", "v6e-8", "3x3", true},
461+
{"Valid TPU v5litepod", "v5litepod-16", "4x4", false},
462+
{"Invalid TPU v5litepod shape", "v5litepod-16", "3x3", true},
463+
{"Invalid TPU v4 dimensions", "ct4p-hightpu-4t", "2x2", true}, // Needs 3D
464+
{"Invalid TPU v4 shape", "ct4p-hightpu-4t", "3x3x3", true},
465+
{"Unknown TPU family fails", "tpu-v7-8", "2x2x2", true},
466+
{"Non-TPU skips validation", "l4-1", "invalid", false},
467+
{"Valid non-standard v4 shape", "ct4p-hightpu-4t", "4x4x12", false},
468+
{"Valid non-standard v5p shape", "ct5p-hightpu-4t", "4x4x12", false},
469+
{"Invalid v5p shape (unsorted)", "ct5p-hightpu-4t", "4x12x4", true},
470+
{"Invalid v4 shape (unsorted)", "ct4p-hightpu-4t", "4x12x4", true},
471+
{"Topology exceeding max cubes", "ct4p-hightpu-4t", "16x16x32", true},
467472
}
468473
for _, tt := range tests {
469474
t.Run(tt.name, func(t *testing.T) {
470-
err := ValidateHardwareRequest(tt.machineType, tt.topology, tt.placementPolicy)
475+
err := ValidateHardwareRequest(tt.machineType, tt.topology)
471476
if (err != nil) != tt.wantErr {
472477
t.Errorf("ValidateHardwareRequest() error = %v, wantErr %v", err, tt.wantErr)
473478
}
@@ -533,20 +538,28 @@ func TestCheckTopologyContainment(t *testing.T) {
533538
wantErr: true,
534539
},
535540
{
536-
name: "V6e invalid subset shape",
541+
name: "V6e invalid subset shape (fits geometrically)",
537542
requested: "3x3",
538543
container: "4x4",
539544
accelType: "v6e",
540-
wantFit: false,
541-
wantErr: true,
545+
wantFit: true,
546+
wantErr: false,
542547
},
543548
{
544-
name: "V4 invalid subset shape",
549+
name: "V4 invalid subset shape (fits geometrically)",
545550
requested: "3x3x3",
546551
container: "4x4x4",
547552
accelType: "v4",
548-
wantFit: false,
549-
wantErr: true,
553+
wantFit: true,
554+
wantErr: false,
555+
},
556+
{
557+
name: "V4 valid non-standard subset shape",
558+
requested: "4x4x12",
559+
container: "4x4x16",
560+
accelType: "v4",
561+
wantFit: true,
562+
wantErr: false,
550563
},
551564
}
552565

0 commit comments

Comments
 (0)