nccl_collective_test.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. #!/usr/bin/env python3
  2. """Collective-corruption test for the four GLM boxes, no model loaded.
  3. Repeats the collectives a GLM-5.3-Flash TP=4 decode step issues -- bf16
  4. all-reduce of [M, 4096] (o_proj, KDA o_proj, MoE, embedding: ~90 per step)
  5. and the bf16 logits all-gather of [M, 38720] -> [M, 154880] -- with inputs
  6. every rank can regenerate deterministically, and checks each result:
  7. all-gather : bit-exact against the locally rebuilt concatenation
  8. all-reduce : "int" data (bf16 integers in [-32, 32], exact in bf16) must be
  9. bit-exact; "float" data must be within 2 bf16 ulps of an fp32
  10. reference; in both cases every rank must hold the identical
  11. result (checked by a gloo all_gather of a hash) and a repeated
  12. all-reduce of the same input must be bit-identical.
  13. Launch one process per box with RANK/WORLD_SIZE/MASTER_ADDR/MASTER_PORT set
  14. (see nccl_test_launch.sh). --mode picks the communicator:
  15. vllm : vLLM's own TP group (init_distributed_environment +
  16. initialize_model_parallel), i.e. the same dispatcher and PyNCCL
  17. communicator the model runner uses
  18. pynccl : vllm.distributed.device_communicators.pynccl.PyNcclCommunicator
  19. on a gloo process group
  20. torch : torch.distributed NCCL backend
  21. """
  22. import argparse
  23. import os
  24. import sys
  25. import time
  26. import torch
  27. import torch.distributed as dist
  28. HIDDEN = 4096
  29. VOCAB = 154880
  30. def parse():
  31. p = argparse.ArgumentParser()
  32. p.add_argument("--mode", default="vllm", choices=["vllm", "pynccl", "torch"])
  33. p.add_argument("--iters", type=int, default=2000, help="decode steps to emulate")
  34. p.add_argument("--ar-per-step", type=int, default=90)
  35. p.add_argument("--tokens", default="1,2,4,8,16,64")
  36. p.add_argument("--prefill-tokens", type=int, default=2304, help="0 to skip")
  37. p.add_argument("--data", default="both", choices=["int", "float", "both"])
  38. return p.parse_args()
  39. def main():
  40. args = parse()
  41. rank = int(os.environ["RANK"])
  42. world = int(os.environ["WORLD_SIZE"])
  43. master = os.environ["MASTER_ADDR"]
  44. port = int(os.environ["MASTER_PORT"])
  45. torch.cuda.set_device(0)
  46. dev = torch.device("cuda:0")
  47. init = f"tcp://{master}:{port}"
  48. for k in sorted(os.environ):
  49. if k.startswith(("NCCL_", "GLOO_", "VLLM_HOST_IP")):
  50. print(f"[rank {rank}] {k}={os.environ[k]}", flush=True)
  51. if args.mode == "vllm":
  52. from vllm.distributed import (
  53. get_tp_group,
  54. init_distributed_environment,
  55. initialize_model_parallel,
  56. )
  57. from vllm.config import VllmConfig, set_current_vllm_config
  58. _cfg_ctx = set_current_vllm_config(VllmConfig())
  59. _cfg_ctx.__enter__() # the TP group and its communicator read the config
  60. init_distributed_environment(world, rank, init, local_rank=0, backend="nccl")
  61. initialize_model_parallel(tensor_model_parallel_size=world)
  62. tp = get_tp_group()
  63. cpu_group = tp.cpu_group
  64. def all_reduce(x):
  65. return tp.all_reduce(x)
  66. def all_gather(x):
  67. return tp.all_gather(x, dim=-1)
  68. names = getattr(getattr(tp, "device_communicator", None), "__class__", type(None)).__name__
  69. print(f"[rank {rank}] vllm tp group world={tp.world_size} device_communicator={names}", flush=True)
  70. else:
  71. dist.init_process_group("gloo" if args.mode == "pynccl" else "nccl",
  72. init_method=init, rank=rank, world_size=world)
  73. cpu_group = dist.new_group(backend="gloo") if args.mode == "torch" else dist.group.WORLD
  74. if args.mode == "pynccl":
  75. from vllm.distributed.device_communicators.pynccl import PyNcclCommunicator
  76. comm = PyNcclCommunicator(group=dist.group.WORLD, device=dev)
  77. assert not comm.disabled
  78. def all_reduce(x):
  79. return comm.all_reduce(x)
  80. def all_gather(x):
  81. out = torch.empty(x.shape[0], x.shape[1] * world, dtype=x.dtype, device=dev)
  82. # nccl all_gather is rank-major over the flat buffer
  83. tmp = torch.empty(world, x.shape[0], x.shape[1], dtype=x.dtype, device=dev)
  84. comm.all_gather(tmp, x.contiguous())
  85. out.copy_(tmp.permute(1, 0, 2).reshape(x.shape[0], -1))
  86. return out
  87. else:
  88. def all_reduce(x):
  89. y = x.clone()
  90. dist.all_reduce(y)
  91. return y
  92. def all_gather(x):
  93. parts = [torch.empty_like(x) for _ in range(world)]
  94. dist.all_gather(parts, x.contiguous())
  95. return torch.cat(parts, dim=-1)
  96. def gen(seed, shape, data):
  97. g = torch.Generator(device=dev).manual_seed(seed)
  98. if data == "int":
  99. return torch.randint(-32, 33, shape, device=dev, generator=g).to(torch.bfloat16)
  100. return (torch.randn(shape, device=dev, generator=g) * 3).to(torch.bfloat16)
  101. def rank_hash(t):
  102. v = t.contiguous().view(torch.int16).reshape(-1).to(torch.int64)
  103. w = torch.arange(1, v.numel() + 1, device=dev, dtype=torch.int64)
  104. return int((v * w).sum().item() & 0xFFFFFFFFFFFF)
  105. def check_consistent(y, tag):
  106. h = rank_hash(y)
  107. objs = [None] * world
  108. dist.all_gather_object(objs, h, group=cpu_group)
  109. if len(set(objs)) != 1:
  110. return f"{tag}: ranks disagree, hashes={objs}"
  111. return None
  112. tokens = [int(t) for t in args.tokens.split(",")]
  113. datas = ["int", "float"] if args.data == "both" else [args.data]
  114. failures = 0
  115. t0 = time.time()
  116. total = 0
  117. for it in range(args.iters):
  118. M = tokens[it % len(tokens)]
  119. data = datas[it % len(datas)]
  120. for j in range(args.ar_per_step):
  121. seed = it * 1_000_003 + j * 7919
  122. xs = [gen(seed + r * 31, (M, HIDDEN), data) for r in range(world)]
  123. ref32 = torch.zeros(M, HIDDEN, device=dev)
  124. for r in range(world):
  125. ref32 += xs[r].float()
  126. y = all_reduce(xs[rank])
  127. y2 = all_reduce(xs[rank])
  128. total += 2
  129. torch.cuda.synchronize()
  130. msg = None
  131. if not torch.isfinite(y).all():
  132. msg = "non-finite output"
  133. elif data == "int":
  134. if not torch.equal(y.float(), ref32):
  135. bad = (y.float() != ref32).nonzero()
  136. msg = f"int all-reduce mismatch at {bad[:4].tolist()} got {y.float()[tuple(bad[0])].item()} want {ref32[tuple(bad[0])].item()}"
  137. else:
  138. err = (y.float() - ref32).abs()
  139. # bound relative to the partial sums NCCL may round, not the
  140. # (possibly cancelled) final value; the int rows are the exact check
  141. ulp = sum(x.float().abs() for x in xs).clamp(min=1e-2) * 2 ** -7
  142. if (err > 2 * ulp).any():
  143. bad = (err > 2 * ulp).nonzero()
  144. msg = f"float all-reduce error at {bad[:4].tolist()} got {y.float()[tuple(bad[0])].item()} want {ref32[tuple(bad[0])].item()}"
  145. if msg is None and not torch.equal(y, y2):
  146. msg = "repeat all-reduce differs (nondeterministic)"
  147. if msg is None and (j % 15 == 0):
  148. msg = check_consistent(y, "all-reduce")
  149. if msg:
  150. failures += 1
  151. print(f"[rank {rank}] FAIL step={it} ar={j} M={M} data={data}: {msg}", flush=True)
  152. # logits all-gather
  153. seed = it * 1_000_003 + 999_331
  154. xs = [gen(seed + r * 31, (M, VOCAB // world), "float") for r in range(world)]
  155. y = all_gather(xs[rank])
  156. total += 1
  157. torch.cuda.synchronize()
  158. exp = torch.cat(xs, dim=-1)
  159. if not torch.equal(y, exp):
  160. bad = (y != exp).nonzero()
  161. failures += 1
  162. print(f"[rank {rank}] FAIL step={it} all-gather M={M}: mismatch at {bad[:4].tolist()} (first bad column {int(bad[0,1])} = rank {int(bad[0,1]) // (VOCAB // world)}'s slice)", flush=True)
  163. if args.prefill_tokens and it % 200 == 0:
  164. xs = [gen(seed + 5 + r * 31, (args.prefill_tokens, HIDDEN), "int") for r in range(world)]
  165. ref32 = sum(x.float() for x in xs)
  166. y = all_reduce(xs[rank])
  167. total += 1
  168. torch.cuda.synchronize()
  169. if not torch.equal(y.float(), ref32):
  170. failures += 1
  171. print(f"[rank {rank}] FAIL step={it} prefill all-reduce [{args.prefill_tokens},4096] mismatch", flush=True)
  172. if it % 100 == 0 and rank == 0:
  173. print(f"[rank 0] step {it}/{args.iters} M={M} {data} collectives={total} failures={failures} {time.time() - t0:.0f}s", flush=True)
  174. # global verdict
  175. objs = [None] * world
  176. dist.all_gather_object(objs, failures, group=cpu_group)
  177. if rank == 0:
  178. print(f"RESULT: failures per rank {objs}, collectives per rank {total}, {'CLEAN' if sum(objs) == 0 else 'CORRUPT'}", flush=True)
  179. dist.barrier(group=cpu_group)
  180. sys.exit(0 if sum(objs) == 0 else 1)
  181. if __name__ == "__main__":
  182. main()