Skip to content

Commit 0b83215

Browse files
authored
Add DAG jobset_ttr_node_reboot for TPU node recovery validation (GoogleCloudPlatform#1269)
This change implements a new DAG `jobset_ttr_node_reboot`, designed to validate the self-healing capability of TPU JobSets under hardware-level disruption. This DAG automates fault injection by rebooting a random TPU node and validates that the JAX workload automatically recovers and reports performance metrics.
1 parent 9fb2f3b commit 0b83215

11 files changed

Lines changed: 341 additions & 56 deletions

dags/common/scheduling_helper/scheduling_helper.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ class DayOfWeek(enum.Enum):
6464
"jobset_uptime_validation": dt.timedelta(minutes=90),
6565
"jobset_ttr_drain_restart": DefaultTimeout,
6666
"tpu_info_metrics_verification": DefaultTimeout,
67+
"jobset_ttr_node_reboot": dt.timedelta(minutes=90),
6768
},
6869
TPU_INTERRUPTION_MOCK_CLUSTER.name: {
6970
"validate_interruption_count_gce_bare_metal_preemption": DefaultTimeout,

dags/tpu_observability/jobset_ttr_kill_process.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def kill_tpu_pod_workload(info: node_pool.Info, pod_name: str) -> None:
150150
node_pool=cluster_info,
151151
)
152152

