Skip to content

Commit 12251fe

Browse files
committed
add maxtext_multi_tier_checkpointing_recover DAG
1 parent 61058a2 commit 12251fe

4 files changed

Lines changed: 303 additions & 0 deletions

File tree

dags/common/test_owner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ class Team(enum.Enum):
5454
# Multi-tier Checkpointing
5555
ABHINAV_S = "ABHINAV S."
5656
XUEFENG_G = "XUEFENG G."
57+
JACKY_F = "JACKY F."
5758

5859
# MLCompass
5960
ORTI_B = "Orti B."
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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+
"""
16+
A DAG to run MaxText multi-tier checkpointing tests.
17+
"""
18+
import datetime
19+
from airflow import models
20+
from dags import composer_env, gcs_bucket
21+
from dags.common.vm_resource import DockerImage, XpkClusters
22+
from dags.multipod.configs import gke_config
23+
from dags.multipod.configs.common import SetupMode # Run once a day at 10 am UTC (2 am PST)
24+
from dags.common import test_owner
25+
26+
SCHEDULED_TIME = "0 10 * * *" if composer_env.is_prod_env() else None
27+
28+
with models.DAG(
29+
dag_id="maxtext_multi_tier_checkpointing_recover",
30+
schedule=SCHEDULED_TIME,
31+
tags=[
32+
"multipod_team",
33+
"maxtext",
34+
"multi_tier_checkpointing_recover",
35+
],
36+
start_date=datetime.datetime(2025, 5, 15),
37+
catchup=False,
38+
concurrency=2,
39+
) as dag:
40+
base_output_directory = (
41+
f"{gcs_bucket.MTC_BUCKET}/maxtext_multi_tier_checkpointing_recover"
42+
)
43+
dataset_path = gcs_bucket.MTC_BUCKET
44+
docker_images = [
45+
(SetupMode.STABLE, DockerImage.MAXTEXT_TPU_JAX_NIGHTLY),
46+
]
47+
test_configs = {
48+
# accelerator: list of slices to test
49+
"v5p-8": [2],
50+
}
51+
clusters = {
52+
# accelerator: cluster name
53+
"v5p-8": XpkClusters.TPU_V5P_8_CLUSTER,
54+
}
55+
56+
for mode, image in docker_images:
57+
for accelerator, slices in test_configs.items():
58+
for slice_num in slices:
59+
command = (
60+
"bash end_to_end/test_mtc_phase_2_save_path.sh"
61+
f" multitiercheckpointing-{slice_num}x-{accelerator}"
62+
f" {base_output_directory} {dataset_path}",
63+
)
64+
maxtext_save_checkpoint = gke_config.get_gke_config(
65+
num_slices=slice_num,
66+
cluster=clusters[accelerator],
67+
time_out_in_min=60,
68+
test_name="maxtext-multi-tier-checkpointing-recover",
69+
run_model_cmds=command,
70+
docker_image=image.value,
71+
test_owner=test_owner.JACKY_F,
72+
).run_with_interruption(
73+
ramdisk_directory="local",
74+
xpk_branch="main",
75+
skip_post_process=True,
76+
mtc_enabled=True,
77+
)
78+
79+
clean_cmd = (f"rm -rf /local/*",)
80+
clean_ramdisk_one = gke_config.get_gke_config(
81+
num_slices=slice_num,
82+
cluster=clusters[accelerator],
83+
time_out_in_min=60,
84+
test_name="clean-ramdisk",
85+
run_model_cmds=clean_cmd,
86+
docker_image=image.value,
87+
test_owner=test_owner.JACKY_F,
88+
).run(
89+
ramdisk_directory="local",
90+
xpk_branch="main",
91+
skip_post_process=True,
92+
mtc_enabled=True,
93+
)
94+
95+
(maxtext_save_checkpoint >> clean_ramdisk_one)

