Skip to content

Commit 4197b32

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 d157855 commit 4197b32

3 files changed

Lines changed: 151 additions & 120 deletions

File tree

dags/orbax/maxtext_mtc_emergency_save_local.py

Lines changed: 50 additions & 27 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,21 @@ 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+
init_delete_cpc = checkpoint_util.initiate_cpc_deletion(
241+
test_config.cpc_config
242+
)
243+
wait_delete_cpc = checkpoint_util.wait_for_cpc_deletion.override(
216244
trigger_rule="all_done"
217245
)(test_config.cpc_config)
218-
apply_cpc = mtc.apply_cpc(test_config.cpc_config)
246+
apply_cpc = checkpoint_util.apply_cpc(test_config.cpc_config)
219247
workload_command = test_config.generate_workload_command(
220248
cp=checkpointing,
221249
checkpoint_dir=RAM_DISK,
222-
out_dir=output_dir,
250+
out_folder=group.group_id,
223251
slice_number=slice_num,
224252
)
225253

226-
start_time = log.generate_timestamp()
254+
start_time = validation_util.generate_timestamp()
227255
maxtext_chkpt_run_test = gke_config.get_gke_config(
228256
num_slices=slice_num,
229257
cluster=test_config.cluster,
@@ -255,20 +283,15 @@ def generate_workload_command(
255283
skip_post_process=True,
256284
)
257285

258-
end_time = log.generate_timestamp()
286+
end_time = validation_util.generate_timestamp()
259287
vali_step = test_config.step - 1
260288
vali_step_list = [
261289
i
262290
for i in range(0, vali_step, test_config.local_checkpoint_step)
263291
]
264292
vali_step_list.append(vali_step)
265293

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(
294+
validate_log = validation_util.validate_log_with_step(
272295
project_id=test_config.cluster.project,
273296
location=zone_to_region(test_config.cluster.zone),
274297
cluster_name=test_config.cluster.name,

dags/orbax/util/multi_tier_checkpoint_util.py renamed to dags/orbax/util/checkpoint_util.py

Lines changed: 60 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from kubernetes.client.rest import ApiException
1313
from airflow.decorators import task
1414
from airflow.exceptions import AirflowFailException
15+
from http import HTTPStatus
1516

1617
from xlml.utils import gke
1718

@@ -27,7 +28,6 @@ class CheckpointConfiguration:
2728
machine_type: str
2829
ramdisk_memory: str
2930
toleration_key: str
30-
cpc_yaml_template: str
3131

3232
def __init__(
3333
self,
@@ -50,7 +50,7 @@ def __init__(
5050
machine_type (str): The machine type for the instance.
5151
ram_disk_memory_in_mi (str): The size of the RAM disk in mebibytes (Mi).
5252
The unit is in mebibytes (Mi) but the value should be passed as a
53-
string with the unit, e.g., "2G" or "2048M". Defaults to "100G"".
53+
string with the unit, e.g., "1G" or "1024Mi".
5454
toleration_key (str): The toleration key for the Kubernetes pod.
5555
Defaults to "google.com/tpu".
5656
"""
@@ -61,21 +61,49 @@ def __init__(
6161
self.machine_type = machine_type
6262
self.ramdisk_memory = ramdisk_memory_in_mi
6363
self.toleration_key = toleration_key
64-
self.cpc_yaml_template = f"""
65-
apiVersion: checkpointing.gke.io/v1
66-
kind: CheckpointConfiguration
67-
metadata:
68-
name: my-checkpointconfiguration # This name will be used for deletion
69-
spec:
70-
cloudStorageBucketName: {self.gcs_bucket}
71-
nodeSelector:
72-
node.kubernetes.io/instance-type: {self.machine_type}
73-
tolerations:
74-
- key: {self.toleration_key}
75-
operator: Exists
76-
effect: NoSchedule
77-
inMemoryVolumeSize: {self.ramdisk_memory}
78-
"""
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
79107

80108
def create_custom_objects_api_client(self) -> k8s_client.CustomObjectsApi:
81109
"""Create a CustomObjectsApi client for the given cluster."""
@@ -89,14 +117,9 @@ def create_custom_objects_api_client(self) -> k8s_client.CustomObjectsApi:
89117
@task
90118
def apply_cpc(cpc: CheckpointConfiguration) -> None:
91119
"""Applies the CheckpointConfiguration to the cluster (create or replace)."""
92-
custom_api = cpc.create_custom_objects_api_client()
93-
cpc_body = yaml.safe_load(cpc.cpc_yaml_template)
94-
api_version = cpc_body.get("apiVersion")
95-
kind = cpc_body.get("kind")
96-
name = cpc_body.get("metadata", {}).get("name")
97120

98-
group, version = api_version.split("/", 1)
99-
plural = f"{kind.lower()}s"
121+
custom_api = cpc.create_custom_objects_api_client()
122+
group, version, plural, name, cpc_body = cpc.load_yaml_and_parse_body()
100123

101124
try:
102125
# Here we first try to create a reasource
@@ -108,8 +131,7 @@ def apply_cpc(cpc: CheckpointConfiguration) -> None:
108131

109132
except ApiException as api_error:
110133
# If it already exists (409 Conflict), then try to replace it
111-
# See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409
112-
if api_error.status == 409:
134+
if api_error.status == HTTPStatus.CONFLICT:
113135
logging.info(
114136
f"CheckpointConfiguration '{name}' already exists. Attempting to replace..."
115137
)
@@ -142,43 +164,31 @@ def initiate_cpc_deletion(cpc: CheckpointConfiguration) -> None:
142164
Sends the delete request for the CheckpointConfiguration.
143165
"""
144166
custom_api = cpc.create_custom_objects_api_client()
145-
cpc_body = yaml.safe_load(cpc.cpc_yaml_template)
146-
name_to_delete = cpc_body.get("metadata", {}).get("name")
167+
group, version, plural, name, _ = cpc.load_yaml_and_parse_body()
147168

148-
if not name_to_delete:
169+
if not name:
149170
logging.error(
150171
"Could not determine CheckpointConfiguration name for deletion."
151172
)
152173
raise AirflowFailException("Failed to determine CPC name for deletion.")
153174

154-
api_version = cpc_body.get("apiVersion")
155-
kind = cpc_body.get("kind")
156-
group, version = api_version.split("/", 1)
157-
plural = f"{kind.lower()}s"
158-
159175
delete_options = k8s_client.V1DeleteOptions(propagation_policy="Foreground")
160-
161176
try:
162-
logging.info(
163-
f"Attempting to delete CheckpointConfiguration: {name_to_delete}"
164-
)
177+
logging.info(f"Attempting to delete CheckpointConfiguration: {name}")
165178
custom_api.delete_cluster_custom_object(
166179
group=group,
167180
version=version,
168181
plural=plural,
169-
name=name_to_delete,
182+
name=name,
170183
body=delete_options,
171184
)
172-
logging.info(
173-
f"Delete request sent for CheckpointConfiguration '{name_to_delete}'."
174-
)
185+
logging.info(f"Delete request sent for CheckpointConfiguration '{name}'.")
175186

176187
except ApiException as e:
177188
# The resource is already gone (404), so we can exit successfully
178-
# See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409
179-
if e.status == 404:
189+
if e.status == HTTPStatus.NOT_FOUND:
180190
logging.info(
181-
f"CheckpointConfiguration '{name_to_delete}' not found. Already deleted or never existed."
191+
f"CheckpointConfiguration '{name}' not found. Already deleted or never existed."
182192
)
183193
return
184194
else:
@@ -193,36 +203,28 @@ def wait_for_cpc_deletion(cpc: CheckpointConfiguration) -> bool:
193203
A sensor that waits for the CheckpointConfiguration to be completely deleted.
194204
"""
195205
custom_api = cpc.create_custom_objects_api_client()
196-
cpc_body = yaml.safe_load(cpc.cpc_yaml_template)
197-
name_to_delete = cpc_body.get("metadata", {}).get("name")
206+
group, version, plural, name, _ = cpc.load_yaml_and_parse_body()
198207

199-
if not name_to_delete:
208+
if not name:
200209
logging.error(
201210
"Could not determine CheckpointConfiguration name for deletion."
202211
)
203212
raise AirflowFailException("Failed to determine CPC name for deletion.")
204213

205-
api_version = cpc_body.get("apiVersion")
206-
kind = cpc_body.get("kind")
207-
group, version = api_version.split("/", 1)
208-
plural = f"{kind.lower()}s"
209-
210214
try:
211215
custom_api.get_cluster_custom_object(
212-
group=group, version=version, plural=plural, name=name_to_delete
216+
group=group, version=version, plural=plural, name=name
213217
)
214218
logging.info(
215-
f"CheckpointConfiguration '{name_to_delete}' still exists. "
219+
f"CheckpointConfiguration '{name}' still exists. "
216220
f"Polling again in 10s..."
217221
)
218222
return False
219223
except ApiException as e:
220224
# The resource is already gone (404), so we can exit successfully
221225
# See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409
222226
if e.status == 404:
223-
logging.info(
224-
f"CheckpointConfiguration: {name_to_delete} successfully deleted"
225-
)
227+
logging.info(f"CheckpointConfiguration: {name} successfully deleted")
226228
return True # Return True to tell the sensor to succeed
227229
else:
228230
logging.error(

0 commit comments

Comments
 (0)