Skip to content

Commit 9b6d5f3

Browse files
raytomatochanglan
authored andcommitted
Improve quality of the benchmark script.
GitOrigin-RevId: 405ef4a
1 parent 1432057 commit 9b6d5f3

1 file changed

Lines changed: 110 additions & 29 deletions

File tree

axlearn/cloud/gcp/examples/colocated_python_benchmark.py

Lines changed: 110 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
#!/usr/bin/env python3
2-
"""
3-
Standalone script to preload a model from GCS using Colocated Python.
2+
"""Standalone script to preload a model from GCS using Colocated Python.
43
54
This script reads the checkpoint index to determine the model structure and creates
65
appropriate TensorSpec objects for preloading.
76
87
Usage:
9-
python colocated_python_benchmark.py --ckpt_path gs://your-bucket/path/to/checkpoint
8+
# Run colocated benchmark (default, no profiling)
9+
python colocated_python_benchmark.py \
10+
--ckpt_path gs://your-bucket/path/to/checkpoint --method colocated
11+
12+
# Run colocated benchmark with profiling
13+
python colocated_python_benchmark.py \
14+
--ckpt_path gs://your-bucket/path/to/checkpoint --method colocated --profile
15+
16+
# Run default (direct to TPU) benchmark
17+
python colocated_python_benchmark.py \
18+
--ckpt_path gs://your-bucket/path/to/checkpoint --method default
1019
"""
1120

1221
import argparse
@@ -17,7 +26,9 @@
1726
import sys
1827
import time
1928
from concurrent.futures import ThreadPoolExecutor
20-
from typing import Any, Dict, Sequence
29+
from contextlib import nullcontext
30+
from datetime import datetime
31+
from typing import Any, Dict, Optional, Sequence
2132

2233
import jax
2334
import jax.numpy as jnp
@@ -33,14 +44,31 @@
3344
from axlearn.common.utils import TensorSpec, infer_mesh_shape
3445

3546

47+
def maybe_profile(enabled: bool, profile_dir: Optional[str]):
48+
"""Return JAX profiler context if enabled, otherwise a no-op context manager.
49+
50+
Args:
51+
enabled: Whether profiling is enabled.
52+
profile_dir: Directory to save profiling results.
53+
54+
Returns:
55+
Context manager for profiling or no-op.
56+
"""
57+
if enabled:
58+
assert profile_dir is not None, "profile_dir must be set when profiling is enabled"
59+
return jax.profiler.trace(profile_dir)
60+
else:
61+
# Return a no-op context manager
62+
return nullcontext()
63+
64+
3665
def _colocated_deserialize(
3766
shardings: Sequence[jax.sharding.NamedSharding],
3867
tensorstore_specs: Sequence[Dict[str, Any]],
3968
global_shapes: Sequence[tuple],
4069
dtypes: Sequence[jnp.dtype],
4170
):
42-
# concurrent_bytes = 1099511627776
43-
concurrent_bytes = 34359738368 * 6 # multiple of 32GB
71+
concurrent_bytes = 34359738368 * 6 # 32GB * 6
4472
cpu_devices = colocated_python.colocated_cpu_devices(jax.devices())
4573
print(f"{cpu_devices=}")
4674

@@ -106,7 +134,7 @@ async def gather_func():
106134
return result
107135

108136

109-
def create_mesh(mesh_shape=(1, 1, 1, 1, 1, 1, -1)):
137+
def create_mesh(mesh_shape=(1, 1, 1, 1, 1, 16, -1)):
110138
"""Create a JAX mesh for distributed computation."""
111139
inferred_mesh_shape = infer_mesh_shape(mesh_shape)
112140
print(f"Using mesh shape {inferred_mesh_shape} for {len(jax.devices())} devices")
@@ -206,6 +234,38 @@ def create_checkpoint_spec_from_state(ckpt_dir: str, state_spec: dict):
206234
return tensorstore_specs, shardings, shapes, dtypes
207235

208236

237+
def cleanup_loaded_arrays(loaded_arrays: list) -> None:
238+
"""Clean up loaded arrays and free device memory.
239+
240+
This function ensures a fair comparison between benchmarks by:
241+
1. Blocking until all async operations complete
242+
2. Deleting Python references to free device memory
243+
3. Clearing JAX caches
244+
4. Forcing device synchronization to ensure HBM cleanup completes
245+
246+
Args:
247+
loaded_arrays: List of JAX arrays to clean up.
248+
"""
249+
print("\nCleaning up arrays...")
250+
251+
# Block until all arrays are ready (ensures async ops complete)
252+
for arr in loaded_arrays:
253+
arr.block_until_ready()
254+
255+
loaded_arrays.clear()
256+
del loaded_arrays
257+
258+
# Clear JAX in-memory compilation cache
259+
# (Persistent cache is disabled via JAX_ENABLE_COMPILATION_CACHE=0)
260+
jax.clear_caches()
261+
262+
# Force device synchronization to ensure all HBM deallocations complete
263+
# This is critical - without it, deallocation may be async and incomplete
264+
jax.block_until_ready(jax.numpy.array(0))
265+
266+
print("Cleanup complete.")
267+
268+
209269
def _default_deserialize(
210270
shardings: Sequence[jax.sharding.NamedSharding],
211271
tensorstore_specs: Sequence[Dict[str, Any]],
@@ -331,37 +391,58 @@ def main():
331391
required=True,
332392
help="GCS path to checkpoint directory (e.g., gs://bucket/path/to/checkpoint)",
333393
)
394+
parser.add_argument(
395+
"--method",
396+
choices=["colocated", "default"],
397+
default="colocated",
398+
help="Loading method to benchmark: 'colocated' (CPU preload) or 'default' (direct to TPU)",
399+
)
400+
parser.add_argument(
401+
"--profile",
402+
action="store_true",
403+
help="Enable JAX profiler (adds overhead, disable for accurate benchmarking)",
404+
)
334405
args = parser.parse_args()
335406

407+
# Disable persistent compilation cache for fair benchmarking
408+
# This ensures benchmarks compile fresh and don't benefit from cached kernels
409+
os.environ["JAX_ENABLE_COMPILATION_CACHE"] = "0"
410+
336411
if os.getenv("JAX_PLATFORMS") == "proxy":
337412
pathwaysutils.initialize()
338413
else:
339414
jax.distributed.initialize()
340415

341416
print(f"JAX devices: {jax.devices()}")
342417

343-
print("--- Running colocated benchmark ---")
344-
# Extract profile dir from ckpt_path. The profile dir should be gs://bucket/profiles/
345-
hostname = os.uname().nodename
346-
profile_dir = f"{args.ckpt_path.split("/checkpoints")[0]}/profiles/{hostname}/colocated-test/"
347-
jax.profiler.start_trace(log_dir=profile_dir)
348-
start_colocated_time = time.perf_counter()
349-
loaded_values_colocated = load_model_colocated(ckpt_path=args.ckpt_path)
350-
print(f"✅ Successfully loaded model from {args.ckpt_path}")
351-
print(f"Deserialize took {time.perf_counter() - start_colocated_time:.2f} seconds")
352-
print(f" Total parameters: {sum(x.size for x in loaded_values_colocated):,}")
353-
jax.profiler.stop_trace()
354-
355-
# Exit early if on pathways
356-
if os.getenv("JAX_PLATFORMS") == "proxy":
357-
sys.exit(0)
358-
359-
print("\n--- Running default benchmark ---")
360-
start_default_time = time.perf_counter()
361-
loaded_values_default = load_model_default(ckpt_path=args.ckpt_path)
362-
print(f"✅ Successfully loaded model from {args.ckpt_path}")
363-
print(f"Deserialize took {time.perf_counter() - start_default_time:.2f} seconds")
364-
print(f" Total parameters: {sum(x.size for x in loaded_values_default):,}")
418+
# Select loading function and profile prefix based on method
419+
if args.method == "colocated":
420+
loader_fn = load_model_colocated
421+
else: # args.method == "default"
422+
loader_fn = load_model_default
423+
print(f"--- Running {args.method} benchmark ---")
424+
425+
profile_dir = None
426+
if args.profile:
427+
# Create timestamped profile directory (minute-level granularity)
428+
timestamp = datetime.now().strftime("%Y%m%d%H%M")
429+
profile_dir = (
430+
f"{args.ckpt_path.split("/checkpoints")[0]}/profiles/{args.method}_{timestamp}/"
431+
)
432+
print(f"Profiling enabled - results will be saved to {profile_dir}")
433+
434+
loaded_values = None
435+
try:
436+
with maybe_profile(args.profile, profile_dir):
437+
start_time = time.perf_counter()
438+
loaded_values = loader_fn(ckpt_path=args.ckpt_path)
439+
print(f"✅ Successfully loaded model from {args.ckpt_path}")
440+
print(f"Deserialize took {time.perf_counter() - start_time:.2f} seconds")
441+
print(f" Total parameters: {sum(x.size for x in loaded_values):,}")
442+
finally:
443+
# Always clean up, even if benchmark fails
444+
if loaded_values is not None:
445+
cleanup_loaded_arrays(loaded_values)
365446

366447

367448
if __name__ == "__main__":

0 commit comments

Comments
 (0)