153-
startup = jobset.create_jobset_startup_group(
153+
startup = jobset.create_jobset_startup_tasks(
154154
node_pool=cluster_info,
155155
jobset_config=jobset_config,
156156
workload_type=Workload.JAX_TPU_BENCHMARK,
@@ -189,7 +189,7 @@ def kill_tpu_pod_workload(info: node_pool.Info, pod_name: str) -> None:
189189
jobset_config,
190190
cluster_info,
191191
create_node_pool,
192-
startup.task_group,
192+
*startup.tasks,
193193
kill_tasks,
194194
wait_for_metric_upload,
195195
cleanup_workload,

dags/tpu_observability/jobset_ttr_node_pool_resize.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@
114114
node_pool=cluster_info,
115115
)
116116

117-
startup = jobset.create_jobset_startup_group(
117+
startup = jobset.create_jobset_startup_tasks(
118118
node_pool=cluster_info,
119119
jobset_config=jobset_config,
120120
workload_type=Workload.JAX_TPU_BENCHMARK,
@@ -154,7 +154,7 @@
154154
jobset_config,
155155
cluster_info,
156156
create_node_pool,
157-
startup.task_group,
157+
*startup.tasks,
158158
node_pool_resize,
159159
wait_for_metric_upload,
160160
cleanup_workload,
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""A DAG to test JobSet Time-To-Recover (TTR) metric by triggering a node reboot."""
16+
17+
import datetime
18+
from datetime import timedelta
19+
20+
from airflow import models
21+
from airflow.models.baseoperator import chain
22+
from airflow.utils.trigger_rule import TriggerRule
23+
24+
from dags import composer_env
25+
from dags.tpu_observability.utils import jobset_util as jobset
26+
from dags.tpu_observability.utils import node_pool_util as node_pool
27+
from dags.tpu_observability.utils.jobset_util import Workload
28+
from dags.tpu_observability.configs.common import (
29+
MachineConfigMap,
30+
GCS_CONFIG_PATH,
31+
GCS_JOBSET_CONFIG_PATH,
32+
)
33+
from dags.common.scheduling_helper.scheduling_helper import (
34+
SchedulingHelper,
35+
get_dag_timeout,
36+
)
37+
from dags.common.task_group_with_timeout import TaskGroupWithTimeout
38+
39+
40+
DAG_ID = "jobset_ttr_node_reboot"
41+
DAGRUN_TIMEOUT = get_dag_timeout(DAG_ID)
42+
SCHEDULE = SchedulingHelper.arrange_schedule_time(DAG_ID)
43+
44+
45+
# Keyword arguments are generated dynamically at runtime (pylint does not
46+
# know this signature).
47+
with models.DAG( # pylint: disable=unexpected-keyword-arg
48+
dag_id=DAG_ID,
49+
start_date=datetime.datetime(2026, 1, 21),
50+
schedule=SCHEDULE if composer_env.is_prod_env() else None,
51+
dagrun_timeout=DAGRUN_TIMEOUT,
52+
catchup=False,
53+
tags=[
54+
"cloud-ml-auto-solutions",
55+
"jobset",
56+
"time-to-recover",
57+
"tpu-observability",
58+
"node_reboot",
59+
"TPU",
60+
"v6e-16",
61+
],
62+
description=(
63+
"Tests JobSet TTR metric by rebooting a random TPU node to trigger "
64+
"recovery, then polls the metric for updates."
65+
),
66+
doc_md="""
67+
# JobSet Time-To-Recover (TTR) Test Using Random Node Reboot
68+
69+
### Description
70+
This DAG verifies that a TPU JobSet can recover from a hardware-level failure.
71+
It launches a JobSet, executes a `reboot` command on a random node via a
72+
privileged container (using nsenter), and uses a sensor to confirm that
73+
the TTR (Time-To-Recover) metric is recorded.
74+
75+
### Prerequisites
76+
This test requires an existing cluster to run.
77+
GKE Cluster with TPU v6e support.
78+
The JobSet must be configured with
79+
privileged: True to allow node-level interaction.
80+
81+
### Procedures
82+
First the node-pool is created, a jobset yaml is then launched on the cluster.
83+
After initialization, a random node reboot is triggered by executing
84+
`nsenter` through a privileged pod. This simulates a hardware failure
85+
by taking one of the TPU nodes offline. A sensor finally polls Cloud
86+
Monitoring to confirm the jobset TTR metric is updated.
87+
""",
88+
) as dag:
89+
for machine in MachineConfigMap:
90+
config = machine.value
91+
92+
# Keyword arguments are generated dynamically at runtime (pylint does not
93+
# know this signature).
94+
with TaskGroupWithTimeout( # pylint: disable=unexpected-keyword-arg
95+
group_id=f"v{config.tpu_version.value}",
96+
timeout=timedelta(minutes=90),
97+
):
98+
selector = jobset.generate_node_pool_selector("jobset-ttr-node-reboot")
99+
100+
jobset_config = jobset.build_jobset_from_gcs_yaml(
101+
gcs_path=GCS_JOBSET_CONFIG_PATH,
102+
dag_name=DAG_ID,
103+
node_pool_selector=selector,
104+
privileged=True,
105+
)
106+
107+
cluster_info = node_pool.build_node_pool_info_from_gcs_yaml.override(
108+
task_id="build_node_pool_info_from_gcs_yaml"
109+
)(
110+
gcs_path=GCS_CONFIG_PATH,
111+
dag_name=DAG_ID,
112+
is_prod=composer_env.is_prod_env(),
113+
machine_type=config.machine_version.value,
114+
tpu_topology=config.tpu_topology,
115+
node_pool_selector=selector,
116+
)
117+
118+
create_node_pool = node_pool.create.override(task_id="create_node_pool")(
119+
node_pool=cluster_info,
120+
)
121+
122+
startup = jobset.create_jobset_startup_tasks(
123+
node_pool=cluster_info,
124+
jobset_config=jobset_config,
125+
workload_type=Workload.JAX_TPU_BENCHMARK,
126+
)
127+
128+
target_pod = jobset.draw_random_pod(
129+
node_pool=cluster_info,
130+
jobset_config=jobset_config,
131+
)
132+
133+
reboot_node = jobset.operate_pod.override(task_id="reboot_node")(
134+
node_pool=cluster_info,
135+
operation=jobset.PodOperationSpec.Reboot(),
136+
pod_name=target_pod,
137+
namespace="default",
138+
)
139+
140+
wait_for_metric_upload = jobset.wait_for_jobset_ttr_to_be_found.override(
141+
task_id="wait_for_jobset_ttr_to_be_found"
142+
)(
143+
node_pool=cluster_info,
144+
jobset_config=jobset_config,
145+
)
146+
147+
cleanup_workload = jobset.end_workload.override(
148+
task_id="cleanup_workload", trigger_rule=TriggerRule.ALL_DONE
149+
)(node_pool=cluster_info, jobset_config=jobset_config).as_teardown(
150+
setups=startup.jobset_start_time
151+
)
152+
153+
cleanup_node_pool = node_pool.delete.override(
154+
task_id="cleanup_node_pool", trigger_rule=TriggerRule.ALL_DONE
155+
)(node_pool=cluster_info).as_teardown(
156+
setups=create_node_pool,
157+
)
158+
159+
chain(
160+
selector,
161+
jobset_config,
162+
cluster_info,
163+
create_node_pool,
164+
*startup.tasks,
165+
target_pod,
166+
reboot_node,
167+
wait_for_metric_upload,
168+
cleanup_workload,
169+
cleanup_node_pool,
170+
)

dags/tpu_observability/jobset_ttr_pod_delete.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@
110110
node_pool=cluster_info,
111111
)
112112

113-
startup = jobset.create_jobset_startup_group(
113+
startup = jobset.create_jobset_startup_tasks(
114114
node_pool=cluster_info,
115115
jobset_config=jobset_config,
116116
workload_type=Workload.JAX_TPU_BENCHMARK,
@@ -147,7 +147,7 @@
147147
jobset_config,
148148
cluster_info,
149149
create_node_pool,
150-
startup.task_group,
150+
*startup.tasks,
151151
delete_random_pod,
152152
wait_for_metric_upload,
153153
cleanup_workload,

dags/tpu_observability/jobset_ttr_rollback.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@
111111
node_pool=cluster_info,
112112
)
113113

114-
startup = jobset.create_jobset_startup_group(
114+
startup = jobset.create_jobset_startup_tasks(
115115
node_pool=cluster_info,
116116
jobset_config=jobset_config,
117117
workload_type=Workload.JAX_TPU_BENCHMARK,
@@ -148,7 +148,7 @@
148148
jobset_config,
149149
cluster_info,
150150
create_node_pool,
151-
startup.task_group,
151+
*startup.tasks,
152152
rollback_node_pool,
153153
wait_for_metric_upload,
154154
cleanup_workload,

dags/tpu_observability/jobset_uptime_validation.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ def get_current_time() -> TimeUtil:
122122
node_pool=cluster_info,
123123
)
124124

125-
startup = jobset.create_jobset_startup_group(
125+
startup = jobset.create_jobset_startup_tasks(
126126
node_pool=cluster_info,
127127
jobset_config=jobset_config,
128128
workload_type=Workload.JAX_TPU_BENCHMARK,
@@ -170,7 +170,7 @@ def get_current_time() -> TimeUtil:
170170
jobset_config,
171171
cluster_info,
172172
create_node_pool,
173-
startup.task_group,
173+
*startup.tasks,
174174
wait_for_jobset_uptime_data,
175175
clean_up_workload,
176176
jobset_clear_time,

dags/tpu_observability/tpu_info_format_validation_dags.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,7 @@ def generate_second_node_pool_name(
401401
node_pool=cluster_info_2,
402402
)
403403

404-
startup = jobset.create_jobset_startup_group(
404+
startup = jobset.create_jobset_startup_tasks(
405405
node_pool=cluster_info,
406406
jobset_config=jobset_config,
407407
workload_type=Workload.JAX_TPU_BENCHMARK,
@@ -508,7 +508,7 @@ def generate_second_node_pool_name(
508508
cluster_info,
509509
cluster_info_2,
510510
create_node_pool,
511-
startup.task_group,
511+
*startup.tasks,
512512
outputs_of_tpu_info,
513513
output_of_tpu_info,
514514
verification_group,

dags/tpu_observability/tpu_info_metrics_dag.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ def generate_second_node_pool_name(
327327

328328
_ = [create_first_node_pool, create_second_node_pool]
329329

330-
startup = jobset.create_jobset_startup_group(
330+
startup = jobset.create_jobset_startup_tasks(
331331
node_pool=cluster_info,
332332
jobset_config=jobset_config,
333333
workload_type=Workload.JAX_TPU_BENCHMARK,
@@ -417,7 +417,7 @@ def generate_second_node_pool_name(
417417
cluster_info,
418418
cluster_info_2,
419419
create_node_pool,
420-
startup.task_group,
420+
*startup.tasks,
421421
all_verification_groups,
422422
summary,
423423
clean_up_workload,

dags/tpu_observability/tpu_sdk_monitoring_validation_dag.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ def validate_monitoring_sdk(info: node_pool.Info, pod_name: str) -> None:
156156
node_pool=cluster_info,
157157
)
158158

159-
startup = jobset.create_jobset_startup_group(
159+
startup = jobset.create_jobset_startup_tasks(
160160
node_pool=cluster_info,
161161
jobset_config=jobset_config,
162162
workload_type=Workload.JAX_TPU_BENCHMARK,
@@ -188,7 +188,7 @@ def validate_monitoring_sdk(info: node_pool.Info, pod_name: str) -> None:
188188
jobset_config,
189189
cluster_info,
190190
create_node_pool,
191-
startup.task_group,
191+
*startup.tasks,
192192
sdk_validation,
193193
cleanup_workload,
194194
cleanup_node_pool,

0 commit comments

Comments
 (0)