Files
kernbench2/benches/ccl_allreduce.py
T
ywkang 998cc85762 Add PE-level IPCQ collective infra + unified ccl_allreduce bench (ADR-0023)
Major changes:

PE-level IPCQ infrastructure:
- New PE_IPCQ component: ring-buffer control plane with 4-direction
  neighbor mapping, head/tail pointers, backpressure (poll/sleep).
- PE_DMA extended with vc_comm channel for IPCQ outbound/inbound DMA,
  including in-flight data snapshot (D9) and op_log recording at
  outbound time for Phase 2 replay correctness.
- IpcqDmaToken piggyback model: data + metadata travel together,
  atomic visibility at receiver (invariant I6).
- Credit return fast path: bottleneck-BW latency, no fabric vc_comm.

Phase 2 data execution (ADR-0020 integration):
- op_log extended: DmaWriteCmd now captures src_space/src_addr for
  Phase 2 dma_write copy; ipcq_copy ops recorded at outbound time.
- DataExecutor replays dma_write + ipcq_copy in t_start order.
- Engine._flush_data_phase: incremental cursor-based replay after
  each engine.wait() so host reads see post-Phase-2 data.
- KernelRunner Phase 1 writes disabled when op_log is active to
  prevent stale data from corrupting the MemoryStore snapshot.

TLContext / kernel API:
- tl.send(dir, src=TensorHandle), tl.recv(dir, shape, dtype),
  tl.recv_async, tl.wait(RecvFuture), copy_to_dst mode.
- TensorHandle operator overloading (add/sub/mul/div) via thread-local
  active TLContext → MathCmd dispatch through PE_MATH.
- PE-local scratch allocator for math output handles.
- tl.load returns space="hbm" handles for correct Phase 2 addressing.
- Additional math functions: maximum, minimum, fma, clamp, softmax, cdiv.

Unified ccl_allreduce bench (PyTorch-compat host code):
- Single benches/ccl_allreduce.py with run() + worker(rank, ws, torch)
  split matching real PyTorch DDP worker pattern.
- torch.distributed facade: init_process_group, get_world_size,
  get_rank, get_backend, all_reduce, barrier — only real PyTorch names.
- AhbmCCLBackend: eager install_ipcq at init, all_reduce dispatches
  kernel via tensor shard metadata (n_elem from shards[0].nbytes).
- world_size derived from topology spec (sips × cubes × pes_per_cube)
  with optional algorithm-level override in ccl.yaml.

Tensor API (PyTorch-compat surface):
- Tensor.numpy(): gather-aware (all shards via VA-based addressing).
- Tensor.copy_(source): scatter from host tensor into sharded target.
- RuntimeContext.from_numpy(arr): host-side staging tensor.
- Tensor.data property fixed to use numpy() (was shards[0]-only).

Algorithm modules moved to src/kernbench/ccl/algorithms/:
- ring_allreduce, mesh_allreduce, tree_allreduce, hello_send.
- Each module exports kernel_args(world_size, n_elem) helper.
- ccl.yaml module paths updated to kernbench.ccl.algorithms.*.

Dead code removed:
- 7 per-variant bench files (ccl_allreduce_{tcm,hbm,sram}, etc.).
- _run_ccl_bench greenlet-per-SIP scheduler.
- benches.loader.is_ccl_bench + run_rank detection.
- benches/ccl/ directory.

Tests:
- New test_ccl_allreduce_matrix.py: 7 parametrized cases
  (ring×3 buffers, ring 8/16, mesh 4, tree 7).
- New test_runtime_api_tensor.py: copy_/numpy/from_numpy unit tests.
- Existing tests updated for new import paths + world_size_override.

Docs:
- Korean ccl-author-guide.md and ADR-0023 paths updated.
- New English versions: ccl-author-guide.en.md, ADR-0023.en.md.

502 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:36:59 -07:00