xlml/apis/task.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,41 @@ def run(
201201

202202
return group
203203

204+
def run_with_interruption(
205+
self,
206+
*,
207+
gcs_location: Optional[airflow.XComArg] = None,
208+
use_vertex_tensorboard: bool = False,
209+
use_pathways: bool = False,
210+
skip_post_process: bool = False,
211+
ramdisk_directory: str = "",
212+
mtc_enabled: bool = False,
213+
xpk_branch: str = xpk.MAIN_BRANCH,
214+
) -> DAGNode:
215+
"""Run a test job within a docker image.
216+
217+
Attributes:
218+
gcs_location: GCS path for all artifacts of the test.
219+
use_vertex_tensorboard: Set to True to view workload data on
220+
Vertex AI Tensorboard.
221+
222+
Returns:
223+
A task group with the following tasks chained: run_model and
224+
post_process.
225+
"""
226+
with TaskGroup(group_id=self.task_test_config.benchmark_id) as group:
227+
run_model, gcs_path = self.run_model_with_interruption(
228+
gcs_location,
229+
use_vertex_tensorboard,
230+
use_pathways,
231+
ramdisk_directory,
232+
mtc_enabled,
233+
xpk_branch,
234+
)
235+
if not skip_post_process:
236+
run_model >> self.post_process(gcs_path)
237+
return group
238+
204239
def run_with_name_gen_and_quarantine(
205240
self,
206241
quarantine_task_group,
@@ -333,6 +368,72 @@ def run_model(
333368
)
334369
return group, gcs_path
335370

371+
def run_model_with_interruption(
372+
self,
373+
gcs_location: Optional[airflow.XComArg] = None,
374+
use_vertex_tensorboard: bool = False,
375+
use_pathways: bool = False,
376+
ramdisk_directory: str = "",
377+
mtc_enabled: bool = False,
378+
xpk_branch: str = xpk.MAIN_BRANCH,
379+
) -> DAGNode:
380+
"""Run the TPU/GPU test in `task_test_config` using xpk.
381+
Different behaviour for testing interruption.
382+
383+
Attributes:
384+
gcs_location: GCS path for all artifacts of the test.
385+
use_vertex_tensorboard: Set to True to view workload data on
386+
Vertex AI Tensorboard.
387+
388+
Returns:
389+
A DAG node that executes the model test.
390+
"""
391+
with TaskGroup(group_id="run_model") as group:
392+
workload_id = xpk.generate_workload_id(self.task_test_config.benchmark_id)
393+
if gcs_location:
394+
gcs_path = gcs_location
395+
else:
396+
gcs_path = name_format.generate_gcs_folder_location(
397+
self.task_test_config.gcs_subfolder,
398+
self.task_test_config.benchmark_id,
399+
)
400+
401+
launch_workload_with_interruption = (
402+
self.launch_workload_with_interruption(
403+
workload_id,
404+
gcs_path,
405+
use_vertex_tensorboard,
406+
use_pathways,
407+
ramdisk_directory,
408+
mtc_enabled,
409+
xpk_branch,
410+
)
411+
)
412+
413+
wait_for_workload_completion = xpk.wait_for_workload_completion.override(
414+
timeout=int(self.task_test_config.timeout.total_seconds()),
415+
)(
416+
workload_id=workload_id,
417+
project_id=self.task_gcp_config.project_name,
418+
region=self.task_gcp_config.zone[:-2],
419+
cluster_name=self.task_test_config.cluster_name,
420+
)
421+
422+
clean_up_workload = xpk.clean_up_workload(
423+
workload_id=workload_id,
424+
project_id=self.task_gcp_config.project_name,
425+
zone=self.task_gcp_config.zone,
426+
cluster_name=self.task_test_config.cluster_name,
427+
)
428+
429+
(
430+
(workload_id, gcs_path)
431+
>> launch_workload_with_interruption
432+
>> wait_for_workload_completion
433+
>> clean_up_workload
434+
)
435+
return group, gcs_path
436+
336437
def launch_workload(
337438
self,
338439
workload_id: str,
@@ -376,6 +477,61 @@ def launch_workload(
376477
run_workload >> wait_for_workload_start
377478
return group
378479

480+
def launch_workload_with_interruption(
481+
self,
482+
workload_id: str,
483+
gcs_path: str,
484+
use_vertex_tensorboard: bool,
485+
use_pathways: bool = False,
486+
ramdisk_directory: str = "",
487+
mtc_enabled: bool = False,
488+
xpk_branch: str = xpk.MAIN_BRANCH,
489+
) -> DAGNode:
490+
"""Create the workload and wait for it to provision."""
491+
with TaskGroup(group_id="launch_workload_with_interruption") as group:
492+
run_workload = xpk.run_workload.override(
493+
owner=self.task_test_config.task_owner
494+
)(
495+
task_id="run_workload",
496+
cluster_project=self.task_gcp_config.project_name,
497+
zone=self.task_gcp_config.zone,
498+
cluster_name=self.task_test_config.cluster_name,
499+
benchmark_id=self.task_test_config.benchmark_id,
500+
workload_id=workload_id,
501+
gcs_path=gcs_path,
502+
docker_image=self.task_test_config.docker_image,
503+
accelerator_type=self.task_test_config.accelerator.name,
504+
run_cmds=self.task_test_config.test_script,
505+
num_slices=self.task_test_config.num_slices,
506+
use_vertex_tensorboard=use_vertex_tensorboard,
507+
use_pathways=use_pathways,
508+
ramdisk_directory=ramdisk_directory,
509+
mtc_enabled=mtc_enabled,
510+
xpk_branch=xpk_branch,
511+
)
512+
513+
wait_for_workload_start = xpk.wait_for_workload_start.override(
514+
timeout=self.workload_provision_timeout.total_seconds()
515+
)(
516+
workload_id=workload_id,
517+
project_id=self.task_gcp_config.project_name,
518+
region=self.task_gcp_config.zone[:-2],
519+
cluster_name=self.task_test_config.cluster_name,
520+
)
521+
522+
run_interruption_workload = xpk.run_interruption_cmd.override(
523+
owner=self.task_test_config.task_owner
524+
)(
525+
task_id="run_interruption_cmd",
526+
workload_id=workload_id,
527+
project_id=self.task_gcp_config.project_name,
528+
region=self.task_gcp_config.zone[:-2],
529+
cluster_name=self.task_test_config.cluster_name,
530+
)
531+
532+
run_workload >> wait_for_workload_start >> run_interruption_workload
533+
return group
534+
379535
def post_process(self, result_location: Optional[str] = None) -> DAGNode:
380536
"""Process metrics and metadata, and insert them into BigQuery tables.
381537

xlml/utils/xpk.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,12 @@ def run_workload(
120120
workload_create_cmd += f" --ramdisk-directory={ramdisk_directory}"
121121
if mtc_enabled:
122122
workload_create_cmd += " --mtc-enabled"
123+
# For Orbax DAG
124+
if ramdisk_directory and mtc_enabled:
125+
workload_create_cmd = workload_create_cmd.replace(
126+
" --restart-on-user-code-failure", ""
127+
)
128+
workload_create_cmd += " --max-restarts=50"
123129

124130
# If using a valid GPU and the XPK branch is set to "main", then branch is switch to "v0.4.1".
125131
if is_valid_gpu_version(accelerator_type) and xpk_branch == MAIN_BRANCH:
@@ -284,6 +290,51 @@ def wait_for_workload_completion(
284290
return True
285291

286292

293+
@task.sensor(poke_interval=120, timeout=1200, mode="reschedule")
294+
def run_interruption_cmd(
295+
task_id: str,
296+
project_id: str,
297+
region: str,
298+
cluster_name: str,
299+
workload_id: str,
300+
) -> bool:
301+
"""Run command to interrupt pod ."""
302+
core_api = _get_core_api_client(project_id, region, cluster_name)
303+
pods = _list_workload_pods(core_api, workload_id)
304+
305+
if any(pod.status.phase in ["Pending"] for pod in pods.items):
306+
logging.info("Some of the pods is still pending. Waiting to start")
307+
return False
308+
309+
try:
310+
for pod in pods.items:
311+
if pod.status.phase == "Failed":
312+
# Don't keep retrying if the pod has failed
313+
raise AirflowFailException(f"Bad pod phase: {pod.status.phase}")
314+
elif pod.status.phase in ["Unknown"]:
315+
raise RuntimeError(f"Bad pod phase: {pod.status.phase}")
316+
finally:
317+
if all(pod.status.phase in ["Running"] for pod in pods.items):
318+
# Pick last one running pod
319+
pod = pods.items[len(pods.items) - 1]
320+
logs = core_api.read_namespaced_pod_log(
321+
name=pod.metadata.name, namespace=pod.metadata.namespace
322+
)
323+
logging.info(f"Logs for pod {pod.metadata.name}:")
324+
for line in logs.split("\n"):
325+
logging.info(line)
326+
327+
# TODO --> More sophisticated way to know when the pod start training.
328+
if "completed step:" in logs:
329+
# Here where regex expresion to kill pod will go. First test with killing the pod
330+
result = core_api.delete_namespaced_pod(
331+
name=pod.metadata.name, namespace=pod.metadata.namespace
332+
)
333+
logging.info("The {pod.metadata.name} pod was successfully deleted.")
334+
return True
335+
return False
336+
337+
287338
@task(trigger_rule="all_done")
288339
def clean_up_workload(
289340
workload_id: str,

0 commit comments

Comments
 (0)