Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4f3d91a
test: import remaining topology e2e tests (TAS8-TAS16)
Ronkahn21 Feb 1, 2026
92d9b79
Update operator/e2e/yaml/tas-large-scale.yaml
Ronkahn21 Feb 3, 2026
147184f
fix: remove packDomain constraint from topology configuration
Ronkahn21 Feb 3, 2026
1091928
refactor: use loop for TAS16 PCS replicas verification
Ronkahn21 Feb 3, 2026
f7ed72a
refactor: replace name generation functions with utils for expected s…
Ronkahn21 Feb 3, 2026
df9c73c
refactor: eliminate replica duplication across TAS tests
Ronkahn21 Feb 3, 2026
01cf2d6
refactor: simplify expected subgroups creation in topology tests
Ronkahn21 Feb 3, 2026
ba26cdf
refactor: update logging messages and unify PodGroup topology verific…
Ronkahn21 Feb 4, 2026
c05cb57
refactor: apply DRY principles to topology tests
Ronkahn21 Feb 4, 2026
f02e7df
test: address PR feedback on topology tests
Ronkahn21 Feb 5, 2026
861493f
test: use lightweight busybox image for TAS tests
Ronkahn21 Feb 5, 2026
97b7154
test: add lightweight busybox container for test pods
Ronkahn21 Feb 5, 2026
1673428
test: remove merge conflict markers and clean up dependencies.yaml
Ronkahn21 Feb 5, 2026
b2218b5
test: add scaled PodGang verification to TAS10 and TAS15
Ronkahn21 Feb 5, 2026
5e6e3bb
fix: add missing testing package import to kai_topology
Ronkahn21 Feb 5, 2026
4a90877
fix: correct scaled PodGroup constraint logic
Ronkahn21 Feb 5, 2026
f84dca9
test: remove CPU requirements from TAS test YAMLs
Ronkahn21 Feb 5, 2026
7ffb877
test: add pod anti-affinity to 3 representative TAS YAMLs
Ronkahn21 Feb 6, 2026
6064fe3
test: add pod anti-affinity to remaining TAS YAMLs
Ronkahn21 Feb 6, 2026
7b7fb06
test: use CPU requests instead of pod anti-affinity
Ronkahn21 Feb 6, 2026
951edd5
test: increase CPU requests to 1000m in TAS YAMLs
Ronkahn21 Feb 6, 2026
781f83b
test: reduce CPU requests from 1000m to 400m in TAS YAMLs
Ronkahn21 Feb 6, 2026
745b2e6
test: use memory limits instead of CPU for pod density
Ronkahn21 Feb 6, 2026
7d3c834
test: reduce memory requests from 51m to 40m
Ronkahn21 Feb 6, 2026
fad0d38
test: fix memory unit from 40m to 40Mi
Ronkahn21 Feb 6, 2026
260500c
test: fix PR review issues from gflarity
Ronkahn21 Feb 8, 2026
3f2fa4a
test: remove pod anti-affinity and use 40Mi in tas-pcsg-scale
Ronkahn21 Feb 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion operator/e2e/dependencies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ images:
version: v0.13.0-rc1
- name: ghcr.io/nvidia/kai-scheduler/scheduler
version: v0.13.0-rc1

# Cert-manager
- name: quay.io/jetstack/cert-manager-controller
version: v1.14.4
Expand All @@ -42,6 +41,10 @@ images:
- name: quay.io/jetstack/cert-manager-ctl
version: v1.14.4

# Lightweight container for test pods
- name: busybox
version: latest

# Helm charts used in E2E tests
helmCharts:
# Kai Scheduler - gang scheduling for Kubernetes
Expand Down
672 changes: 647 additions & 25 deletions operator/e2e/tests/topology_test.go

Large diffs are not rendered by default.

