Skip to content

Commit 21614ce

Browse files
authored
feat: Add Vertex AI integration for log uploads (GoogleCloudPlatform#1169)
This change adds support for uploading training metrics to Vertex AI TensorBoard for both RL and SFT post-training DAGs, improves run name generation for better traceability, and enhances multi-slice training support. The changes include new utility functions, configuration structures, and updates to training command generation and orchestration.
1 parent b1a9112 commit 21614ce

4 files changed

Lines changed: 163 additions & 32 deletions

File tree

dags/post_training/maxtext_rl.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from dags.common.vm_resource import XpkClusters
1919
from dags.multipod.configs import gke_config
2020
from dags.post_training.util import validation_util, test_config_util
21+
from dags.post_training.util.test_config_util import VertexAI
2122
from xlml.utils.xpk import MAIN_BRANCH
2223
from xlml.utils.gke import zone_to_region
2324

@@ -52,21 +53,23 @@
5253
2. **Model Loading:** Init Llama3.1 70B with HF auth
5354
3. **Training Run:** Run train_rl with JAX proxy for GRPO/GSPO
5455
4. **Log Validation:** Check for 'Post RL Training' signal
55-
5. **Success/Failure:** Report status from logs and completion
56+
5. **Vertex AI Upload:** Execute script to upload metrics to TensorBoard
57+
6. **Success/Failure:** Report status from logs and completion
5658
5759
### Success Criteria
5860
The test passes when:
5961
1. Training jobs complete successfully without errors
6062
2. "Post RL Training" log message appears in jax-tpu container logs
6163
3. No infrastructure failures or container launch issues occur
6264
4. All training steps execute within expected parameters
65+
5. Metrics are successfully synced to Vertex AI TensorBoard
6366
""",
6467
concurrency=2,
6568
) as dag:
6669
training_config = test_config_util.RLTestConfig(
6770
cluster=XpkClusters.TPU_V5P_128_CLUSTER,
6871
accelerator="v5p-128",
69-
slices=[1], # Single slice for RL training
72+
slices=[1, 2], # Multi-slice support
7073
model_name="llama3.1-70b",
7174
base_dir=(
7275
f"{test_config_util.DEFAULT_BUCKET}/llama3.1-70b-Instruct/outputs"
@@ -92,13 +95,17 @@
9295

9396
for loss_algo in training_config.loss_algos:
9497
for slice_num in training_config.slices:
95-
current_datetime = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
96-
run_name = f"{loss_algo.value}-{mode.value}-{current_datetime}"
98+
run_name = validation_util.generate_run_name(
99+
prefix=loss_algo.value,
100+
mode=mode.value,
101+
num_slices=slice_num,
102+
)
97103

98104
rl_training_command = training_config.generate_rl_training_command(
99105
loss_algo=loss_algo,
100106
run_name=run_name,
101107
hf_token=HF_TOKEN_LLAMA3_1,
108+
num_slices=slice_num,
102109
)
103110

104111
with TaskGroup(
@@ -166,7 +173,13 @@
166173
)
167174
)
168175

169-
chain(
170-
training_group,
171-
validation_group,
176+
upload_to_vertex_ai = validation_util.upload_to_vertex_ai(
177+
project_id=VertexAI.POST_TRAINING.project_id,
178+
region=VertexAI.POST_TRAINING.region,
179+
tensorboard_id=VertexAI.POST_TRAINING.tensorboard_id,
180+
logdir=f"{training_config.base_dir}/{run_name}/tensorboard/",
181+
experiment_name=loss_algo.value,
182+
run_name_prefix=run_name,
172183
)
184+
185+
chain(training_group, [validation_group, upload_to_vertex_ai])

dags/post_training/maxtext_sft.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from dags.common.vm_resource import DockerImage, XpkClusters
2121
from dags.multipod.configs import gke_config
2222
from dags.post_training.util import validation_util, test_config_util
23+
from dags.post_training.util.test_config_util import VertexAI
2324
from xlml.utils.xpk import MAIN_BRANCH
2425
from xlml.utils.gke import zone_to_region
2526

@@ -128,7 +129,7 @@ def validate_training(
128129
training_config = test_config_util.SFTTestConfig(
129130
cluster=XpkClusters.TPU_V5P_128_CLUSTER,
130131
accelerator="v5p-128",
131-
slices=[1], # Single slice for SFT training
132+
slices=[2],
132133
model_name="llama3.1-70b",
133134
steps=training_steps,
134135
short_id="msft",
@@ -151,8 +152,11 @@ def validate_training(
151152
continue # Skip stable for SFT training tests
152153

153154
for slice_num in training_config.slices:
154-
current_datetime = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
155-
run_name = f"sft-{mode.value}-{current_datetime}"
155+
run_name = validation_util.generate_run_name(
156+
prefix="sft",
157+
mode=mode.value,
158+
num_slices=slice_num,
159+
)
156160

157161
sft_training_command = training_config.generate_sft_training_command(
158162
run_name=run_name,
@@ -168,4 +172,13 @@ def validate_training(
168172
training_config, training_steps, start_time, end_time
169173
)
170174

171-
chain(training_group, validation_group)
175+
upload_to_vertex_ai = validation_util.upload_to_vertex_ai(
176+
project_id=VertexAI.POST_TRAINING.project_id,
177+
region=VertexAI.POST_TRAINING.region,
178+
tensorboard_id=VertexAI.POST_TRAINING.tensorboard_id,
179+
logdir=f"{training_config.base_dir}/{run_name}/tensorboard/",
180+
experiment_name="sft",
181+
run_name_prefix=run_name,
182+
)
183+
184+
chain(training_group, [validation_group, upload_to_vertex_ai])

dags/post_training/util/test_config_util.py

Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,34 @@
44
from enum import Enum
55

66
from dags import gcs_bucket
7-
from dags.common.vm_resource import XpkClusters, DockerImage
7+
from dags.common.vm_resource import (
8+
XpkClusters,
9+
DockerImage,
10+
Project,
11+
Region,
12+
)
813
from dags.multipod.configs.common import SetupMode
914

1015

16+
@dataclass(frozen=True)
17+
class VertexAIConfig:
18+
"""Configuration for Vertex AI TensorBoard instance."""
19+
20+
project_id: str
21+
region: str
22+
tensorboard_id: str
23+
24+
25+
class VertexAI:
26+
"""Vertex AI TensorBoard instances for post-training dashboards."""
27+
28+
POST_TRAINING = VertexAIConfig(
29+
project_id=Project.CLOUD_TPU_MULTIPOD_DEV.value,
30+
region=Region.US_CENTRAL1.value,
31+
tensorboard_id="9105049192543289344",
32+
)
33+
34+
1135
DEFAULT_BUCKET = gcs_bucket.RL_AUTOMATION_BUCKET
1236

1337
# Docker images for post-training
@@ -107,40 +131,56 @@ def __init__(
107131
self.rl_config_path = rl_config_path
108132

109133
def generate_rl_training_command(
110-
self, loss_algo: LossAlgo, run_name: str, hf_token: str
134+
self,
135+
loss_algo: LossAlgo,
136+
run_name: str,
137+
hf_token: str,
138+
num_slices: int = 1,
111139
) -> tuple[str]:
112-
"""Generates the RL training command as a tuple for GKE compatibility.
140+
"""Generates the RL training command as a tuple for GKE compatibility."""
141+
command_params = [
142+
f"run_name={run_name}",
143+
f"model_name={self.model_name}",
144+
f"tokenizer_path={self.tokenizer_path}",
145+
f"load_parameters_path={self.load_parameters_path}",
146+
f"base_output_directory={self.base_dir}",
147+
f"rl.loss_algo={loss_algo.loss_name}",
148+
]
113149

114-
Args:
115-
loss_algo: The loss algorithm to use (e.g., LossAlgo.GRPO).
116-
run_name: The run name for the training job.
117-
hf_token: The HuggingFace token for authentication.
150+
num_trainer = 1
151+
num_samplers = max(1, num_slices - num_trainer)
152+
153+
if num_slices > 1:
154+
command_params.extend([
155+
f"num_trainer_slices={num_trainer}",
156+
f"num_samplers_slices={num_samplers}",
157+
f"rollout_data_parallelism={num_samplers * 2}",
158+
])
118159

119-
Returns:
120-
A tuple containing the RL training command string.
121-
"""
122160
rl_command = (
123161
"python -m src.MaxText.rl.train_rl "
124-
f"{self.rl_config_path} run_name={run_name} "
125-
f"model_name={self.model_name} "
126-
f"tokenizer_path={self.tokenizer_path} "
127-
f"load_parameters_path={self.load_parameters_path} "
128-
f"base_output_directory={self.base_dir} "
129-
f"rl.loss_algo={loss_algo.loss_name}"
162+
f"{self.rl_config_path} " + " ".join(command_params)
130163
)
131-
command = " && ".join([
164+
165+
environment_variables = [
132166
f"export HF_TOKEN={hf_token}",
133167
"export TPU_MIN_LOG_LEVEL=0",
134168
"export TF_CPP_MIN_LOG_LEVEL=0",
135169
"export TPU_STDERR_LOG_LEVEL=0",
136170
"export JAX_PLATFORMS=proxy,cpu",
137171
"export JAX_BACKEND_TARGET=grpc://127.0.0.1:29000",
138172
"export ENABLE_PATHWAYS_PERSISTENCE='1'",
139-
rl_command,
140-
])
173+
]
141174

142-
# Return as tuple for k8s yaml compatibility.
143-
return (command,)
175+
if num_slices > 1:
176+
environment_variables.extend([
177+
f"export NUM_SLICES={num_samplers}",
178+
"export JAX_RANDOM_WEIGHTS=true",
179+
"export VLLM_ENABLE_V1_MULTIPROCESSING=0",
180+
])
181+
182+
full_command = " && ".join(environment_variables + [rl_command])
183+
return (full_command,)
144184

145185

146186
@dataclass

dags/post_training/util/validation_util.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,78 @@
44
workflows, reusing generic utilities from the orbax module where applicable.
55
"""
66

7+
from absl import logging
8+
from datetime import datetime, timezone
9+
10+
from airflow import AirflowException
11+
from airflow.decorators import task
12+
from google.cloud import aiplatform
13+
from google.cloud import storage
14+
715
# Re-export commonly used validation functions from orbax
816
from dags.orbax.util.validation_util import (
917
generate_timestamp,
1018
validate_log_exist,
1119
)
1220

21+
22+
def _ensure_tfevents_exist(logdir: str) -> None:
23+
"""Ensures the provided GCS logdir contains TensorBoard event files."""
24+
if not logdir.startswith("gs://"):
25+
raise AirflowException("Vertex AI upload only supports GCS paths.")
26+
27+
gcs_path = logdir[len("gs://") :]
28+
bucket_name, _, prefix = gcs_path.partition("/")
29+
prefix = prefix.lstrip("/")
30+
storage_client = storage.Client()
31+
blobs = storage_client.list_blobs(bucket_name, prefix=prefix)
32+
has_event_files = any(".tfevents." in blob.name for blob in blobs)
33+
if not has_event_files:
34+
raise AirflowException(
35+
"No TensorBoard event files found at logdir: "
36+
f"{logdir}. Ensure the path contains .tfevents.* files."
37+
)
38+
39+
40+
@task
41+
def generate_run_name(
42+
prefix: str,
43+
mode: str,
44+
num_slices: int,
45+
) -> str:
46+
"""Generates a unique run name with a timestamp."""
47+
current_datetime = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H-%M-%S")
48+
return f"{prefix}-{mode}-{num_slices}x-{current_datetime}"
49+
50+
51+
@task
52+
def upload_to_vertex_ai(
53+
project_id: str,
54+
region: str,
55+
tensorboard_id: str,
56+
logdir: str,
57+
experiment_name: str,
58+
run_name_prefix: str,
59+
) -> None:
60+
"""Uploads TensorBoard logs to Vertex AI.
61+
62+
This task uses the Vertex AI Python SDK to sync logs from a GCS directory
63+
to a specific TensorBoard experiment.
64+
"""
65+
_ensure_tfevents_exist(logdir)
66+
67+
aiplatform.init(project=project_id, location=region)
68+
aiplatform.upload_tb_log(
69+
tensorboard_id=tensorboard_id,
70+
tensorboard_experiment_name=experiment_name,
71+
logdir=logdir,
72+
run_name_prefix=run_name_prefix,
73+
)
74+
logging.info("Upload completed successfully.")
75+
76+
1377
__all__ = [
1478
"generate_timestamp",
1579
"validate_log_exist",
80+
"upload_to_vertex_ai",
1681
]

0 commit comments

Comments
 (0)