Files
hao-ai-lab-FastVideo/fastvideo/models/loader/benchmarks/benchmark_weight_loading_comparison.py
William Lin 7d61590213 [bugfix] Fix the CUDA device of Ray workers on multi-GPU nodes (#1932)
Ray sets CUDA_VISIBLE_DEVICES of each GPU actor to the GPU of that actor. The
import of fastvideo initializes CUDA, so the executor cannot change the device
list later. On a node with more than one GPU, each worker after the first worker
failed with "invalid device ordinal".

On NVIDIA GPUs, each worker actor now keeps the device list of its raylet
(RAY_EXPERIMENTAL_NOSET_CUDA_VISIBLE_DEVICES=1). Each worker uses the position of
its GPU in that list as its local rank. The safetensors loader now reads each
file on node-group rank 0, which is the rank that sends the broadcast.
2026-10-07 15:00:23 -07:00

209 lines
6.9 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""A/B benchmark: independent-read vs rank-0-broadcast weight loading.
Compares two strategies:
- "before" (independent): every rank reads safetensors from disk to GPU
- "after" (broadcast): rank 0 reads from disk, broadcasts to other ranks
Usage:
# 1 GPU
torchrun --nproc_per_node=1 fastvideo/models/loader/benchmarks/benchmark_weight_loading_comparison.py \
--model-path /path/to/model --subfolder transformer
# 2 GPUs
torchrun --nproc_per_node=2 fastvideo/models/loader/benchmarks/benchmark_weight_loading_comparison.py \
--model-path /path/to/model --subfolder transformer
# 4 GPUs
torchrun --nproc_per_node=4 fastvideo/models/loader/benchmarks/benchmark_weight_loading_comparison.py \
--model-path /path/to/model --subfolder transformer
"""
import argparse
import os
import time
import torch
import torch.distributed as dist
from safetensors.torch import safe_open
from fastvideo.distributed.parallel_state import (
cleanup_dist_env_and_memory,
get_node_group,
maybe_init_distributed_environment_and_model_parallel,
)
from fastvideo.models.loader.weight_utils import (
SAFETENSORS_TO_TORCH_DTYPE,
resolve_safetensors_files,
)
def load_independent(files: list[str], device: str):
"""Before-PR behavior: every rank reads every tensor from disk to GPU."""
for st_file in files:
with safe_open(st_file, framework="pt", device=device) as f:
for name in f.keys(): # noqa: SIM118
param = f.get_tensor(name)
yield name, param
def load_broadcast(files: list[str], device: str, node_group, async_op: bool = False):
"""After-PR behavior: rank 0 reads from disk, broadcasts to other ranks."""
# The broadcast source is node-group rank 0, which is not always on CUDA device 0.
is_leader = node_group.rank_in_group == 0
handles = []
for st_file in files:
with safe_open(st_file, framework="pt", device=device) as f:
for name in f.keys(): # noqa: SIM118
if is_leader:
param = f.get_tensor(name)
else:
sl = f.get_slice(name)
shape = sl.get_shape()
dtype = SAFETENSORS_TO_TORCH_DTYPE[sl.get_dtype()]
param = torch.empty(shape, device=device, dtype=dtype)
if node_group.world_size > 1:
group = node_group.device_group
if async_op:
handle = dist.broadcast(
param,
src=dist.get_global_rank(group, 0),
async_op=True,
group=group,
)
handles.append(handle)
else:
dist.broadcast(
param,
src=dist.get_global_rank(group, 0),
group=group,
)
yield name, param
if async_op:
for handle in handles:
handle.wait()
handles.clear()
def run_benchmark(label, loader_fn, files, warmup, repeats):
rank = dist.get_rank()
# Count tensors/bytes on first pass
total_params = 0
total_bytes = 0
for name, tensor in loader_fn(files):
total_params += 1
total_bytes += tensor.nelement() * tensor.element_size()
dist.barrier()
# Warmup
for _ in range(warmup):
for _ in loader_fn(files):
pass
dist.barrier()
# Timed runs
times = []
for _ in range(repeats):
dist.barrier()
torch.cuda.synchronize()
t0 = time.perf_counter()
for _ in loader_fn(files):
pass
torch.cuda.synchronize()
dist.barrier()
times.append(time.perf_counter() - t0)
if rank == 0:
avg = sum(times) / len(times)
best = min(times)
throughput = total_bytes / avg / 1e9
print(f" {label:30s} "
f"avg {avg:.3f}s best {best:.3f}s "
f"throughput {throughput:.2f} GB/s "
f"({total_params} tensors, {total_bytes/1e9:.2f} GB)")
return avg, best, throughput, total_bytes
return None, None, None, None
def main():
parser = argparse.ArgumentParser(description="A/B benchmark: independent vs broadcast weight loading")
parser.add_argument("--model-path", type=str, required=True)
parser.add_argument("--subfolder", type=str, default=None)
parser.add_argument("--warmup", type=int, default=1)
parser.add_argument("--repeats", type=int, default=3)
args = parser.parse_args()
load_path = args.model_path
if args.subfolder:
load_path = os.path.join(load_path, args.subfolder)
files = resolve_safetensors_files(load_path)
maybe_init_distributed_environment_and_model_parallel(1, 1)
rank = dist.get_rank()
world_size = dist.get_world_size()
node_group = get_node_group()
local_rank = node_group.local_rank
device = f"cuda:{local_rank}"
if rank == 0:
print(f"\n{'='*72}")
print(f"Weight Loading Benchmark: {load_path}")
print(f"GPUs: {world_size} | safetensors files: {len(files)} "
f"| warmup: {args.warmup} | repeats: {args.repeats}")
print(f"{'='*72}")
# --- Before PR: every rank reads independently ---
before_avg, before_best, before_tp, total_bytes = run_benchmark(
"before (independent read)",
lambda f: load_independent(f, device),
files,
args.warmup,
args.repeats,
)
# Clear GPU memory between benchmarks
torch.cuda.empty_cache()
dist.barrier()
# --- After PR: rank 0 reads, sync broadcasts ---
after_sync_avg, after_sync_best, after_sync_tp, _ = run_benchmark(
"after (sync broadcast)",
lambda f: load_broadcast(f, device, node_group, async_op=False),
files,
args.warmup,
args.repeats,
)
# Clear GPU memory between benchmarks
torch.cuda.empty_cache()
dist.barrier()
# --- After PR: rank 0 reads, async broadcasts ---
after_async_avg, after_async_best, after_async_tp, _ = run_benchmark(
"after (async broadcast)",
lambda f: load_broadcast(f, device, node_group, async_op=True),
files,
args.warmup,
args.repeats,
)
if rank == 0:
print(f"{'─'*72}")
if before_avg and after_sync_avg:
sync_speedup = before_avg / after_sync_avg
print(f" sync speedup: {sync_speedup:.2f}x "
f"(saved {before_avg - after_sync_avg:.3f}s)")
if before_avg and after_async_avg:
async_speedup = before_avg / after_async_avg
print(f" async speedup: {async_speedup:.2f}x "
f"(saved {before_avg - after_async_avg:.3f}s)")
print(f"{'='*72}\n")
cleanup_dist_env_and_memory()
if __name__ == "__main__":
main()