Skip to content

Commit bc105d6

Browse files
alfredyu-cienetcamiloCienet
authored andcommitted
Fix: Change name logging_mtc.py to validate_util.py. Since validate_util as the filename make more sense, we are validating the logs.
Fix: checkpoint_steps --> to Optional. Since this value exist but is not use in this test. Fix: Change name mx-sv --> max-sv-loc. Helps with clarity in bucket checkpoints. Fixes for code review
1 parent 91e6062 commit bc105d6

5 files changed

Lines changed: 276 additions & 308 deletions

File tree

dags/orbax/maxtext_mtc_emergency_save_local.py

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import datetime
1111
from dataclasses import dataclass
1212
import posixpath
13+
from typing import Optional
1314

1415
from airflow import models
1516
from airflow.utils.task_group import TaskGroup
@@ -19,16 +20,16 @@
1920
from dags.common.vm_resource import DockerImage, XpkClusters
2021
from dags.multipod.configs import gke_config
2122
from dags.multipod.configs.common import SetupMode
22-
from dags.orbax.util import logging_mtc as log
23-
from dags.orbax.util import multi_tier_checkpoint_util as mtc
23+
from dags.orbax.util import validation_util
24+
from dags.orbax.util import checkpoint_util
2425
from xlml.utils.xpk import BRANCH_ABHINAV_MTC
2526
from xlml.utils.gke import zone_to_region
2627

2728
SCHEDULE = "0 10 * * *" if composer_env.is_prod_env() else None
2829
DAG_TEST_NAME = "maxtext_emc_and_mtc_orbax_save_local"
29-
BASE_OUTPUT_DIRECTORY = gcs_bucket.MTC_AUTOMATION_BUCKET
30+
BASE_OUTPUT_DIR = gcs_bucket.MTC_AUTOMATION_BUCKET
3031

