"""Tests for wire cut-through via `Transaction.head_arrived` event (ADR-0033 D1). The wire model (ADR-0015 D2) currently delivers a message to the destination in_port only after the full nbytes/bw_gbs transfer time has elapsed (store-and-forward). Phase 2 adds a `head_arrived` SimPy event on the Transaction that fires at `prop_ns + FLIT_BYTES / bw_gbs` — letting opted-in destinations (e.g., HBM CTRL) start processing the leading flit before the tail arrives. The wire's BW occupancy (`available_at`) is unchanged. These tests assert the *behavioral* consequence: when both the wire and HBM CTRL contribute meaningfully to total latency, the model must not double-count their time. They are written BEFORE Phase 2 production changes and expected to FAIL on current code. """ from __future__ import annotations from pathlib import Path from kernbench.policy.address.phyaddr import PhysAddr from kernbench.runtime_api.kernel import MemoryWriteMsg from kernbench.sim_engine.engine import GraphEngine from kernbench.topology.builder import load_topology TOPOLOGY_PATH = Path(__file__).parent.parent / "topology.yaml" def _engine() -> GraphEngine: return GraphEngine(load_topology(TOPOLOGY_PATH)) def _hbm_pa(sip: int = 0, cube: int = 0, pe_id: int = 0) -> int: slice_bytes = 48 * (1 << 30) // 8 return PhysAddr.pe_hbm_addr( sip_id=sip, die_id=cube, pe_id=pe_id, pe_local_hbm_offset=0x1000, slice_size_bytes=slice_bytes, ).encode() def _path_drain_for_write(eng: GraphEngine, msg: MemoryWriteMsg) -> float: """Dynamically compute the engine's path drain for this write.""" pcie_ep_id = eng._resolver.find_pcie_ep(msg.dst_sip) pa = PhysAddr.decode(msg.dst_pa) hbm_node = eng._resolver.resolve(pa) path = eng._router.find_memory_path(pcie_ep_id, hbm_node) return eng._path_drain_ns(path, msg.nbytes) def _write_ns(nbytes: int) -> tuple[float, float]: """Return (total_ns, path_drain_ns) for the MemoryWrite of given nbytes.""" eng = _engine() msg = MemoryWriteMsg( correlation_id="cut-through", request_id=f"w-{nbytes}", dst_sip=0, dst_cube=0, dst_pe=0, dst_pa=_hbm_pa(), nbytes=nbytes, pattern="zero", target_pe=0, ) drain = _path_drain_for_write(eng, msg) h = eng.submit(msg) eng.wait(h) _, t = eng.get_completion(h) return t["total_ns"], drain # ── 1. Effective slope: total_ns vs nbytes should grow at the rate of # the bottleneck BW, not 2x that rate (which double-counts wire+HBM). def test_effective_slope_single_bw_not_doubled(): """The effective ns-per-byte slope should match the path bottleneck rate (= 1 / bottleneck_bw), NOT 2× that rate (which would double-count wire and HBM drain). Drain is computed dynamically from the engine path. Measurement: linear fit between two large transfer sizes. Constants cancel; slope is the discriminator. """ n1, n2 = 32768, 131072 # 32KB and 128KB t1, drain1 = _write_ns(n1) t2, drain2 = _write_ns(n2) slope = (t2 - t1) / (n2 - n1) # ns per byte expected_slope = drain2 / n2 # = 1 / bottleneck_bw (ns/byte) # 50% tolerance above ideal accounts for propagation prop_ns at # large-N regimes; still well below 2× (doubled) doubling. assert slope < expected_slope * 1.5, ( f"Effective slope {slope*1000:.4f} ps/byte too steep; " f"expected ~{expected_slope*1000:.4f} ps/byte at path bottleneck. " f"A doubled (wire + HBM drain) model would give ~" f"{expected_slope*2*1000:.4f} ps/byte." ) # ── 2. Absolute upper bound: 1MB transfer not 2x wire time ── def test_1mb_transfer_upper_bound(): """A 1MB write should complete in roughly the path-bottleneck transfer time, plus modest fixed overhead. A doubled (wire + HBM drain) model would give ~2× that. """ nbytes = 1 << 20 # 1 MB total, drain = _write_ns(nbytes) assert total < drain * 1.5, ( f"1MB write should not be ~2x bottleneck transfer time. " f"drain={drain:.2f}ns, total={total:.2f}ns, " f"ratio={total/drain:.2f} (expected < 1.5)" ) # ── 3. Small transfer: cut-through dominated by component overhead ── def test_small_transfer_remains_finite_and_positive(): """Sanity: small (single-chunk) transfer still completes with positive finite latency. Cut-through should not introduce zero-latency bugs. """ t, _ = _write_ns(256) assert t > 0 assert t < 1000.0, f"256B write should be << 1us, got {t}ns" # ── 4. Monotonicity preserved under cut-through ── def test_monotonicity_at_extreme_sizes(): """Once payload is large enough to be wire-dominated, monotonicity must hold: a much larger write takes more time than a smaller one. Note: in the PC parallelism regime (ADR-0033 D1), a small single-PC transfer can actually be slower than a small few-PC transfer (a 1KB write spans 4 PCs in parallel and finishes around the same wall-clock time as a 256B write that only loads 1 PC). This is physically correct and matches real-HW behavior; strict monotonicity over the sub-PC regime is not asserted. We assert it only across an extreme range where the wire-transfer term dominates. """ small, _ = _write_ns(256) large, _ = _write_ns(65536) assert large > small, ( f"65KB ({large:.2f}ns) must exceed 256B ({small:.2f}ns) — " f"wire transfer at 256GB/s alone is 256ns for 64KB, so total " f"must dominate any sub-microsecond small-transfer time." )