130 lines
5.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""CCL all-reduce bench — single unified entry point.
Driven entirely by ``ccl.yaml`` + ``topology.yaml``:
- ``defaults.algorithm`` in ``ccl.yaml`` picks which kernel to run
(``ring_allreduce_{tcm,hbm,sram}`` / ``mesh_allreduce_4`` /
``tree_allreduce_7``).
- ``world_size`` is derived from the algorithm entry's override or from
the topology spec (``sips × cubes_per_sip × pes_per_cube``).
- The host code uses only real PyTorch ``torch.distributed`` names:
``init_process_group``, ``get_world_size``, ``get_rank``, ``all_reduce``.
The bench is split into ``worker(rank, world_size, torch)`` — the
per-rank business logic, designed to look like a real PyTorch DDP
training worker so future model benches can reuse the same skeleton —
and ``run(torch)`` — the kernbench-specific launcher that initializes
the process group and invokes the worker.
"""
from __future__ import annotations
import numpy as np
from kernbench.ccl.install import load_ccl_config, resolve_algorithm_config
from kernbench.policy.placement.dp import DPPolicy
# Default per-rank tile size if ccl.yaml doesn't override it. Real
# pytorch benches hardcode batch/feature dims similarly.
DEFAULT_N_ELEM = 32
def _derive_dp(spec: dict, world_size: int) -> DPPolicy:
"""Pick a DPPolicy that fans the tensor across exactly ``world_size`` PEs.
Mirrors what a real PyTorch DDP user does manually with
``tensor.to(f"cuda:{rank}")``: the host code chooses the placement so
that the collective sees the right number of participating ranks.
"""
sips = int(spec["system"]["sips"]["count"])
cm = spec["sip"]["cube_mesh"]
pl = spec["cube"]["pe_layout"]
pes_per_cube = int(pl["pe_per_corner"]) * len(pl["corners"])
cubes_per_sip = int(cm["w"]) * int(cm["h"])
total = sips * cubes_per_sip * pes_per_cube
if world_size == total:
return DPPolicy(sip="column_wise", cube="column_wise", pe="column_wise")
if world_size <= pes_per_cube:
return DPPolicy(
sip="replicate", cube="replicate", pe="column_wise",
num_sips=1, num_cubes=1, num_pes=world_size,
)
if world_size <= cubes_per_sip * pes_per_cube:
return DPPolicy(
sip="replicate", cube="column_wise", pe="column_wise",
num_sips=1, num_cubes=world_size // pes_per_cube,
)
return DPPolicy(sip="column_wise", cube="column_wise", pe="column_wise")
def worker(rank: int, world_size: int, torch) -> None:
"""Per-rank business logic. Mirrors a real PyTorch DDP worker.
In real PyTorch DDP, this function runs in N separate processes,
each with its own ``rank``. In kernbench (single-process multi-device)
it is invoked once with ``rank=0`` on the single host driver; the
actual per-PE parallelism is handled by ``torch.launch`` fanning out
the kernel across all participating PEs via the tensor's DPPolicy.
The ``rank`` parameter is therefore always 0 today, and is kept as
an explicit argument for parity with real DDP workers (``if rank ==
0`` logging guards, future multi-host extensions).
"""
cfg = resolve_algorithm_config(load_ccl_config())
algo_name = cfg["algorithm"]
n_elem = int(cfg.get("n_elem", DEFAULT_N_ELEM))
# Pick a DP that produces exactly ``world_size`` shards on this topology.
dp = _derive_dp(torch.spec, world_size)
tensor = torch.zeros(
(1, world_size * n_elem), dtype="f16", dp=dp, name="ccl_in",
)
# Initialize: CCL rank r's slice gets value (r + 1). Real PyTorch idiom:
# target.copy_(torch.from_numpy(source))
init = np.zeros((1, world_size * n_elem), dtype=np.float16)
for r in range(world_size):
init[0, r * n_elem : (r + 1) * n_elem] = float(r + 1)
tensor.copy_(torch.from_numpy(init))
# The main act: one all_reduce call — the backend installs IPCQ at
# init_process_group time and here only dispatches the kernel.
torch.distributed.all_reduce(tensor, op="sum")
# Verify: each shard should hold sum(1..world_size) after all-reduce.
result = tensor.numpy()
expected = float(sum(range(1, world_size + 1)))
all_ok = bool(np.allclose(result, expected, rtol=1e-1, atol=1e-1))
# Print only on rank 0 — real PyTorch DDP idiom for single-source logs.
if rank == 0:
if all_ok:
print(f" {algo_name} (ws={world_size}): {world_size} OK")
else:
flat = result.reshape(-1)
n_fail = 0
for r in range(world_size):
slice_r = flat[r * n_elem : (r + 1) * n_elem]
if not np.allclose(slice_r, expected, rtol=1e-1, atol=1e-1):
n_fail += 1
if n_fail <= 5:
print(
f" [FAIL] rank {r} "
f"(ws={world_size}, algo={algo_name}): "
f"got mean={float(slice_r.mean()):.3f}, "
f"expected={expected:.3f}"
)
print(
f" {algo_name} (ws={world_size}): "
f"{world_size - n_fail} OK / {n_fail} FAIL"
)
def run(torch) -> None:
"""CLI entry point: initialize the process group, invoke worker."""
dist = torch.distributed
dist.init_process_group(backend="ahbm")
worker(
rank=dist.get_rank(),
world_size=dist.get_world_size(),
torch=torch,
)