22
33import datetime
44import inspect
5+ import logging
56import textwrap
7+ import airflow
8+ import json
9+ import re
10+
11+ from airflow .decorators import task as airflow_task
612from airflow .models .taskmixin import DAGNode
13+ from airflow .models .baseoperator import chain
14+ from airflow .operators .empty import EmptyOperator
15+ from airflow .utils .trigger_rule import TriggerRule
16+ from airflow .utils .task_group import TaskGroup
717
818from dags .common .vm_resource import (
919 Project ,
1424 Zone ,
1525)
1626from dags .post_training .util import test_config_util
17- from xlml .apis import gcp_config , metric_config , task , test_config
27+ from xlml .apis import gcp_config , gcs , metric_config , task , test_config
28+
29+ NOTEBOOK_CONFIG_GCS_PATH = (
30+ "gs://ml-auto-solutions-dag-configs/post-training/notebook_dag_configs.yaml"
31+ )
1832
1933
2034def build_maxtext_setup_script () -> str :
@@ -78,9 +92,6 @@ def _run_parameter_injection(
7892 parameters: Dict of {key: value} for literal injection.
7993 env_params: List of keys to be injected as `os.getenv("KEY")`.
8094 """
81- import json
82- import re
83-
8495 with open (notebook_path , encoding = "utf-8" ) as f :
8596 nb = json .load (f )
8697
@@ -252,6 +263,7 @@ def initialize_notebook_test(
252263 set_up_script : str ,
253264 parameters : dict [str , any ],
254265 task_owner : str ,
266+ tpu_version : TpuVersion ,
255267) -> test_config .TpuVmTest :
256268 """Creates a TpuVmTest configuration for notebook execution."""
257269 notebook_execution = build_notebook_execution_command (
@@ -262,7 +274,7 @@ def initialize_notebook_test(
262274 )
263275 return test_config .TpuVmTest (
264276 test_config .Tpu (
265- version = TpuVersion . V5E ,
277+ version = tpu_version ,
266278 cores = 8 ,
267279 runtime_version = RuntimeVersion .V2_ALPHA_TPUV6 .value ,
268280 reserved = False ,
@@ -279,14 +291,143 @@ def initialize_notebook_test(
279291 )
280292
281293
282- def run_training (config : test_config .TpuVmTest , hf_token : str ) -> DAGNode :
294+ class NotebookConfig :
295+ """A simple container holding dynamic XComArg fields."""
296+
297+ def __init__ (self , config_arg : airflow .XComArg ) -> None :
298+ self .zone = config_arg ["zone" ]
299+ self .tpu_version = config_arg ["tpu_version" ]
300+
301+
302+ @airflow_task (multiple_outputs = True )
303+ def load_notebook_config_from_gcs_yaml (
304+ gcs_path : str , dag_name : str
305+ ) -> dict [str , str ]:
306+ """Loads and parses TPU version and zone configs from GCS yaml config."""
307+ config = gcs .load_yaml_from_gcs (gcs_path )
308+ dag_cfg = config .get ("dag" , {}).get (dag_name , {})
309+
310+ tpu_version = dag_cfg .get ("tpu_version" )
311+ zone = dag_cfg .get ("zone" )
312+
313+ # Validate that GCS config values correspond to valid Enum values
314+ def assert_is_valid_enum (value : str , enum_class ) -> None :
315+ try :
316+ enum_class (value )
317+ except ValueError as e :
318+ raise ValueError (f"Config Validation Error: { e } " ) from e
319+
320+ assert_is_valid_enum (zone , Zone )
321+ assert_is_valid_enum (tpu_version , TpuVersion )
322+
323+ logging .info (
324+ f"Loaded configuration: tpu_version='{ tpu_version } ', zone='{ zone } '."
325+ )
326+
327+ return {"tpu_version" : tpu_version , "zone" : zone }
328+
329+
330+ def run_training (
331+ config : test_config .TpuVmTest , hf_token : str , zone : str | airflow .XComArg
332+ ) -> DAGNode :
283333 return task .run_queued_resource_test (
284334 task_test_config = config ,
285335 task_gcp_config = gcp_config .GCPConfig (
286336 project_name = Project .CLOUD_ML_AUTO_SOLUTIONS .value ,
287- zone = Zone . EUROPE_WEST4_B . value ,
337+ zone = zone ,
288338 dataset_name = metric_config .DatasetOption .XLML_DATASET ,
289339 ),
290340 skip_post_process = True ,
291341 custom_env = {"HF_TOKEN" : hf_token },
292342 )
343+
344+
345+ def run_notebook_tests (
346+ dag_name : str ,
347+ task_id_prefix : str ,
348+ notebook_path : str ,
349+ set_up_script : str ,
350+ parameters : dict [str , any ],
351+ task_owner : str ,
352+ hf_token : str ,
353+ config : NotebookConfig ,
354+ previous_task : DAGNode | None = None ,
355+ ) -> TaskGroup :
356+ """Creates and chains branched notebook tests for all TPU versions.
357+
358+ Args:
359+ dag_name: Name of the DAG.
360+ task_id_prefix: Prefix for task and operator IDs (e.g. "rl_grpo" or "sft").
361+ notebook_path: Path to the notebook to run.
362+ set_up_script: Setup script for MaxText environment.
363+ parameters: Dict of parameters to inject in the notebook.
364+ task_owner: Owner of the task.
365+ hf_token: HuggingFace access token.
366+ config: A `NotebookConfig` wrapper containing zone and tpu_version.
367+ previous_task: Optional task/DAGNode to chain *before* the branches.
368+
369+ Returns:
370+ A TaskGroup representing the entire branched notebook test workflow.
371+ """
372+ with TaskGroup (
373+ group_id = f"{ task_id_prefix } _tests" , prefix_group_id = False
374+ ) as group :
375+ # 1. Initialize and create the V5E test and task group
376+ notebook_test_v5e = initialize_notebook_test (
377+ test_name = f"{ dag_name } _{ task_id_prefix } " ,
378+ dag_name = dag_name ,
379+ notebook_path = notebook_path ,
380+ set_up_script = set_up_script ,
381+ parameters = parameters ,
382+ task_owner = task_owner ,
383+ tpu_version = TpuVersion .V5E ,
384+ )
385+ run_task_v5e = run_training (notebook_test_v5e , hf_token , zone = config .zone )
386+
387+ # 2. Initialize and create the TRILLIUM/V6E test and task group
388+ notebook_test_v6e = initialize_notebook_test (
389+ test_name = f"{ dag_name } _{ task_id_prefix } " ,
390+ dag_name = dag_name ,
391+ notebook_path = notebook_path ,
392+ set_up_script = set_up_script ,
393+ parameters = parameters ,
394+ task_owner = task_owner ,
395+ tpu_version = TpuVersion .TRILLIUM ,
396+ )
397+ run_task_v6e = run_training (notebook_test_v6e , hf_token , zone = config .zone )
398+
399+ # 3. Create skipped fallback empty operator task
400+ skipped = EmptyOperator (task_id = f"skipped_{ task_id_prefix } " )
401+
402+ # 4. Define central Task-decorated Branch Operator accepting dynamic parameters
403+ @airflow_task .branch (
404+ task_id = f"task_path_decider_{ task_id_prefix } " ,
405+ trigger_rule = TriggerRule .ALL_DONE ,
406+ retries = 0 ,
407+ )
408+ def task_path_decider (tpu_version : str ) -> str :
409+ logging .info (f"Configured active TPU version: '{ tpu_version } '" )
410+ active_tpu_version = TpuVersion (tpu_version )
411+
412+ match active_tpu_version :
413+ case TpuVersion .V5E :
414+ decided_task_id = run_task_v5e .group_id
415+ case TpuVersion .TRILLIUM :
416+ decided_task_id = run_task_v6e .group_id
417+ case _:
418+ decided_task_id = skipped .task_id
419+
420+ logging .info (f"running task_id: { decided_task_id } " )
421+ return decided_task_id
422+
423+ # 5. Instantiate branch decider task, passing XComArg directly for Airflow auto-resolution
424+ task_decider = task_path_decider (tpu_version = config .tpu_version )
425+
426+ # 6. Chain previous tasks to the decider if exist
427+ if previous_task :
428+ chain (previous_task , task_decider )
429+
430+ # 7. Connect branch decider to target branches
431+ chain (task_decider , [run_task_v5e , run_task_v6e , skipped ])
432+
433+ return group
0 commit comments