From 84a1325e5c8b9b2236610a25d08f41a8a607e361 Mon Sep 17 00:00:00 2001 From: Mukesh Garg Date: Mon, 27 Apr 2026 21:28:34 -0700 Subject: [PATCH] ADR-0023 D9.7: IPCQ slot-memory latency model (TCM/SRAM/HBM) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Charge per-tier bandwidth + setup overhead at IPCQ slot WRITE (receiver inbound DMA, in pe_dma._handle_ipcq_inbound) and slot READ (recv consume, in pe_ipcq._handle_recv). Tier table (common/ipcq_types.py): tcm : 512 GB/s, 0 ns sram : 128 GB/s, 2 ns hbm : 32 GB/s, 6 ns Before this change, slot read/write was free regardless of buffer_kind, making memory-tier choice invisible in simulated latency. After the change, swapping buffer_kind in ccl.yaml produces measurable per-tier separation in allreduce latency. Tests: test_ipcq_buffer_kind_latency.py — three micro-tests asserting tcm < sram < hbm ordering, payload-scaling, and that buffer_kind sensitivity grows with payload (credit-only path stays fabric-bound). test_allreduce_buffer_kind_sweep.py — 12-config parametrized sweep emitting buffer_kind_sweep.png (3 lines, torus_2d). conftest sessionfinish hook generalised to dispatch multiple sweep aggregators (allreduce + buffer-kind). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../buffer_kind_sweep.csv | 13 ++ .../buffer_kind_sweep.png | Bin 0 -> 69684 bytes src/kernbench/common/ipcq_types.py | 20 ++ src/kernbench/components/builtin/pe_dma.py | 10 + src/kernbench/components/builtin/pe_ipcq.py | 10 + tests/conftest.py | 38 +-- tests/test_allreduce_buffer_kind_sweep.py | 196 ++++++++++++++++ tests/test_ipcq_buffer_kind_latency.py | 219 ++++++++++++++++++ 8 files changed, 489 insertions(+), 17 deletions(-) create mode 100644 docs/diagrams/allreduce_latency_plots/buffer_kind_sweep.csv create mode 100644 docs/diagrams/allreduce_latency_plots/buffer_kind_sweep.png create mode 100644 tests/test_allreduce_buffer_kind_sweep.py create mode 100644 tests/test_ipcq_buffer_kind_latency.py diff --git a/docs/diagrams/allreduce_latency_plots/buffer_kind_sweep.csv b/docs/diagrams/allreduce_latency_plots/buffer_kind_sweep.csv new file mode 100644 index 0000000..c41cccf --- /dev/null +++ b/docs/diagrams/allreduce_latency_plots/buffer_kind_sweep.csv @@ -0,0 +1,13 @@ +buffer_kind,sip_topology,n_sips,n_elem,bytes_per_pe,latency_ns +hbm,torus_2d,6,128,256,2002.0399999999827 +hbm,torus_2d,6,1024,2048,3541.0399999999827 +hbm,torus_2d,6,8192,16384,15889.03999999999 +hbm,torus_2d,6,32768,65536,58225.03999999998 +sram,torus_2d,6,128,256,1762.0399999999827 +sram,torus_2d,6,1024,2048,2293.0399999999827 +sram,torus_2d,6,8192,16384,6577.039999999986 +sram,torus_2d,6,32768,65536,21265.03999999992 +tcm,torus_2d,6,128,256,1678.0399999999827 +tcm,torus_2d,6,1024,2048,1957.0399999999827 +tcm,torus_2d,6,8192,16384,4225.039999999986 +tcm,torus_2d,6,32768,65536,12001.03999999992 diff --git a/docs/diagrams/allreduce_latency_plots/buffer_kind_sweep.png b/docs/diagrams/allreduce_latency_plots/buffer_kind_sweep.png new file mode 100644 index 0000000000000000000000000000000000000000..194b9751c4206019d69e781d2b326311726ee710 GIT binary patch literal 69684 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