31-
# For this DAG, only one version of the Docker image is supported at the moment.
32+
# Only one version of the Docker image is supported at the moment.
3233
# Other versions (e.g., "stable") may be introduced later.
3334
DOCKER_IMAGES = [(
3435
SetupMode.NIGHTLY,
@@ -66,7 +67,7 @@ class TestConfig:
6667
local_checkpoint_step: int
6768
checkpoint_step: int
6869
ram_disk_size: str
69-
cpc_config: mtc.CheckpointConfiguration
70+
cpc_config: checkpoint_util.CheckpointConfiguration
7071

7172
def __init__(
7273
self,
@@ -79,8 +80,8 @@ def __init__(
7980
replicator_backup_time: int,
8081
step: int,
8182
local_checkpoint_step: int,
82-
checkpoint_step: int,
8383
ram_disk_size_in_mi: str,
84+
checkpoint_step: Optional[int] = None,
8485
):
8586
"""
8687
Initializes the test configurations.
@@ -114,7 +115,7 @@ def __init__(
114115
self.local_checkpoint_step = local_checkpoint_step
115116
self.checkpoint_step = checkpoint_step
116117
self.ram_disk_size = ram_disk_size_in_mi
117-
self.cpc_config = mtc.CheckpointConfiguration(
118+
self.cpc_config = checkpoint_util.CheckpointConfiguration(
118119
project_id=self.cluster.project,
119120
region=zone_to_region(self.cluster.zone),
120121
cluster_name=self.cluster.name,
@@ -127,7 +128,7 @@ def generate_workload_command(
127128
self,
128129
cp: Checkpointing,
129130
checkpoint_dir: str,
130-
out_dir: str,
131+
out_folder: str,
131132
slice_number: int,
132133
) -> str:
133134
run_time = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M")
@@ -138,7 +139,7 @@ def generate_workload_command(
138139
"python3 -m MaxText.train MaxText/configs/base.yml "
139140
"remat_policy=full "
140141
"global_parameter_scale=1 "
141-
f"base_output_directory={out_dir} "
142+
f"base_output_directory={posixpath.join(BASE_OUTPUT_DIR, out_folder)} "
142143
"dataset_type=synthetic "
143144
f"steps={self.step} "
144145
"per_device_batch_size=1 "
@@ -169,21 +170,49 @@ def generate_workload_command(
169170
"orbax",
170171
],
171172
description="A DAG to run MaxText multi-tier checkpointing tests.",
172-
doc_md="",
173+
doc_md="""
174+
# Emergency Checkpoint Manager and Multi-tier Checkpoint Validation DAG
175+
176+
### Description
177+
This DAG (Directed Acyclic Graph) automates the process of validating
178+
checkpoint saving when using both **Emergency Checkpoint Manager**
179+
and **Multi-tier Checkpoint** features. The flag that controls the
180+
behaviour of using MTC or EMC is **user_replicatior**.
181+
Also the steps flag controls how many steps the job will run.
182+
183+
### Prerequisites
184+
To run this test, you need an existing cluster with the Multi-tier
185+
Checkpointing configuration enabled, as well as a bucket with HNS
186+
(Hierarchical Namespace) enabled.
187+
188+
### Procedures
189+
1. **Apply Configuration:** A Checkpoint Configuration YAML file is
190+
applied to the cluster, enabling Multi-tier Checkpoint (MTC) features.
191+
2. **Run Maxtext Jobsets:** The DAG runs a Maxtext jobset twice.
192+
One run has `replicator_enabled` set to `True` (for MTC), and the
193+
other has it set to `False` (for Emergency Checkpoint Manager).
194+
3. **Validate Checkpoints:** The DAG validates that **local checkpoints**
195+
are being saved correctly in the `local/` directory for both job runs.
196+
197+
4. The validation logic is the same for both the MTC and Emergency
198+
Checkpoint Manager job runs because their local checkpoint saving
199+
behavior is similar. This is why a single validation task is used for both.
200+
""",
173201
concurrency=2,
174202
) as dag:
203+
# Only one set of test configurations (e.g., v5p-128) is supported at the moment.
204+
# Other configurations (e.g., v5e and/or v6e) may be introduced later.
175205
test_configs = [
176206
TestConfig(
177207
cluster=XpkClusters.TPU_V5P_128_CLUSTER_ORBAX,
178208
machine_type="ct5p-hightpu-4t",
179209
accelerator="v5p-128",
180210
slices=[2],
181211
model_name="llama2-7b",
182-
short_id="max-sv",
212+
short_id="max-sv-loc",
183213
replicator_backup_time=30,
184214
step=100,
185215
local_checkpoint_step=20,
186-
checkpoint_step=25,
187216
ram_disk_size_in_mi="800000Mi",
188217
),
189218
]
@@ -200,9 +229,6 @@ def generate_workload_command(
200229
use_replicator=False,
201230
),
202231
]:
203-
folder = f"maxtext_{checkpointing.name}_orbax_save_local"
204-
output_dir = posixpath.join(BASE_OUTPUT_DIRECTORY, folder)
205-
206232
with TaskGroup(
207233
group_id=f"maxtext_{checkpointing.name}_orbax_save_local",
208234
) as group:
@@ -211,19 +237,18 @@ def generate_workload_command(
211237
for slice_num in test_config.slices:
212238
# We conditionally set the trigger_rule on the first task.
213239
# If first task group failed the next one can execute.
214-
init_delete_cpc = mtc.initiate_cpc_deletion(test_config.cpc_config)
215-
wait_delete_cpc = mtc.wait_for_cpc_deletion.override(
240+
wait_delete_cpc = checkpoint_util.wait_for_cpc_deletion.override(
216241
trigger_rule="all_done"
217242
)(test_config.cpc_config)
218-
apply_cpc = mtc.apply_cpc(test_config.cpc_config)
243+
apply_cpc = checkpoint_util.apply_cpc(test_config.cpc_config)
219244
workload_command = test_config.generate_workload_command(
220245
cp=checkpointing,
221246
checkpoint_dir=RAM_DISK,
222-
out_dir=output_dir,
247+
out_folder=group.group_id,
223248
slice_number=slice_num,
224249
)
225250

226-
start_time = log.generate_timestamp()
251+
start_time = validation_util.generate_timestamp()
227252
maxtext_chkpt_run_test = gke_config.get_gke_config(
228253
num_slices=slice_num,
229254
cluster=test_config.cluster,
@@ -255,20 +280,15 @@ def generate_workload_command(
255280
skip_post_process=True,
256281
)
257282

258-
end_time = log.generate_timestamp()
283+
end_time = validation_util.generate_timestamp()
259284
vali_step = test_config.step - 1
260285
vali_step_list = [
261286
i
262287
for i in range(0, vali_step, test_config.local_checkpoint_step)
263288
]
264289
vali_step_list.append(vali_step)
265290

266-
# Here we are looking for the string '(blocking + background)'.
267-
# We will compare expected steps with the ones we found when query
268-
# this regex. Should be the same. If for some reason the restore
269-
# start from 0 this task will fail
270-
# because len(valid_step_list) != len(founded_steps)
271-
validate_log = log.validate_log_with_step(
291+
validate_log = validation_util.validate_log_with_step(
272292
project_id=test_config.cluster.project,
273293
location=zone_to_region(test_config.cluster.zone),
274294
cluster_name=test_config.cluster.name,
@@ -279,8 +299,7 @@ def generate_workload_command(
279299
)
280300

281301
(
282-
init_delete_cpc
283-
>> wait_delete_cpc
302+
wait_delete_cpc
284303
>> apply_cpc
285304
>> start_time
286305
>> maxtext_chkpt_run_test

dags/orbax/util/checkpoint_util.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
"""Utility functions for managing Multi-tier Cluster Configuration.
2+
3+
This module provides tasks for creating, applying, and deleting
4+
Multi-tier Driver cluster Configurations for enable Multi Tier Checkpointing.
5+
"""
6+
7+
from absl import logging
8+
import yaml
9+
from dataclasses import dataclass
10+
11+
from kubernetes import client as k8s_client
12+
from kubernetes.client.rest import ApiException
13+
from airflow.decorators import task
14+
from airflow.exceptions import AirflowFailException
15+
from http import HTTPStatus
16+
17+
from xlml.utils import gke
18+
19+
20+
@dataclass
21+
class CheckpointConfiguration:
22+
"""A dataclass to hold attributes of a Cloud Public Compute (CPC) instance."""
23+
24+
project_id: str
25+
region: str
26+
cluster_name: str
27+
gcs_bucket: str
28+
machine_type: str
29+
ramdisk_memory: str
30+
toleration_key: str
31+
32+
def __init__(
33+
self,
34+
project_id: str,
35+
region: str,
36+
cluster_name: str,
37+
gcs_bucket: str,
38+
machine_type: str,
39+
ramdisk_memory_in_mi: str,
40+
toleration_key: str = "google.com/tpu",
41+
):
42+
"""
43+
Initializes the CheckpointConfiguration.
44+
45+
Args:
46+
project_id (str): The Google Cloud project ID.
47+
region (str): The Google Cloud region.
48+
cluster_name (str): The name of the GKE cluster.
49+
gcs_bucket (str): The name of the GCS bucket for checkpoints.
50+
machine_type (str): The machine type for the instance.
51+
ram_disk_memory_in_mi (str): The size of the RAM disk in mebibytes (Mi).
52+
The unit is in mebibytes (Mi) but the value should be passed as a
53+
string with the unit, e.g., "1G" or "1024Mi".
54+
toleration_key (str): The toleration key for the Kubernetes pod.
55+
Defaults to "google.com/tpu".
56+
"""
57+
self.project_id = project_id
58+
self.region = region
59+
self.cluster_name = cluster_name
60+
self.gcs_bucket = gcs_bucket
61+
self.machine_type = machine_type
62+
self.ramdisk_memory = ramdisk_memory_in_mi
63+
self.toleration_key = toleration_key
64+
65+
def load_yaml_and_parse_body(
66+
self,
67+
) -> tuple[str, str, str, str, dict[str, any]]:
68+
"""
69+
Loads a YAML string template, populates it with class attributes, and parses the resulting body.
70+
71+
This method constructs a CheckpointConfiguration YAML manifest as a string,
72+
using class attributes such as `self.gcs_bucket`, `self.machine_type`,
73+
`self.toleration_key`, and `self.ramdisk_memory` to fill in the
74+
placeholders. It then uses `yaml.safe_load` to convert this YAML string
75+
into a Python dictionary.
76+
77+
Finally, it extracts key fields—group, version, plural, and name—from the
78+
loaded dictionary for use in API requests or other operations.
79+
80+
Returns:
81+
tuple[str, str, str, str, dict[str, any]]: A tuple containing the
82+
extracted API group, API version, plural name, resource name, and the
83+
full parsed YAML body as a dictionary.
84+
"""
85+
86+
cpc_yaml_template = f"""
87+
apiVersion: checkpointing.gke.io/v1
88+
kind: CheckpointConfiguration
89+
metadata:
90+
name: my-checkpointconfiguration # This name will be used for deletion
91+
spec:
92+
cloudStorageBucketName: {self.gcs_bucket}
93+
nodeSelector:
94+
node.kubernetes.io/instance-type: {self.machine_type}
95+
tolerations:
96+
- key: {self.toleration_key}
97+
operator: Exists
98+
effect: NoSchedule
99+
inMemoryVolumeSize: {self.ramdisk_memory}
100+
"""
101+
cpc_body = yaml.safe_load(cpc_yaml_template)
102+
group, version = cpc_body.get("apiVersion").split("/", 1)
103+
plural = cpc_body.get("kind").lower() + "s"
104+
name = cpc_body.get("metadata", {}).get("name")
105+
106+
return group, version, plural, name, cpc_body
107+
108+
def create_custom_objects_api_client(self) -> k8s_client.CustomObjectsApi:
109+
"""Create a CustomObjectsApi client for the given cluster."""
110+
return k8s_client.CustomObjectsApi(
111+
gke.get_authenticated_client(
112+
self.project_id, self.region, self.cluster_name
113+
)
114+
)
115+
116+
117+
@task
118+
def apply_cpc(cpc: CheckpointConfiguration) -> None:
119+
"""Applies the CheckpointConfiguration to the cluster (create or replace)."""
120+
121+
custom_api = cpc.create_custom_objects_api_client()
122+
group, version, plural, name, cpc_body = cpc.load_yaml_and_parse_body()
123+
124+
try:
125+
# Here we first try to create a reasource
126+
logging.info(f"Attempting to create CheckpointConfiguration '{name}'...")
127+
custom_api.create_cluster_custom_object(
128+
group=group, version=version, plural=plural, body=cpc_body
129+
)
130+
logging.info(f"CheckpointConfiguration '{name}' created successfully.")
131+
132+
except ApiException as api_error:
133+
raise AirflowFailException(
134+
f"Failed to apply CheckpointConfiguration: {api_error.reason}"
135+
)
136+
137+
138+
def _delete_cpc(cpc: CheckpointConfiguration) -> bool:
139+
"""
140+
Sends the delete request for the CheckpointConfiguration.
141+
"""
142+
143+
custom_api = cpc.create_custom_objects_api_client()
144+
group, version, plural, name, _ = cpc.load_yaml_and_parse_body()
145+
146+
if not name:
147+
logging.error(
148+
"Could not determine CheckpointConfiguration name for deletion."
149+
)
150+
raise AirflowFailException("Failed to determine CPC name for deletion.")
151+
152+
delete_options = k8s_client.V1DeleteOptions(propagation_policy="Foreground")
153+
154+
try:
155+
logging.info(f"Attempting to delete CheckpointConfiguration: {name}")
156+
custom_api.delete_cluster_custom_object(
157+
group=group,
158+
version=version,
159+
plural=plural,
160+
name=name,
161+
body=delete_options,
162+
)
163+
logging.info(f"Delete request sent for CheckpointConfiguration '{name}'.")
164+
except Exception as e:
165+
logging.info(
166+
f"An warning has occurred while deleting CPC. Please take a look: {e}"
167+
)
168+
pass
169+
170+
try:
171+
custom_api.get_cluster_custom_object(
172+
group=group, version=version, plural=plural, name=name
173+
)
174+
except ApiException as e:
175+
return e.status == HTTPStatus.NOT_FOUND
176+
177+
178+
@task.sensor(poke_interval=10)
179+
def wait_for_cpc_deletion(cpc: CheckpointConfiguration) -> bool:
180+
"""
181+
A sensor that waits for the CheckpointConfiguration to be completely deleted.
182+
"""
183+
return _delete_cpc(cpc)

0 commit comments

Comments
 (0)