110 changes: 103 additions & 7 deletions operator/e2e/utils/kai_topology.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ package utils
import (
"context"
"fmt"
"testing"
"time"

kaischedulingv2alpha2 "github.com/NVIDIA/KAI-scheduler/pkg/apis/scheduling/v2alpha2"
nameutils "github.com/ai-dynamo/grove/operator/api/common"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/dynamic"
"k8s.io/utils/ptr"
Expand All @@ -38,10 +40,30 @@ type ExpectedSubGroup struct {
PreferredTopologyLevel string
}

// PCSGCliqueConfig defines configuration for a single clique in a PCSG
type PCSGCliqueConfig struct {
Name string
PodCount int32
Constraint string
}

// ScaledPCSGConfig defines configuration for verifying a scaled PCSG replica
type ScaledPCSGConfig struct {
Name string
PCSGName string
PCSGReplica int
MinAvailable int
CliqueConfigs []PCSGCliqueConfig
Constraint string
}

// CreateExpectedStandalonePCLQSubGroup creates an ExpectedSubGroup for a standalone PodClique (not in PCSG)
// Name format: <pcs-name>-<pcs-replica>-<clique-name>
func CreateExpectedStandalonePCLQSubGroup(pcsName string, pcsReplica int, cliqueName string, minMember int32, topologyLevel string) ExpectedSubGroup {
name := GetStandalonePCLQSubGroupName(pcsName, pcsReplica, cliqueName)
name := nameutils.GeneratePodCliqueName(
nameutils.ResourceNameReplica{Name: pcsName, Replica: pcsReplica},
cliqueName,
)
return ExpectedSubGroup{
Name: name,
MinMember: minMember,
Expand All @@ -53,7 +75,11 @@ func CreateExpectedStandalonePCLQSubGroup(pcsName string, pcsReplica int, clique
// CreateExpectedPCSGParentSubGroup creates an ExpectedSubGroup for a PCSG parent (scaling group replica)
// Name format: <pcs-name>-<pcs-replica>-<sg-name>-<sg-replica>
func CreateExpectedPCSGParentSubGroup(pcsName string, pcsReplica int, sgName string, sgReplica int, topologyLevel string) ExpectedSubGroup {
name := GetPCSGParentSubGroupName(pcsName, pcsReplica, sgName, sgReplica)
pcsgFQN := nameutils.GeneratePodCliqueScalingGroupName(
nameutils.ResourceNameReplica{Name: pcsName, Replica: pcsReplica},
sgName,
)
name := fmt.Sprintf("%s-%d", pcsgFQN, sgReplica)
return ExpectedSubGroup{
Name: name,
MinMember: 0,
Expand All @@ -62,19 +88,41 @@ func CreateExpectedPCSGParentSubGroup(pcsName string, pcsReplica int, sgName str
}
}

// CreateExpectedPCLQInPCSGSubGroup creates an ExpectedSubGroup for a PodClique within a PCSG
// CreateExpectedPCLQInPCSGSubGroup creates an ExpectedSubGroup for a PodClique within a PCSG with parent
// Name format: <pcs-name>-<pcs-replica>-<sg-name>-<sg-replica>-<clique-name>
func CreateExpectedPCLQInPCSGSubGroup(pcsName string, pcsReplica int, sgName string, sgReplica int, cliqueName string, minMember int32, topologyLevel string) ExpectedSubGroup {
name := GetPCLQInPCSGSubGroupName(pcsName, pcsReplica, sgName, sgReplica, cliqueName)
parentName := GetPCSGParentSubGroupName(pcsName, pcsReplica, sgName, sgReplica)
return createExpectedPCLQInPCSGSubGroup(pcsName, pcsReplica, sgName, sgReplica, cliqueName, minMember, topologyLevel, true)
}

func createExpectedPCLQInPCSGSubGroup(pcsName string, pcsReplica int, sgName string, sgReplica int, cliqueName string,
minMember int32, topologyLevel string, hasParent bool) ExpectedSubGroup {
pcsgFQN := nameutils.GeneratePodCliqueScalingGroupName(
nameutils.ResourceNameReplica{Name: pcsName, Replica: pcsReplica},
sgName,
)
name := nameutils.GeneratePodCliqueName(
nameutils.ResourceNameReplica{Name: pcsgFQN, Replica: sgReplica},
cliqueName,
)
var parentPtr *string
if hasParent {
parentPtr = ptr.To(fmt.Sprintf("%s-%d", pcsgFQN, sgReplica))
}
return ExpectedSubGroup{
Name: name,
MinMember: minMember,
Parent: ptr.To(parentName),
Parent: parentPtr,
RequiredTopologyLevel: topologyLevel,
}
}

// CreateExpectedPCLQInPCSGSubGroupNoParent creates an ExpectedSubGroup for a PodClique within a PCSG without parent
// Used when PCSG has no topology constraint (no parent SubGroup created)
// Name format: <pcs-name>-<pcs-replica>-<sg-name>-<sg-replica>-<clique-name>
func CreateExpectedPCLQInPCSGSubGroupNoParent(pcsName string, pcsReplica int, sgName string, sgReplica int, cliqueName string, minMember int32, topologyLevel string) ExpectedSubGroup {
return createExpectedPCLQInPCSGSubGroup(pcsName, pcsReplica, sgName, sgReplica, cliqueName, minMember, topologyLevel, false)
}

// GetKAIPodGroupsForPCS retrieves all KAI PodGroups for a given PodCliqueSet by label selector
// KAI scheduler creates PodGroups with label: app.kubernetes.io/part-of=<pcs-name>
// Returns a list of PodGroups that tests can filter by owner reference if needed
Expand Down Expand Up @@ -233,7 +281,7 @@ func GetPodGroupForBasePodGangReplica(
return nil, fmt.Errorf("failed to get KAI PodGroups: %w", err)
}

basePodGangName := GetBasePodGangName(workloadName, pgsReplica)
basePodGangName := nameutils.GenerateBasePodGangName(nameutils.ResourceNameReplica{Name: workloadName, Replica: pgsReplica})
basePodGroup, err := FilterPodGroupByOwner(podGroups, basePodGangName)
if err != nil {
return nil, fmt.Errorf("failed to find PodGroup for PodGang %s: %w", basePodGangName, err)
Expand All @@ -259,3 +307,51 @@ func VerifyPodGroupTopology(

return nil
}

// VerifyScaledPCSGReplicaTopology verifies KAI PodGroup for ONE scaled PCSG replica.
// Scaled PodGroup top-level constraint: uses pcsConstraint ONLY if PCSG has NO constraint.
func VerifyScaledPCSGReplicaTopology(
ctx context.Context,
t *testing.T,
dynamicClient dynamic.Interface,
namespace string,
pcsName string,
pcsReplica int,
pcsgConfig ScaledPCSGConfig,
pcsConstraint string,
logger *Logger,
) {
podGroups, err := GetKAIPodGroupsForPCS(ctx, dynamicClient, namespace, pcsName)
if err != nil {
t.Fatalf("Failed to get KAI PodGroups: %v", err)
}

pcsgFQN := nameutils.GeneratePodCliqueScalingGroupName(
nameutils.ResourceNameReplica{Name: pcsName, Replica: pcsReplica},
pcsgConfig.PCSGName,
)

scaledPodGangName := nameutils.CreatePodGangNameFromPCSGFQN(pcsgFQN, pcsgConfig.PCSGReplica-pcsgConfig.MinAvailable)

scaledPodGroup, err := FilterPodGroupByOwner(podGroups, scaledPodGangName)
if err != nil {
t.Fatalf("Failed to find scaled PodGroup for %s: %v", scaledPodGangName, err)
}

var expectedSubGroups []ExpectedSubGroup

for _, cliqueConfig := range pcsgConfig.CliqueConfigs {
expectedSubGroups = append(expectedSubGroups,
CreateExpectedPCLQInPCSGSubGroupNoParent(pcsName, pcsReplica, pcsgConfig.PCSGName, pcsgConfig.PCSGReplica, cliqueConfig.Name, cliqueConfig.PodCount, cliqueConfig.Constraint))
}

scaledTopConstraint := pcsConstraint
if pcsgConfig.Constraint != "" {
scaledTopConstraint = pcsgConfig.Constraint
}

if err := VerifyPodGroupTopology(scaledPodGroup, scaledTopConstraint, "", expectedSubGroups, logger); err != nil {
t.Fatalf("Failed to verify scaled PodGroup %s (%s replica %d) topology: %v",
scaledPodGangName, pcsgConfig.Name, pcsgConfig.PCSGReplica, err)
}
}
45 changes: 0 additions & 45 deletions operator/e2e/utils/naming.go

This file was deleted.

66 changes: 66 additions & 0 deletions operator/e2e/utils/topology.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,3 +203,69 @@ func VerifyLabeledPodsInTopologyDomain(

return VerifyPodsInSameTopologyDomain(ctx, clientset, filteredPods, topologyKey, logger)
}

// PCSGTypeConfig defines configuration for a PCSG type verification
type PCSGTypeConfig struct {
Name string // Human-readable name (e.g., "decoder")
FQN string // Fully-qualified PCSG name
}

// VerifyPCSGReplicasInTopologyDomain verifies that each PCSG replica's pods
// are in the same topology domain (e.g., rack, host).
func VerifyPCSGReplicasInTopologyDomain(
ctx context.Context,
clientset kubernetes.Interface,
allPods []v1.Pod,
pcsgLabel string,
replicaCount int,
podsPerReplica int,
topologyLabel string,
logger *Logger,
) error {
for replica := 0; replica < replicaCount; replica++ {
replicaPods := FilterPodsByLabel(
FilterPodsByLabel(allPods, "grove.io/podcliquescalinggroup", pcsgLabel),
"grove.io/podcliquescalinggroup-replica-index",
fmt.Sprintf("%d", replica),
)
if len(replicaPods) != podsPerReplica {
return fmt.Errorf("expected %d PCSG replica %d pods, got %d", podsPerReplica, replica, len(replicaPods))
}
if err := VerifyPodsInSameTopologyDomain(ctx, clientset, replicaPods, topologyLabel, logger); err != nil {
return fmt.Errorf("failed to verify PCSG replica %d pods in same topology domain: %w", replica, err)
}
}
return nil
}

// VerifyMultiTypePCSGReplicas verifies multiple PCSG types across replicas.
// Each PCSG type has multiple replicas, and each replica's pods should be in the same topology domain.
func VerifyMultiTypePCSGReplicas(
ctx context.Context,
clientset kubernetes.Interface,
allPods []v1.Pod,
pcsgTypes []PCSGTypeConfig,
replicasPerType int,
podsPerReplica int,
topologyLabel string,
logger *Logger,
) error {
for _, pcsgType := range pcsgTypes {
for replica := 0; replica < replicasPerType; replica++ {
replicaPods := FilterPodsByLabel(
FilterPodsByLabel(allPods, "grove.io/podcliquescalinggroup", pcsgType.FQN),
"grove.io/podcliquescalinggroup-replica-index",
fmt.Sprintf("%d", replica),
)
if len(replicaPods) != podsPerReplica {
return fmt.Errorf("expected %d %s replica-%d pods, got %d",
podsPerReplica, pcsgType.Name, replica, len(replicaPods))
}
if err := VerifyPodsInSameTopologyDomain(ctx, clientset, replicaPods, topologyLabel, logger); err != nil {
return fmt.Errorf("failed to verify %s replica-%d pods in same topology domain: %w",
pcsgType.Name, replica, err)
}
}
}
return nil
}
88 changes: 88 additions & 0 deletions operator/e2e/yaml/tas-hierarchy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Workload 8: SP-1 - Full 3-Level Hierarchy with Cascading Constraints
# Test scenario: PCS (block) → PCSG (rack) → PCLQ (host) - demonstrating constraint inheritance
---
apiVersion: grove.io/v1alpha1
kind: PodCliqueSet
metadata:
name: tas-hierarchy
labels:
app: tas-hierarchy
spec:
replicas: 1
template:
topologyConstraint:
packDomain: block # PCS level - broadest
podCliqueScalingGroups:
- name: inference-group
replicas: 2
minAvailable: 2
topologyConstraint:
packDomain: rack # PCSG level - stricter than parent
cliqueNames:
- prefill
- decode
cliques:
- name: prefill
labels:
kai.scheduler/queue: test
topologyConstraint:
packDomain: host # PCLQ level - strictest
spec:
roleName: prefill
replicas: 2
minAvailable: 2
podSpec:
schedulerName: kai-scheduler
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node_role.e2e.grove.nvidia.com
operator: In
values:
- agent
tolerations:
- key: node_role.e2e.grove.nvidia.com
operator: Equal
value: agent
effect: NoSchedule
containers:
- name: prefill
image: registry:5001/busybox:latest
command: ["sleep", "infinity"]
resources:
requests:
memory: 40Mi
- name: decode
labels:
kai.scheduler/queue: test
topologyConstraint:
packDomain: host # PCLQ level - strictest
spec:
roleName: decode
replicas: 2
minAvailable: 2
podSpec:
schedulerName: kai-scheduler
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node_role.e2e.grove.nvidia.com
operator: In
values:
- agent
tolerations:
- key: node_role.e2e.grove.nvidia.com
operator: Equal
value: agent
effect: NoSchedule
containers:
- name: decode
image: registry:5001/busybox:latest
command: ["sleep", "infinity"]
resources:
requests:
memory: 40Mi
6 changes: 4 additions & 2 deletions operator/e2e/yaml/tas-host-level.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ spec:
effect: NoSchedule
containers:
- name: worker
image: registry:5001/nginx:alpine-slim
image: registry:5001/busybox:latest
command: ["sleep", "infinity"]
resources:
requests:
memory: 30Mi
memory: 40Mi

Loading
Loading