|
1 | 1 | #!/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. |
4 | 3 |
|
5 | 4 | This script reads the checkpoint index to determine the model structure and creates |
6 | 5 | appropriate TensorSpec objects for preloading. |
7 | 6 |
|
8 | 7 | 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 |
10 | 19 | """ |
11 | 20 |
|
12 | 21 | import argparse |
|
17 | 26 | import sys |
18 | 27 | import time |
19 | 28 | 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 |
21 | 32 |
|
22 | 33 | import jax |
23 | 34 | import jax.numpy as jnp |
|
33 | 44 | from axlearn.common.utils import TensorSpec, infer_mesh_shape |
34 | 45 |
|
35 | 46 |
|
| 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 | + |
36 | 65 | def _colocated_deserialize( |
37 | 66 | shardings: Sequence[jax.sharding.NamedSharding], |
38 | 67 | tensorstore_specs: Sequence[Dict[str, Any]], |
39 | 68 | global_shapes: Sequence[tuple], |
40 | 69 | dtypes: Sequence[jnp.dtype], |
41 | 70 | ): |
42 | | - # concurrent_bytes = 1099511627776 |
43 | | - concurrent_bytes = 34359738368 * 6 # multiple of 32GB |
| 71 | + concurrent_bytes = 34359738368 * 6 # 32GB * 6 |
44 | 72 | cpu_devices = colocated_python.colocated_cpu_devices(jax.devices()) |
45 | 73 | print(f"{cpu_devices=}") |
46 | 74 |
|
@@ -106,7 +134,7 @@ async def gather_func(): |
106 | 134 | return result |
107 | 135 |
|
108 | 136 |
|
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)): |
110 | 138 | """Create a JAX mesh for distributed computation.""" |
111 | 139 | inferred_mesh_shape = infer_mesh_shape(mesh_shape) |
112 | 140 | 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): |
206 | 234 | return tensorstore_specs, shardings, shapes, dtypes |
207 | 235 |
|
208 | 236 |
|
| 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 | + |
209 | 269 | def _default_deserialize( |
210 | 270 | shardings: Sequence[jax.sharding.NamedSharding], |
211 | 271 | tensorstore_specs: Sequence[Dict[str, Any]], |
@@ -331,37 +391,58 @@ def main(): |
331 | 391 | required=True, |
332 | 392 | help="GCS path to checkpoint directory (e.g., gs://bucket/path/to/checkpoint)", |
333 | 393 | ) |
| 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 | + ) |
334 | 405 | args = parser.parse_args() |
335 | 406 |
|
| 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 | + |
336 | 411 | if os.getenv("JAX_PLATFORMS") == "proxy": |
337 | 412 | pathwaysutils.initialize() |
338 | 413 | else: |
339 | 414 | jax.distributed.initialize() |
340 | 415 |
|
341 | 416 | print(f"JAX devices: {jax.devices()}") |
342 | 417 |
|
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) |
365 | 446 |
|
366 | 447 |
|
367 | 448 | if __name__ == "__main__": |
|
0 commit comments