Skip to content

Commit ce5f8c9

Browse files
authored
Fix resource allocation for post-training notebook DAGs (GoogleCloudPlatform#1262)
1. Support v5e and v6e TPU versions in the DAG. 2. Read YAML configurations from a GCS bucket to set the TPU version and zone for the run.
1 parent f347656 commit ce5f8c9

5 files changed

Lines changed: 207 additions & 26 deletions

File tree

dags/post_training/maxtext_rl_notebook.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import datetime
99

1010
from airflow import models
11-
from airflow.models.baseoperator import chain
1211

1312
from dags import composer_env
1413
from dags.common import test_owner
@@ -76,23 +75,26 @@
7675
test_run_name = "llama31_rl_notebook"
7776
current_datetime = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
7877

78+
# Load configurations from GCS
79+
config_arg = notebook_util.load_notebook_config_from_gcs_yaml(
80+
gcs_path=notebook_util.NOTEBOOK_CONFIG_GCS_PATH,
81+
dag_name=DAG_TEST_NAME,
82+
)
83+
config = notebook_util.NotebookConfig(config_arg)
84+
7985
# Setup commands for MaxText environment
8086
setup_script = notebook_util.build_maxtext_setup_script()
8187

82-
notebook_rl_tests = []
83-
# Test both GRPO and GSPO algorithms
88+
previous_task = config_arg
8489
for loss_algo in loss_algos:
85-
rl_notebook_test = notebook_util.initialize_notebook_test(
86-
test_name=f"{DAG_TEST_NAME}_rl_{loss_algo.value}",
90+
previous_task = notebook_util.run_notebook_tests(
8791
dag_name=DAG_TEST_NAME,
92+
task_id_prefix=f"rl_{loss_algo.value}",
8893
notebook_path="src/maxtext/examples/rl_llama3_demo.ipynb",
8994
set_up_script=setup_script,
9095
parameters={"LOSS_ALGO": loss_algo.loss_name},
9196
task_owner=test_owner.DEPP_L,
97+
hf_token=HF_TOKEN_LLAMA31,
98+
config=config,
99+
previous_task=previous_task,
92100
)
93-
94-
notebook_rl_tests.append(
95-
notebook_util.run_training(rl_notebook_test, HF_TOKEN_LLAMA31)
96-
)
97-
98-
chain(*notebook_rl_tests)

dags/post_training/maxtext_sft_notebook.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,17 +72,24 @@
7272
f"{gcs_bucket.ORBAX_AUTOMATION_BUCKET_EUROPE_WEST4}/post-training/sft/"
7373
)
7474

75+
# Load configurations from GCS
76+
config_arg = notebook_util.load_notebook_config_from_gcs_yaml(
77+
gcs_path=notebook_util.NOTEBOOK_CONFIG_GCS_PATH,
78+
dag_name=DAG_TEST_NAME,
79+
)
80+
config = notebook_util.NotebookConfig(config_arg)
81+
7582
# Setup commands for MaxText environment
7683
setup_script = notebook_util.build_maxtext_setup_script()
7784

78-
# Test SFT training
79-
sft_notebook_test = notebook_util.initialize_notebook_test(
80-
test_name=f"{DAG_TEST_NAME}_sft",
85+
notebook_util.run_notebook_tests(
8186
dag_name=DAG_TEST_NAME,
87+
task_id_prefix="sft",
8288
notebook_path="src/maxtext/examples/sft_llama3_demo_tpu.ipynb",
8389
set_up_script=setup_script,
8490
parameters={"BASE_OUTPUT_DIRECTORY": BASE_OUTPUT_DIRECTORY},
8591
task_owner=test_owner.DEPP_L,
92+
hf_token=HF_TOKEN_LLAMA31,
93+
config=config,
94+
previous_task=config_arg,
8695
)
87-
88-
notebook_util.run_training(sft_notebook_test, HF_TOKEN_LLAMA31)

dags/post_training/util/notebook_util.py

Lines changed: 148 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,18 @@
22

33
import datetime
44
import inspect
5+
import logging
56
import textwrap
7+
import airflow
8+
import json
9+
import re
10+
11+
from airflow.decorators import task as airflow_task
612
from 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

818
from dags.common.vm_resource import (
919
Project,
@@ -14,7 +24,11 @@
1424
Zone,
1525
)
1626
from 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

2034
def 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

xlml/apis/gcp_config.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
"""Config file for Google Cloud Project (GCP)."""
1616

1717
import dataclasses
18+
from typing import Any
1819

20+
from airflow.exceptions import AirflowException
21+
from airflow.models.xcom_arg import XComArg
22+
from airflow.operators.python import get_current_context
1923
from dags.common.vm_resource import Project
2024
from xlml.apis import metric_config
2125

@@ -33,7 +37,32 @@ class GCPConfig:
3337
"""
3438

3539
project_name: str
36-
zone: str
40+
zone: str | XComArg
3741
dataset_name: metric_config.DatasetOption
3842
dataset_project: str = Project.CLOUD_ML_AUTO_SOLUTIONS.value
3943
composer_project: str = Project.CLOUD_ML_AUTO_SOLUTIONS.value
44+
45+
def __getattribute__(self, name: str) -> Any:
46+
# First obtain the underlying value of the attribute.
47+
value = super().__getattribute__(name)
48+
49+
# Actively resolve an XComArg under execution phase.
50+
match value:
51+
case XComArg():
52+
try:
53+
context = get_current_context()
54+
except AirflowException:
55+
# AirflowException means we are not in execution phase yet,
56+
# return the XComArg as-is
57+
return value
58+
59+
resolved_value = value.resolve(context)
60+
61+
# Store the resolved result back so that
62+
# further references won't have to resolve again.
63+
super().__setattr__(name, resolved_value)
64+
65+
return resolved_value
66+
67+
case _:
68+
return value

xlml/utils/tpu.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323

2424
from absl import logging
2525
import airflow
26+
27+
from airflow.utils.trigger_rule import TriggerRule
2628
from airflow.decorators import task, task_group
2729
from airflow.utils.task_group import TaskGroup
2830
from airflow.operators.python import get_current_context
@@ -253,7 +255,7 @@ def delete_queued_resource(qualified_name: airflow.XComArg):
253255
resource.
254256
"""
255257

256-
@task(trigger_rule='all_done')
258+
@task(trigger_rule=TriggerRule.NONE_SKIPPED)
257259
def delete_tpu_nodes_request(qualified_name: str):
258260
# TODO(wcromar): Find a less repetitive way to manage the TPU client.
259261
creds, _ = google.auth.default()
@@ -299,7 +301,7 @@ def wait_for_tpu_deletion(qualified_name: str):
299301
logging.info(f'TPU Nodes: {qr.tpu.node_spec}')
300302
return False
301303

302-
@task(trigger_rule='all_done')
304+
@task(trigger_rule=TriggerRule.NONE_SKIPPED)
303305
def delete_queued_resource_request(qualified_name: str) -> Optional[str]:
304306
creds, _ = google.auth.default()
305307
client = tpu_api.TpuClient(credentials=creds)

0 commit comments

Comments
 (0)