3131from dags .maxtext_pathways .configs import recipe_config as recipe_cfg
3232from xlml .utils import kpo , gke , xpk
3333
34- ELASTIC_TYPE = ["Pause-resume" , " Replica-resize" ]
34+ ELASTIC_TYPE = ["Pause-resume" , "Replica-resize" ]
3535elastic_params = ui_params .PARAMETERS .copy ()
3636elastic_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
139142def 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+
217294def 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