#!/usr/bin/env python3 """Profile the NCCL all-reduce paths consumed by the Qwen235B experiment.""" from __future__ import annotations import argparse import csv import hashlib import json import os import statistics import sys from datetime import datetime, timezone from pathlib import Path import torch import torch.distributed as dist DEFAULT_TOKENS = [ 1, 2, 3, 4, 6, 8, 12, 16, 24, 32, 48, 63, 64, 65, 96, 127, 128, 129, 192, 255, 256, 257, 384, 511, 512, 513, 768, 1023, 1024, 1025, 1536, 2047, 2048, 2049, 3072, 4095, 4096, 4097, 6144, 8191, 8192, 8193, 12288, 16383, 16384, ] def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("--output", type=Path, required=True) parser.add_argument("--hidden-size", type=int, default=4096) parser.add_argument("--world-sizes", type=int, nargs="+", default=[4, 8]) parser.add_argument("--tokens", type=int, nargs="+", default=DEFAULT_TOKENS) parser.add_argument("--warmup-iterations", type=int, default=5) parser.add_argument("--measured-iterations", type=int, default=30) return parser.parse_args() def sha256(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as source: for chunk in iter(lambda: source.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() def profile_one_size( *, group: dist.ProcessGroup, group_size: int, num_tokens: int, hidden_size: int, warmup_iterations: int, measured_iterations: int, ) -> list[float]: tensor = torch.ones( (num_tokens, hidden_size), dtype=torch.bfloat16, device="cuda" ) for _ in range(warmup_iterations): dist.all_reduce(tensor, group=group) dist.barrier(group=group) torch.cuda.synchronize() local_samples: list[float] = [] for _ in range(measured_iterations): start = torch.cuda.Event(enable_timing=True) end = torch.cuda.Event(enable_timing=True) start.record() dist.all_reduce(tensor, group=group) end.record() end.synchronize() local_samples.append(float(start.elapsed_time(end))) samples_by_rank: list[list[float] | None] = [None] * group_size dist.all_gather_object(samples_by_rank, local_samples, group=group) del tensor # Collective completion is limited by the slowest rank. Preserve that # critical-path statistic instead of reporting only rank 0. return [ max(float(rank_samples[index]) for rank_samples in samples_by_rank if rank_samples) for index in range(measured_iterations) ] def profile_group( *, group: dist.ProcessGroup, group_ranks: list[int], args: argparse.Namespace, ) -> list[dict[str, object]]: global_rank = dist.get_rank() if global_rank not in group_ranks: return [] group_size = len(group_ranks) group_rank = dist.get_rank(group=group) rows: list[dict[str, object]] = [] trial_orders = (("forward", args.tokens), ("reverse", list(reversed(args.tokens)))) for trial_id, (order_name, tokens) in enumerate(trial_orders): for num_tokens in tokens: samples = profile_one_size( group=group, group_size=group_size, num_tokens=num_tokens, hidden_size=args.hidden_size, warmup_iterations=args.warmup_iterations, measured_iterations=args.measured_iterations, ) if group_rank != 0: continue ordered = sorted(samples) p95_index = int(0.95 * (len(ordered) - 1)) rows.append( { "time_stats.all_reduce.min": min(samples), "time_stats.all_reduce.max": max(samples), "time_stats.all_reduce.mean": statistics.fmean(samples), "time_stats.all_reduce.median": statistics.median(samples), "time_stats.all_reduce.std": statistics.stdev(samples), "time_stats.all_reduce.p95": ordered[p95_index], "rank": 0, "num_workers": group_size, "size": num_tokens * args.hidden_size * 2, "collective": "all_reduce", "devices_per_node": group_size, "max_devices_per_node": dist.get_world_size(), "profiling_precision": "BF16", "measurement_type": "CUDA_EVENT", "backend": "nccl", "num_tokens": num_tokens, "hidden_size": args.hidden_size, "trial_id": trial_id, "order": order_name, "warmup_iterations": args.warmup_iterations, "measured_iterations": args.measured_iterations, } ) return rows def main() -> None: args = parse_args() if args.hidden_size <= 0: raise ValueError("hidden-size must be positive") if args.warmup_iterations <= 0 or args.measured_iterations < 2: raise ValueError("warmup must be positive and measured iterations must be >= 2") if sorted(set(args.tokens)) != sorted(args.tokens) or min(args.tokens) <= 0: raise ValueError("tokens must be unique positive integers") local_rank = int(os.environ["LOCAL_RANK"]) torch.cuda.set_device(local_rank) dist.init_process_group(backend="nccl") global_rank = dist.get_rank() global_world_size = dist.get_world_size() if max(args.world_sizes) > global_world_size: raise ValueError( f"requested world size {max(args.world_sizes)} exceeds torchrun world size " f"{global_world_size}" ) groups: dict[int, tuple[dist.ProcessGroup, list[int]]] = {} for world_size in sorted(set(args.world_sizes)): ranks = list(range(world_size)) group = dist.group.WORLD if world_size == global_world_size else dist.new_group(ranks) groups[world_size] = (group, ranks) all_rows: list[dict[str, object]] = [] for world_size in args.world_sizes: group, ranks = groups[world_size] all_rows.extend(profile_group(group=group, group_ranks=ranks, args=args)) dist.barrier() if global_rank == 0: expected_rows = len(set(args.world_sizes)) * len(args.tokens) * 2 if len(all_rows) != expected_rows: raise RuntimeError( f"profile row count mismatch: expected={expected_rows}, actual={len(all_rows)}" ) args.output.parent.mkdir(parents=True, exist_ok=True) with args.output.open("w", newline="", encoding="utf-8") as output_file: writer = csv.DictWriter(output_file, fieldnames=list(all_rows[0])) writer.writeheader() writer.writerows(all_rows) manifest = { "generated_at_utc": datetime.now(timezone.utc).isoformat(), "command": [sys.executable, *sys.argv], "output": str(args.output.resolve()), "output_sha256": sha256(args.output), "rows": len(all_rows), "world_sizes": args.world_sizes, "tokens": args.tokens, "hidden_size": args.hidden_size, "dtype": "torch.bfloat16", "backend": dist.get_backend(), "torch_version": torch.__version__, "cuda_version": torch.version.cuda, "nccl_version": list(torch.cuda.nccl.version()), "gpu": torch.cuda.get_device_name(0), "warmup_iterations": args.warmup_iterations, "measured_iterations": args.measured_iterations, "sample_semantics": "per-iteration maximum CUDA-event time across participating ranks", } manifest_path = args.output.with_suffix(args.output.suffix + ".manifest.json") manifest_path.write_text( json.dumps(manifest, indent=2) + "\n", encoding="utf-8" ) print(json.dumps(manifest, indent=2)) dist.destroy_process_group() if __name__ == "__main__": main()