Skip to content

Commit d4c0695

Browse files
authored
Add an new DAG for replica resize (GoogleCloudPlatform#1290)
While similar to the existing `pause-resume` workflow, standard replica resizing previously ran into checkpointing issues (b/511164291). To ensure checkpoints function correctly during an elastic resize, this DAG implements two critical configurations: - Changes the `dataset_type` to synthetic (default). - Disables `colocated_python`. - Turns on the `enable checkpointing`: True.
1 parent 1ace5db commit d4c0695

2 files changed

Lines changed: 244 additions & 10 deletions

File tree

dags/common/scheduling_helper/scheduling_helper.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ class DayOfWeek(enum.Enum):
9494
PW_MCJAX_CLUSTER.name: {
9595
"pw_mcjax_benchmark_recipe": DefaultTimeout,
9696
"pw_mcjax_benchmark_recipe_elastic": DefaultTimeout,
97+
"pw_mcjax_benchmark_recipe_elastic_Replica-resize": DefaultTimeout,
9798
},
9899
}
99100

dags/maxtext_pathways/pw_mcjax_elastic.py

Lines changed: 243 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
from dags.maxtext_pathways.configs import recipe_config as recipe_cfg
3232
from xlml.utils import kpo, gke, xpk
3333

34-
ELASTIC_TYPE = ["Pause-resume", " Replica-resize"]
34+
ELASTIC_TYPE = ["Pause-resume", "Replica-resize"]
3535
elastic_params = ui_params.PARAMETERS.copy()
3636
elastic_params.update({
3737
"cluster_name": ui_params.Param(
@@ -122,6 +122,7 @@ def generate_derived_parameters(dag_params: dict) -> dict:
122122
if dag_params["selected_model_names"] == "customized_model_name":
123123
derived_params["selected_model_names"] = dag_params["customized_model_name"]
124124

125+
# TODO(cienet): Refine the parameter parsing logic
125126
core_calc = dag_params["core_count"] // 4
126127
if dag_params["elastic_type"] == "Pause-resume":
127128
derived_params["elastic_min_slice_count"] = -1
@@ -135,6 +136,8 @@ def generate_derived_parameters(dag_params: dict) -> dict:
135136
return derived_params
136137

137138

139+
# TODO(cienet): Remove the temporary local code once these changes have been
140+
# merged into the maxtext repository.
138141
@task
139142
def generate_commands(
140143
dag_params: dict, derived_params: dict, recipe_instance: recipe_cfg.Recipe
@@ -197,7 +200,8 @@ def generate_commands(
197200
# Add proxy_flags to enable elastic training and colocated Python data input.
198201
recipe_cmd += (
199202
f" --proxy_flags='--virtual_slices={derived_params['topology']} "
200-
f"--num_elastic_slices={derived_params['num_elastic_slices']} --sidecar_name=external'"
203+
f"--num_elastic_slices={derived_params['num_elastic_slices']} "
204+
" --sidecar_name=external'"
201205
)
202206
# Add the skip-validation flag in the recipe to bypass xpk checks.
203207
recipe_cmd += " --skip-validation"
@@ -214,6 +218,79 @@ def generate_commands(
214218
return commands
215219

216220

221+
# TODO(cienet): Remove the temporary local code once these changes have been
222+
# merged into the maxtext repository.
223+
@task
224+
def generate_commands_replica(
225+
dag_params: dict, derived_params: dict, recipe_instance: recipe_cfg.Recipe
226+
) -> str:
227+
"""Generates a command string using config and derived parameters.
228+
229+
Runtime modifications are made to the recipe command to enable elastic
230+
training and colocated Python data input.
231+
"""
232+
# Initialization command.
233+
env_cmds = generate_install_dependencies_commands()
234+
recipe_cmd = recipe_instance.run_command
235+
236+
# Patch command for enabling elastic training & colocated Python data input.
237+
patch_cmd_runner = (
238+
r'sed -i "/python3 -m maxtext.trainers.pre_train.train/a '
239+
# enable elastice training
240+
r" f\"elastic_enabled=True\",\n"
241+
r" f\"enable_single_controller=True\",\n"
242+
f' \\"elastic_min_slice_count='
243+
f'{derived_params["elastic_min_slice_count"]}\\",\\n'
244+
# enable goodput setting
245+
r" f\"enable_pathways_goodput=True\",\n"
246+
r" f\"enable_goodput_recording=True\",\n"
247+
r" f\"goodput_upload_interval_seconds=30\",\n"
248+
r" f\"monitor_goodput=True\",\n"
249+
# enanble checkpointing for elastic training
250+
r" f\"async_checkpointing=True\",\n"
251+
r" f\"enable_checkpoint_cloud_logger=True\",\n"
252+
r" f\"checkpoint_period=10\",\n"
253+
r'" benchmarks/maxtext_xpk_runner.py'
254+
)
255+
256+
# changing to synthetic and disabling colocated_python:
257+
# https://b.corp.google.com/issues/511164291#comment26
258+
patch_cmd_model_configs_sub = (
259+
r'sed -i "/model_name=\"default-basic-1\"/,/xla_flags/ { '
260+
r"s/\"enable_checkpointing\": False/\"enable_checkpointing\": True/; "
261+
r"/\"profiler\":/d; "
262+
r'}" benchmarks/maxtext_trillium_model_configs.py'
263+
)
264+
265+
# Combine parameters to further generate the final command.
266+
all_params = {**dag_params, **derived_params}
267+
for key, value in all_params.items():
268+
if key in recipe_cfg.RECIPE_FLAG:
269+
if isinstance(value, int):
270+
recipe_cmd += f" --{key}={value}"
271+
else:
272+
recipe_cmd += f" --{key}='{value}'"
273+
# Override the default benchmark_steps too bigger.
274+
recipe_cmd += " --benchmark_steps=3000"
275+
# Add proxy_flags to enable elastic training and colocated Python data input.
276+
recipe_cmd += (
277+
f" --proxy_flags='--virtual_slices={derived_params['topology']} "
278+
f"--num_elastic_slices={derived_params['num_elastic_slices']}'"
279+
)
280+
# Add the skip-validation flag in the recipe to bypass xpk checks.
281+
recipe_cmd += " --skip-validation"
282+
formatted_cmds = recipe_cmd.replace(" --", " \n --")
283+
logging.info(f"\n {formatted_cmds}")
284+
commands = " && ".join([
285+
env_cmds,
286+
patch_cmd_runner,
287+
patch_cmd_model_configs_sub,
288+
recipe_cmd,
289+
])
290+
291+
return commands
292+
293+
217294
def generate_install_dependencies_commands() -> str:
218295
"""Generate shell commands to install necessary dependencies in the Pod."""
219296
# fmt: off
@@ -256,6 +333,9 @@ def worker_pod_interruption(
256333
region: str = "",
257334
cluster_name: str = "",
258335
workload_id: str = "",
336+
entry_log_pattern: str = "completed step:",
337+
elastic_log_pattern: str = "Elastic attempt",
338+
end_log_pattern: str = "Sufficient slices active:",
259339
) -> DAGNode:
260340
"""Run a test job with worker pod interruption."""
261341
with TaskGroup(group_id="worker_pod_interruption") as group:
@@ -268,7 +348,7 @@ def worker_pod_interruption(
268348
region=region,
269349
cluster_name=cluster_name,
270350
workload_id=workload_id,
271-
expect_log_contains="completed step:",
351+
expect_log_contains=entry_log_pattern,
272352
)
273353

274354
trigger_interrupt = xpk.interrupt_worker_pod.override(
@@ -282,7 +362,8 @@ def worker_pod_interruption(
282362

283363
# TODO(cienet): refine validation
284364
# 1. more precise log content and order
285-
# 2. use kubectl instead of CoreV1Api (since it doesn't support "since_time")
365+
# 2. use kubectl instead of CoreV1Api
366+
# (since it doesn't support "since_time")
286367
# 3. cache a timestamp, to skip the old logs
287368

288369
wait_for_elastic_attempt = xpk.check_logs_exist.override(
@@ -292,7 +373,7 @@ def worker_pod_interruption(
292373
region=region,
293374
cluster_name=cluster_name,
294375
workload_id=workload_id,
295-
expect_log_contains=f"Elastic attempt {i+1}",
376+
expect_log_contains=elastic_log_pattern,
296377
)
297378

298379
wait_for_slices_active = xpk.check_logs_exist.override(
@@ -302,10 +383,11 @@ def worker_pod_interruption(
302383
region=region,
303384
cluster_name=cluster_name,
304385
workload_id=workload_id,
305-
expect_log_contains="Sufficient slices active:",
386+
expect_log_contains=end_log_pattern,
306387
expected_count=i + 1,
307388
)
308389

390+
# TODO(cienet): Refine the mechanism to chain tasks
309391
_ = (
310392
wait_for_step
311393
>> trigger_interrupt
@@ -349,10 +431,13 @@ def worker_pod_interruption(
349431
# A DAG to run a MaxText {RECIPE_NAME} with elastic training on GKE.
350432
351433
### Description
352-
Specify different models and number of slices to test the MaxText
353-
{RECIPE_NAME} on different clusters. The DAG first generates recipe
354-
command through UI parameters, then runs the workload, waits and monitors
355-
the workload logs, and finally cleans up the workload.
434+
Pause-resume refers to the process of halting the training execution,
435+
saving its state (typically to a checkpoint), and later restarting
436+
the training, loading the state from the checkpoint to continue.
437+
Stop the training process when slices become unavailable, and starts it
438+
again later on the new set inherently. This mechanism is crucial for
439+
fault tolerance and elasticity. Resuming can occur on the same
440+
set of resources or a different set.
356441
357442
### Prerequisites
358443
- This test requires an existing cluster.
@@ -390,11 +475,159 @@ def worker_pod_interruption(
390475
image_full_url=fetched_params["runner"],
391476
)
392477

478+
# TODO(cienet): Add comments or documentation to explain the expected log
479+
# patterns.
480+
interruption_task = worker_pod_interruption(
481+
project_id=fetched_params["project"],
482+
region=calculated_params["region"],
483+
cluster_name=fetched_params["cluster_name"],
484+
workload_id=calculated_params["workload_id"],
485+
entry_log_pattern="completed step:",
486+
elastic_log_pattern="Elastic attempt",
487+
end_log_pattern="Sufficient slices active: 1 >= 1",
488+
)
489+
490+
wait_for_workload_complete = xpk.wait_for_workload_completion.override(
491+
task_id="wait_for_workload_complete",
492+
timeout=3600,
493+
)(
494+
workload_id=calculated_params["workload_id"],
495+
project_id=fetched_params["project"],
496+
region=calculated_params["region"],
497+
cluster_name=fetched_params["cluster_name"],
498+
)
499+
500+
clean_up_recipe = xpk.clean_up_workload.override(
501+
task_id="clean_up_recipe", trigger_rule=TriggerRule.ALL_DONE
502+
)(
503+
workload_id=calculated_params["workload_id"],
504+
project_id=fetched_params["project"],
505+
zone=fetched_params["zone"],
506+
cluster_name=fetched_params["cluster_name"],
507+
)
508+
509+
(
510+
fetched_params
511+
>> calculated_params
512+
>> generated_cmds
513+
>> start_recipe
514+
>> interruption_task
515+
>> wait_for_workload_complete
516+
>> clean_up_recipe
517+
)
518+
519+
replica_params = elastic_params.copy()
520+
replica_params.update({
521+
"elastic_type": ui_params.Param(
522+
ELASTIC_TYPE[1],
523+
type="string",
524+
title="Elastic Type",
525+
description="Pause-resume/Replica-resize",
526+
enum=ELASTIC_TYPE,
527+
),
528+
"num_slices_list": ui_params.Param(
529+
2,
530+
type="integer",
531+
title="Number Slices",
532+
description="Number of slices",
533+
),
534+
"runner": ui_params.Param(
535+
# TODO(cienet): Replace with an official or production-ready image
536+
# TODO(cienet): Use an image tag instead of the full SHA hash
537+
"gcr.io/cloud-tpu-multipod-dev/lidanny_rr_dag@sha256:08dfca25461e3f2fb736e7313a742e1ceea6123858c0aec5dfe43d0c51d14e38",
538+
type="string",
539+
title="Runner Image",
540+
description="Runner image for the cluster",
541+
),
542+
})
543+
544+
DAG_ID_RESIZE = f"{RECIPE_NAME}_elastic_{ELASTIC_TYPE[1]}"
545+
SCHEDULE_RESIZE = SchedulingHelper.arrange_schedule_time(DAG_ID_RESIZE)
546+
547+
with models.DAG(
548+
dag_id=DAG_ID_RESIZE,
549+
start_date=datetime.datetime(2025, 1, 1),
550+
schedule_interval=SCHEDULE_RESIZE if composer_env.is_prod_env() else None,
551+
catchup=False,
552+
default_args={
553+
"retries": 0,
554+
},
555+
tags=[
556+
"maxtext",
557+
"pathways",
558+
"mcjax",
559+
"benchmark",
560+
"nightly",
561+
"TPU",
562+
"v6e",
563+
],
564+
description=(
565+
f"A DAG to run a MaxText {RECIPE_NAME}"
566+
"with elastic replica resize on GKE."
567+
),
568+
params=replica_params,
569+
doc_md=f"""
570+
# A DAG to run a MaxText {RECIPE_NAME} with elastic replica resize on GKE.
571+
572+
### Description
573+
Replica-resize refers to the ability of the training job to dynamically
574+
adjust the number of active TPU slices (replicas) it uses during execution.
575+
Expected Behavior:
576+
- A change in slice availability (failure or addition)
577+
triggers an event. Often, a slice failure results in an error.
578+
- The elastic training framework detects this change.
579+
- Training on the previous configuration halts, and try to identify
580+
the new set of healthy, available slice.
581+
- The training job is automatically relaunched, loading the model
582+
state from the most recent checkpoint. The relaunched job now runs on
583+
the new set of available slices.
584+
585+
### Prerequisites
586+
- This test requires an existing cluster.
587+
- If you're using a service account to pull an image from a different
588+
project, you need to grant the service account the
589+
`Artifact Registry Reader` role in that project.
590+
591+
### Procedures
592+
An Airflow Composer environment must be created, and the required DAG code
593+
must be deployed to the associated GCS bucket. To initiate the recipe, the
594+
user must access the Airflow UI, locate the specific DAG, and trigger it.
595+
596+
### Model Configuration
597+
If you want to add other TPU type models, you need to manually modify
598+
`/ml-auto-solutions/dags/maxtext_pathways/configs/model_configs.py`.
599+
""",
600+
) as dag:
601+
recipe_runtime = (
602+
RECIPE_NAME.replace("_", "-") + '-{{ execution_date.strftime("%H%M%S") }}'
603+
)
604+
605+
# Define task dependencies by instantiating and linking tasks.
606+
fetched_params = get_dag_parameters()
607+
calculated_params = generate_derived_parameters(fetched_params)
608+
generated_cmds = generate_commands_replica(
609+
fetched_params, calculated_params, RECIPE_INSTANCE
610+
)
611+
612+
start_recipe = kpo.run_command_in_kpo(
613+
start_cli_command=generated_cmds,
614+
workload_id="start_recipe",
615+
task_owner=test_owner.DORA_H,
616+
provisioning_timeout=datetime.timedelta(minutes=5),
617+
workload_run_timeout=datetime.timedelta(minutes=15),
618+
image_full_url=fetched_params["runner"],
619+
)
620+
621+
# TODO(cienet): Add comments or documentation to explain the expected log
622+
# patterns.
393623
interruption_task = worker_pod_interruption(
394624
project_id=fetched_params["project"],
395625
region=calculated_params["region"],
396626
cluster_name=fetched_params["cluster_name"],
397627
workload_id=calculated_params["workload_id"],
628+
entry_log_pattern="live slice count: 2",
629+
elastic_log_pattern="Elastic attempt",
630+
end_log_pattern="Sufficient slices active: 2 >= 1",
398631
)
399632

400633
wait_for_workload_complete = xpk.wait_for_workload_completion.override(

0 commit comments

Comments
 (0)