Skip to content

Commit 00a24f9

Browse files
committed
refactor(trainer): simplify pod filtering per reviewer feedback
- Remove _select_best_pod_for_role() helper with status-priority logic - Sort pods by creation_timestamp and use seen set to skip duplicates - Add test case to parametrized test_get_job() per AGENTS.md pattern - Remove standalone test_get_job_with_pod_restarts() function
1 parent ab7d286 commit 00a24f9

2 files changed

Lines changed: 219 additions & 303 deletions

File tree

kubeflow/trainer/backends/kubernetes/backend.py

Lines changed: 22 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -86,58 +86,6 @@ def verify_backend(self) -> None:
8686
)
8787
return
8888

89-
def _select_best_pod_for_role(
90-
self, pods: list[models.IoK8sApiCoreV1Pod]
91-
) -> Optional[models.IoK8sApiCoreV1Pod]:
92-
"""
93-
Select the best Pod for a role based on status priority and creation timestamp.
94-
95-
Priority order:
96-
1. Running or Succeeded Pods (prefer most recent)
97-
2. Failed Pods (prefer most recent)
98-
3. Pending Pods (prefer most recent)
99-
4. Unknown Pods (prefer most recent)
100-
"""
101-
if not pods:
102-
return None
103-
104-
# Pod status priority (higher number = higher priority)
105-
status_priority = {
106-
constants.POD_RUNNING: 4, # Highest priority
107-
constants.POD_SUCCEEDED: 3, # Second highest
108-
constants.POD_FAILED: 2, # Third priority
109-
constants.POD_PENDING: 1, # Low priority
110-
constants.POD_UNKNOWN: 0, # Lowest priority
111-
}
112-
113-
# Group Pods by status priority
114-
pods_by_status = {}
115-
for pod in pods:
116-
status = pod.status.phase if pod.status else constants.POD_UNKNOWN
117-
priority = status_priority.get(status, 0)
118-
119-
if priority not in pods_by_status:
120-
pods_by_status[priority] = []
121-
pods_by_status[priority].append(pod)
122-
123-
# Find the highest priority status that has Pods
124-
highest_priority = max(pods_by_status.keys()) if pods_by_status else 0
125-
candidate_pods = pods_by_status[highest_priority]
126-
127-
# Among Pods with the same status, select the most recent one
128-
if len(candidate_pods) == 1:
129-
return candidate_pods[0]
130-
131-
# Sort by creation timestamp (most recent first)
132-
candidate_pods.sort(
133-
key=lambda p: (
134-
p.metadata.creation_timestamp or datetime.datetime.min.replace(tzinfo=timezone.utc)
135-
),
136-
reverse=True,
137-
)
138-
139-
return candidate_pods[0]
140-
14189
def list_runtimes(self) -> list[types.Runtime]:
14290
"""List available runtimes, preferring namespaced over cluster-scoped for duplicates.
14391
@@ -656,62 +604,6 @@ def __get_runtime_from_cr(
656604
),
657605
)
658606

659-
def _select_best_pod_for_role(
660-
self, pods: list[models.IoK8sApiCoreV1Pod]
661-
) -> Optional[models.IoK8sApiCoreV1Pod]:
662-
"""
663-
Select the best Pod for a role based on status priority and creation timestamp.
664-
665-
Priority order (higher priority = preferred):
666-
1. Running or Succeeded Pods (equal priority, prefer most recent)
667-
2. Failed Pods (prefer most recent)
668-
3. Pending Pods (prefer most recent)
669-
4. Unknown Pods (prefer most recent)
670-
671-
Both Running and Succeeded are considered healthy states with equal priority.
672-
When multiple pods share the same priority, the most recently created pod is selected.
673-
"""
674-
if not pods:
675-
return None
676-
677-
# Pod status priority (higher number = higher priority)
678-
# Running and Succeeded have equal priority as both are healthy states
679-
status_priority = {
680-
constants.POD_RUNNING: 4, # Highest priority (healthy)
681-
constants.POD_SUCCEEDED: 4, # Highest priority (healthy)
682-
constants.POD_FAILED: 2, # Lower priority
683-
constants.POD_PENDING: 1, # Low priority
684-
constants.POD_UNKNOWN: 0, # Lowest priority
685-
}
686-
687-
# Group Pods by status priority
688-
pods_by_status = {}
689-
for pod in pods:
690-
status = pod.status.phase if pod.status else constants.POD_UNKNOWN
691-
priority = status_priority.get(status, 0)
692-
693-
if priority not in pods_by_status:
694-
pods_by_status[priority] = []
695-
pods_by_status[priority].append(pod)
696-
697-
# Find the highest priority status that has Pods
698-
highest_priority = max(pods_by_status.keys()) if pods_by_status else 0
699-
candidate_pods = pods_by_status[highest_priority]
700-
701-
# Among Pods with the same priority, select the most recent one
702-
if len(candidate_pods) == 1:
703-
return candidate_pods[0]
704-
705-
# Sort by creation timestamp (most recent first)
706-
candidate_pods.sort(
707-
key=lambda p: (
708-
p.metadata.creation_timestamp or datetime.datetime.min.replace(tzinfo=timezone.utc)
709-
),
710-
reverse=True,
711-
)
712-
713-
return candidate_pods[0]
714-
715607
def _read_pod_logs(self, pod_name: str, container_name: str, follow: bool) -> Iterator[str]:
716608
"""Read logs from a pod container."""
717609
try:
@@ -787,9 +679,21 @@ def __get_trainjob_from_cr(
787679
if not pod_list:
788680
return trainjob
789681

790-
# Collect all Pods for this role
791-
pods_by_role: dict[str, list[models.IoK8sApiCoreV1Pod]] = {}
792-
for pod in pod_list.items:
682+
# Sort Pods by creation timestamp (newest first) to ensure we process
683+
# the most recent Pod for each role when duplicates exist (e.g., restarts)
684+
pods = sorted(
685+
pod_list.items,
686+
key=lambda p: (
687+
p.metadata.creation_timestamp
688+
or datetime.datetime.min.replace(tzinfo=timezone.utc)
689+
),
690+
reverse=True,
691+
)
692+
693+
# Track seen role keys to skip duplicate Pods (older ones)
694+
seen_roles: set[str] = set()
695+
696+
for pod in pods:
793697
# Pod must have labels to detect the TrainJob step.
794698
# Every Pod always has a single TrainJob step.
795699
if not (pod.metadata and pod.metadata.name and pod.metadata.labels and pod.spec):
@@ -805,24 +709,13 @@ def __get_trainjob_from_cr(
805709
else:
806710
key = role
807711

808-
if key not in pods_by_role:
809-
pods_by_role[key] = []
810-
pods_by_role[key].append(pod)
811-
812-
# Select the best Pod for each role using status-priority logic
813-
selected_pods: dict[str, models.IoK8sApiCoreV1Pod] = {}
814-
for role_key, pods in pods_by_role.items():
815-
best_pod = self._select_best_pod_for_role(pods)
816-
if best_pod:
817-
selected_pods[role_key] = best_pod
712+
# Skip if we've already processed a Pod for this role (newer one)
713+
if key in seen_roles:
714+
continue
715+
seen_roles.add(key)
818716

819-
# Process only the selected Pod for each role
820-
for _role_key, pod in selected_pods.items():
821717
# Get the Initializer step.
822-
if pod.metadata.labels[constants.JOBSET_RJOB_NAME_LABEL] in {
823-
constants.DATASET_INITIALIZER,
824-
constants.MODEL_INITIALIZER,
825-
}:
718+
if role in {constants.DATASET_INITIALIZER, constants.MODEL_INITIALIZER}:
826719
trainjob.steps.append(
827720
utils.get_trainjob_initializer_step(
828721
pod.metadata.name,
@@ -831,17 +724,14 @@ def __get_trainjob_from_cr(
831724
)
832725
)
833726
# Get the Node step.
834-
elif pod.metadata.labels[constants.JOBSET_RJOB_NAME_LABEL] in {
835-
constants.LAUNCHER,
836-
constants.NODE,
837-
}:
727+
elif role in {constants.LAUNCHER, constants.NODE}:
838728
trainjob.steps.append(
839729
utils.get_trainjob_node_step(
840730
pod.metadata.name,
841731
pod.spec,
842732
pod.status,
843733
trainjob.runtime,
844-
pod.metadata.labels[constants.JOBSET_RJOB_NAME_LABEL],
734+
role,
845735
int(pod.metadata.labels[constants.JOB_INDEX_LABEL]),
846736
)
847737
)

0 commit comments

Comments
 (0)