`wbJ6WgF;-v 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 DD^}2O literal 0 HcmV?d00001 diff --git a/src/kernbench/common/ipcq_types.py b/src/kernbench/common/ipcq_types.py index 578aaed..3dc6e0f 100644 --- a/src/kernbench/common/ipcq_types.py +++ b/src/kernbench/common/ipcq_types.py @@ -31,6 +31,26 @@ class IpcqInvalidDirection(ValueError): has no neighbor installed for this PE.""" +# ── ADR-0023 D9.7: IPCQ slot-memory latency model ─────────────────── +# +# Per-tier (bw_gbs, overhead_ns) used to charge the slot write (inbound) +# and slot read (recv consume). Mirrors topology.yaml component values. +_BUFFER_KIND_BW: dict[str, tuple[float, float]] = { + "tcm": (512.0, 0.0), + "sram": (128.0, 2.0), + "hbm": (32.0, 6.0), +} + + +def slot_io_latency_ns(buffer_kind: str, nbytes: int) -> float: + """Per-access latency for one slot read/write of ``nbytes`` against + the IPCQ backing memory tier (``buffer_kind``).""" + bw_gbs, overhead_ns = _BUFFER_KIND_BW.get( + buffer_kind, _BUFFER_KIND_BW["tcm"], + ) + return float(nbytes) / bw_gbs + overhead_ns + + # ── D2.5: IpcqEndpoint ─────────────────────────────────────────────── diff --git a/src/kernbench/components/builtin/pe_dma.py b/src/kernbench/components/builtin/pe_dma.py index 04c6129..35fca81 100644 --- a/src/kernbench/components/builtin/pe_dma.py +++ b/src/kernbench/components/builtin/pe_dma.py @@ -219,6 +219,16 @@ class PeDmaComponent(PeEngineBase): token = txn.request + # ADR-0023 D9.7: charge IPCQ slot-WRITE latency against the + # backing-memory tier (tcm/sram/hbm) before the atomic block. + # Must come BEFORE the atomic write→IpcqMetaArrival pair (I6). + from kernbench.common.ipcq_types import slot_io_latency_ns + slot_write_ns = slot_io_latency_ns( + token.dst_endpoint.buffer_kind, token.nbytes, + ) + if slot_write_ns > 0: + yield env.timeout(slot_write_ns) + # ── ATOMIC: do not introduce yield between these two operations ── # 1. Move data via MemoryStore (single-hop DMA write). # Prefer the in-flight snapshot stashed by the sender PE_DMA; diff --git a/src/kernbench/components/builtin/pe_ipcq.py b/src/kernbench/components/builtin/pe_ipcq.py index 8ef75dd..43a456c 100644 --- a/src/kernbench/components/builtin/pe_ipcq.py +++ b/src/kernbench/components/builtin/pe_ipcq.py @@ -329,6 +329,16 @@ class PeIpcqComponent(ComponentBase): qp["my_tail"] += 1 + # ADR-0023 D9.7: charge IPCQ slot-READ latency against the + # backing-memory tier (tcm/sram/hbm). Recv blocks for the + # kernel-side slot consume; pe_exec_ns reflects this cost. + from kernbench.common.ipcq_types import slot_io_latency_ns + slot_read_ns = slot_io_latency_ns( + self._buffer_kind, req.result_data.get("nbytes", 0), + ) + if slot_read_ns > 0: + yield env.timeout(slot_read_ns) + # Diagnostics trace (D14) from kernbench.ccl import diagnostics if diagnostics.trace_enabled(): diff --git a/tests/conftest.py b/tests/conftest.py index 1c6cced..3d9725b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -27,23 +27,27 @@ def pytest_sessionfinish(session, exitstatus): import sys from pathlib import Path - mod_path = Path(__file__).parent / "test_allreduce_multidevice.py" - if not mod_path.exists(): - return - spec = importlib.util.spec_from_file_location( - "_test_allreduce_multidevice_for_aggregate", mod_path, - ) - if spec is None or spec.loader is None: - return - mod = importlib.util.module_from_spec(spec) - sys.modules[spec.name] = mod - try: - spec.loader.exec_module(mod) - agg = getattr(mod, "_aggregate_sweep_plots", None) - if agg is not None: - agg() - except Exception as e: - print(f"[conftest] sweep aggregation failed: {e}") + def _exec(name: str, attr: str) -> None: + mod_path = Path(__file__).parent / name + if not mod_path.exists(): + return + s = importlib.util.spec_from_file_location( + f"_{name.removesuffix('.py')}_for_aggregate", mod_path, + ) + if s is None or s.loader is None: + return + mod = importlib.util.module_from_spec(s) + sys.modules[s.name] = mod + try: + s.loader.exec_module(mod) + fn = getattr(mod, attr, None) + if fn is not None: + fn() + except Exception as e: + print(f"[conftest] aggregator {attr}() in {name} failed: {e}") + + _exec("test_allreduce_multidevice.py", "_aggregate_sweep_plots") + _exec("test_allreduce_buffer_kind_sweep.py", "aggregate_buffer_kind_plot") @pytest.fixture(scope="session") diff --git a/tests/test_allreduce_buffer_kind_sweep.py b/tests/test_allreduce_buffer_kind_sweep.py new file mode 100644 index 0000000..9e8aab9 --- /dev/null +++ b/tests/test_allreduce_buffer_kind_sweep.py @@ -0,0 +1,196 @@ +"""Phase 1 buffer-kind allreduce sweep — torus_2d 6 SIPs. + +Parametrized over (buffer_kind, n_elem). Each case runs the standard +config-driven allreduce app and writes a JSON row to a shared staging +dir; the conftest sessionfinish hook (added in Phase 1) aggregates +rows into ``docs/diagrams/allreduce_latency_plots/buffer_kind_sweep.png``. + +Pre-Phase-2: the three buffer-kind lines overlap exactly because slot +access is latency-free today. Post-Phase-2 they spread out (tcm +fastest, hbm slowest). +""" +from __future__ import annotations + +import json +from pathlib import Path + +import pytest +import yaml + +from kernbench.runtime_api.context import RuntimeContext +from kernbench.runtime_api.types import DeviceSelector +from kernbench.sim_engine.engine import GraphEngine +from kernbench.topology.builder import resolve_topology + +# Reuse the allreduce app helpers. +from tests.test_allreduce_multidevice import ( + _write_temp_configs, + run_allreduce, +) + + +_BUFFER_KINDS = ["tcm", "sram", "hbm"] +_N_ELEM_GRID = [128, 1024, 8192, 32768] # 256 B → 64 KB per slot +_ELEM_BYTES_F16 = 2 + +_OUT_DIR = (Path(__file__).parent.parent / "docs" / "diagrams" + / "allreduce_latency_plots") +_ROWS_DIR = _OUT_DIR / "_buffer_kind_rows" + + +def _bk_params(): + out = [] + for bk in _BUFFER_KINDS: + for n_elem in _N_ELEM_GRID: + out.append(pytest.param(bk, n_elem, id=f"{bk}-n_elem{n_elem}")) + return out + + +@pytest.mark.parametrize("buffer_kind,n_elem", _bk_params()) +def test_buffer_kind_allreduce_one(tmp_path, buffer_kind, n_elem): + """One config of the buffer-kind sweep. xdist parallelizes.""" + sub = tmp_path / f"{buffer_kind}_{n_elem}" + sub.mkdir() + topo_path, ccl_path = _write_temp_configs( + sub, + sip_topology="torus_2d", + n_sips=6, + algorithm="intercube_allreduce", + sip_w=3, sip_h=2, + n_elem_override=n_elem, + ) + # Override buffer_kind in the temp ccl.yaml. + with open(ccl_path) as f: + ccl_cfg = yaml.safe_load(f) + ccl_cfg.setdefault("defaults", {})["buffer_kind"] = buffer_kind + ccl_cfg.setdefault("algorithms", {}).setdefault( + "intercube_allreduce", {}, + )["buffer_kind"] = buffer_kind + with open(ccl_path, "w") as f: + yaml.dump(ccl_cfg, f, default_flow_style=False) + + topo = resolve_topology(topo_path) + engine = GraphEngine(topo.topology_obj, enable_data=True) + spec = topo.topology_obj.spec + + with RuntimeContext( + engine=engine, + target_device=DeviceSelector("all"), + correlation_id=f"bk_sweep_{buffer_kind}_{n_elem}", + spec=spec, + ) as ctx: + result = run_allreduce( + ctx, engine, spec, + algorithm="intercube_allreduce", ccl_yaml=ccl_path, + ) + assert result["ok_cubes"] > 0 + + pe_exec_vals = [ + float(tr.get("pe_exec_ns", 0.0) or 0.0) + for _, (_, tr) in engine._results.items() + if isinstance(tr, dict) + ] + crit_ns = max(pe_exec_vals) if pe_exec_vals else 0.0 + + bytes_per_pe = n_elem * _ELEM_BYTES_F16 + record = { + "buffer_kind": buffer_kind, + "sip_topology": "torus_2d", + "n_sips": 6, + "n_elem": n_elem, + "bytes_per_pe": bytes_per_pe, + "latency_ns": crit_ns, + } + _ROWS_DIR.mkdir(parents=True, exist_ok=True) + row_path = _ROWS_DIR / f"{buffer_kind}_{n_elem}.json" + with open(row_path, "w", encoding="utf-8") as f: + json.dump(record, f) + + +def aggregate_buffer_kind_plot() -> bool: + """Read per-config rows and emit buffer_kind_sweep.png + CSV. + + Called from conftest.pytest_sessionfinish (controller-only). + Returns True if rows were aggregated. + """ + import csv + + if not _ROWS_DIR.exists(): + return False + row_files = sorted(_ROWS_DIR.glob("*.json")) + if not row_files: + return False + + records = [] + for p in row_files: + with open(p, encoding="utf-8") as f: + records.append(json.load(f)) + + import matplotlib.pyplot as plt + from matplotlib.ticker import FuncFormatter + + def _fmt_bytes(x, _pos): + if x <= 0: + return "0" + if x >= 1024 * 1024: + return f"{x / (1024 * 1024):.0f} MB" + if x >= 1024: + return f"{x / 1024:.0f} KB" + return f"{x:.0f} B" + + _bytes_fmt = FuncFormatter(_fmt_bytes) + + _OUT_DIR.mkdir(parents=True, exist_ok=True) + with open(_OUT_DIR / "buffer_kind_sweep.csv", "w", + newline="", encoding="utf-8") as f: + w = csv.DictWriter(f, fieldnames=[ + "buffer_kind", "sip_topology", "n_sips", "n_elem", + "bytes_per_pe", "latency_ns", + ]) + w.writeheader() + for r in sorted(records, key=lambda r: ( + r["buffer_kind"], r["bytes_per_pe"], + )): + w.writerow(r) + + colors = {"tcm": "tab:blue", "sram": "tab:orange", "hbm": "tab:red"} + fig, ax = plt.subplots(figsize=(10, 6)) + for bk in ["tcm", "sram", "hbm"]: + rs = sorted( + [r for r in records if r["buffer_kind"] == bk], + key=lambda r: r["bytes_per_pe"], + ) + if not rs: + continue + ax.plot( + [r["bytes_per_pe"] for r in rs], + [r["latency_ns"] for r in rs], + marker="o", lw=2.0, + color=colors[bk], label=f"buffer_kind = {bk}", + ) + ax.set_xscale("log", base=2) + ax.set_xlabel("Bytes per PE (log scale)") + ax.set_ylabel("Time (ns)") + ax.set_title( + "Allreduce torus_2d (6 SIPs, 3×2) — IPCQ slot memory tier" + ) + ax.grid(True, alpha=0.3) + ax.legend() + ax.xaxis.set_major_formatter(_bytes_fmt) + fig.tight_layout() + fig.savefig(_OUT_DIR / "buffer_kind_sweep.png", dpi=130) + plt.close(fig) + + for p in row_files: + try: + p.unlink() + except OSError: + pass + try: + _ROWS_DIR.rmdir() + except OSError: + pass + + print(f"\nWrote {_OUT_DIR / 'buffer_kind_sweep.png'} " + f"from {len(records)} rows") + return True diff --git a/tests/test_ipcq_buffer_kind_latency.py b/tests/test_ipcq_buffer_kind_latency.py new file mode 100644 index 0000000..b2aeaa0 --- /dev/null +++ b/tests/test_ipcq_buffer_kind_latency.py @@ -0,0 +1,219 @@ +"""Phase 1 micro-tests for IPCQ slot-memory latency model. + +These tests assert the TARGET behavior expected after Phase 2 wires +``buffer_kind`` (tcm/sram/hbm) into the IPCQ slot read/write latency +charges. They are written BEFORE the production change and are +EXPECTED TO FAIL today. + +Failure semantics today: + - Slot access is latency-free, so the tcm/sram/hbm runs produce + identical pe_exec_ns. The ordering assertion therefore fails with + "tcm == sram == hbm" — proving the test harness is wired and that + Phase 2 production work is what makes them pass. + +Reference (Phase 2 will edit these): + - src/kernbench/components/builtin/pe_dma.py — _handle_ipcq_inbound + - src/kernbench/components/builtin/pe_ipcq.py — _handle_recv, + _BUFFER_KIND_BW table + - src/kernbench/runtime_api/kernel.py — IpcqDmaToken adds + buffer_kind field + - ccl.yaml — algorithm.buffer_kind + +The tests reuse the existing config-driven allreduce app +(``run_allreduce`` in tests/test_allreduce_multidevice.py) with a 2-SIP +ring topology and a SMALL n_elem so they finish fast (~3-5 s each). +""" +from __future__ import annotations + +from pathlib import Path +from typing import Any + +import pytest + +from kernbench.runtime_api.context import RuntimeContext +from kernbench.runtime_api.types import DeviceSelector +from kernbench.sim_engine.engine import GraphEngine +from kernbench.topology.builder import resolve_topology + +# Reuse the test app's helpers so this micro-test file does not +# duplicate the run-allreduce + write-temp-configs plumbing. +from tests.test_allreduce_multidevice import ( + _write_temp_configs, + run_allreduce, +) + + +# Expected per-tier BW + overhead (Phase 2 will encode this in +# pe_ipcq.py). Mirrors topology.yaml component values. +_EXPECTED_BW = { + "tcm": (512.0, 0.0), + "sram": (128.0, 2.0), + "hbm": (32.0, 6.0), +} + + +def _expected_slot_io_ns(buffer_kind: str, nbytes: int) -> float: + """Per-access latency the model is expected to add (write OR read).""" + bw_gbs, overhead_ns = _EXPECTED_BW[buffer_kind] + # 1 GB/s = 1 byte/ns + return nbytes / bw_gbs + overhead_ns + + +def _run_torus_allreduce( + tmp_path: Path, *, buffer_kind: str, n_elem: int, +) -> float: + """Run one torus_2d 6-SIP allreduce and return critical-path + pe_exec_ns. The buffer_kind override is wired into ccl.yaml. + """ + sub = tmp_path / f"{buffer_kind}_{n_elem}" + sub.mkdir() + topo_path, ccl_path = _write_temp_configs( + sub, + sip_topology="torus_2d", + n_sips=6, + algorithm="intercube_allreduce", + sip_w=3, sip_h=2, + n_elem_override=n_elem, + ) + # Patch ccl.yaml in-place so the algorithm picks up buffer_kind. + import yaml + + with open(ccl_path) as f: + ccl_cfg = yaml.safe_load(f) + ccl_cfg.setdefault("defaults", {})["buffer_kind"] = buffer_kind + ccl_cfg.setdefault("algorithms", {}).setdefault( + "intercube_allreduce", {}, + )["buffer_kind"] = buffer_kind + with open(ccl_path, "w") as f: + yaml.dump(ccl_cfg, f, default_flow_style=False) + + topo = resolve_topology(topo_path) + engine = GraphEngine(topo.topology_obj, enable_data=True) + spec = topo.topology_obj.spec + + with RuntimeContext( + engine=engine, + target_device=DeviceSelector("all"), + correlation_id=f"bk_{buffer_kind}_{n_elem}", + spec=spec, + ) as ctx: + result = run_allreduce( + ctx, engine, spec, + algorithm="intercube_allreduce", ccl_yaml=ccl_path, + ) + assert result["ok_cubes"] > 0, "allreduce did not validate" + + pe_exec_vals = [ + float(tr.get("pe_exec_ns", 0.0) or 0.0) + for _, (_, tr) in engine._results.items() + if isinstance(tr, dict) + ] + return max(pe_exec_vals) if pe_exec_vals else 0.0 + + +# ── Phase 1 assertions ─────────────────────────────────────────────── + + +def test_slot_write_latency_orders_tcm_sram_hbm(tmp_path): + """tcm < sram < hbm at 8192 B per send. + + Pre-Phase-2: all three return the same pe_exec_ns and this + assertion fails. Post-Phase-2: the per-tier BW + overhead make + hbm visibly slower than sram, which is slower than tcm. + """ + n_elem = 4096 # 8192 B per slot + lat_tcm = _run_torus_allreduce(tmp_path, buffer_kind="tcm", n_elem=n_elem) + lat_sram = _run_torus_allreduce(tmp_path, buffer_kind="sram", n_elem=n_elem) + lat_hbm = _run_torus_allreduce(tmp_path, buffer_kind="hbm", n_elem=n_elem) + + # Expected per-access deltas (write+read = 2× the per-access value). + exp_tcm = 2 * _expected_slot_io_ns("tcm", n_elem * 2) + exp_sram = 2 * _expected_slot_io_ns("sram", n_elem * 2) + exp_hbm = 2 * _expected_slot_io_ns("hbm", n_elem * 2) + # Floor margin: 50% of the raw expected per-access delta — lets Phase 2 + # implementation choose to charge only one side without breaking the test, + # but still requires a clearly observable gap. + margin_sram_tcm = 0.5 * (exp_sram - exp_tcm) + margin_hbm_sram = 0.5 * (exp_hbm - exp_sram) + + assert lat_sram > lat_tcm + margin_sram_tcm, ( + f"sram should be slower than tcm by ≥ {margin_sram_tcm:.1f} ns " + f"per allreduce, got sram={lat_sram:.1f} tcm={lat_tcm:.1f} " + f"(delta={lat_sram - lat_tcm:.1f})" + ) + assert lat_hbm > lat_sram + margin_hbm_sram, ( + f"hbm should be slower than sram by ≥ {margin_hbm_sram:.1f} ns " + f"per allreduce, got hbm={lat_hbm:.1f} sram={lat_sram:.1f} " + f"(delta={lat_hbm - lat_sram:.1f})" + ) + + +def test_slot_io_scales_linearly_with_nbytes(tmp_path): + """For buffer_kind=hbm, doubling nbytes should add ~nbytes/32 ns + of latency to each slot access. Sanity-checks the slope. + + Pre-Phase-2: latency does not respond to nbytes via memory BW + (only via fabric drain), so the observed slope is dominated by + fabric BW and does NOT match 1/32 ns/B. + """ + lat_4k = _run_torus_allreduce(tmp_path, buffer_kind="hbm", n_elem=2048) + lat_8k = _run_torus_allreduce(tmp_path, buffer_kind="hbm", n_elem=4096) + + # Expected delta from doubling: at least one slot-IO event per cube + # in the critical path (very conservative). Per-access add = 4096/32 ≈ 128 + # ns on HBM going from 4k → 8k. Multiple slot accesses on the critical + # path should make the observed delta meaningfully larger. + expected_min_delta = 0.5 * (4096 / 32.0) # ≈ 64 ns + assert lat_8k - lat_4k > expected_min_delta, ( + f"doubling nbytes on hbm should add ≥ {expected_min_delta:.1f} ns " + f"of slot-IO latency, got delta={lat_8k - lat_4k:.1f} ns " + f"(lat_4k={lat_4k:.1f}, lat_8k={lat_8k:.1f})" + ) + + +def test_buffer_kind_sensitivity_grows_with_payload(tmp_path): + """Credit-return cost is fabric-only by design (16 B packet); only + the data slot-IO charge depends on ``buffer_kind``. Therefore the + tcm-vs-hbm gap must scale with payload size and be a small fraction + of the large-payload gap at small payloads. + + Concrete invariant the model must satisfy: + gap_small / gap_large < 0.10 + + Pre-Phase-2: gap_small == gap_large == 0 (division undefined → test + fails because gap_large is required > 0). Post-Phase-2: at small + nbytes the slot-IO charge is dominated by the constant + ``overhead_ns`` term, while at large nbytes it is dominated by the + ``nbytes / bw_gbs`` term — so gap_large grows linearly while + gap_small stays small. + """ + n_elem_small = 8 # 16 B per slot — overhead-bound + n_elem_large = 16384 # 32 KB per slot — bandwidth-bound + + lat_tcm_small = _run_torus_allreduce( + tmp_path, buffer_kind="tcm", n_elem=n_elem_small, + ) + lat_hbm_small = _run_torus_allreduce( + tmp_path, buffer_kind="hbm", n_elem=n_elem_small, + ) + lat_tcm_large = _run_torus_allreduce( + tmp_path, buffer_kind="tcm", n_elem=n_elem_large, + ) + lat_hbm_large = _run_torus_allreduce( + tmp_path, buffer_kind="hbm", n_elem=n_elem_large, + ) + + gap_small = abs(lat_hbm_small - lat_tcm_small) + gap_large = abs(lat_hbm_large - lat_tcm_large) + + assert gap_large > 1000.0, ( + f"large-payload buffer_kind gap must be observably large " + f"(this is the sweep's whole point). got gap_large={gap_large:.1f} ns " + f"(lat_tcm_large={lat_tcm_large:.1f}, lat_hbm_large={lat_hbm_large:.1f})" + ) + assert gap_small / gap_large < 0.10, ( + f"buffer_kind sensitivity should grow with payload — " + f"small-payload gap should be < 10% of large-payload gap. " + f"got gap_small={gap_small:.1f} ns, gap_large={gap_large:.1f} ns, " + f"ratio={gap_small / gap_large:.3f}" + )