From fd56b6cacd8c5475eb2492db5506b7457b1a3548 Mon Sep 17 00:00:00 2001 From: Mukesh Garg Date: Thu, 21 May 2026 10:26:25 -0700 Subject: [PATCH] adr: add ADR-0043/0044 (eval harnesses); reconcile ADR-0024/0032 for SIP w/h MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document the allreduce + GEMM evaluation harnesses and bring the affected allreduce ADRs in line with the refactored code. New (Accepted, EN + KO): - ADR-0043 — allreduce evaluation harness (tests/sccl/): distributed-driven correctness, latency/buffer-kind sweeps, sessionfinish plot aggregators, topology + FSIM-comparison figures. Verified against the implementation. - ADR-0044 — GEMM evaluation harness (scripts/gemm_sweep.py + tests/gemm/): heavy-script data gen vs. fast test-rendered figures, slow regenerator, the 3-figure set. Records two limitations as open questions: the theoretical-model constants are inherited (not yet traced to ADR-0033/ 0014), and the *_measured figure is a naming misnomer. Updated (EN + KO): - ADR-0024 — add D5: SIP grid w/h resolution (explicit sips.w/h, square fallback, fail-loud), documenting the AhbmCCLBackend fix. - ADR-0032 — D4/D5/Non-goals reconciled: rectangular SIP grids (e.g. 6 SIPs as 3x2) are supported via explicit w/h; the square requirement now applies only to the fallback. Affected-files repointed to tests/sccl/. Verification: ADR-0023 and ADR-0042 confirmed still matching the code (no change). verify_adr_lang_pairs.py passes (EN/KO Status blocks byte-equal). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/adr-ko/ADR-0024-par-sip-tp-launcher.md | 30 ++++ .../ADR-0032-algo-intercube-allreduce.md | 28 ++-- .../adr-ko/ADR-0043-eval-allreduce-harness.md | 126 +++++++++++++++++ docs/adr-ko/ADR-0044-eval-gemm-harness.md | 127 +++++++++++++++++ docs/adr/ADR-0024-par-sip-tp-launcher.md | 31 +++++ docs/adr/ADR-0032-algo-intercube-allreduce.md | 27 ++-- docs/adr/ADR-0043-eval-allreduce-harness.md | 130 ++++++++++++++++++ docs/adr/ADR-0044-eval-gemm-harness.md | 130 ++++++++++++++++++ 8 files changed, 606 insertions(+), 23 deletions(-) create mode 100644 docs/adr-ko/ADR-0043-eval-allreduce-harness.md create mode 100644 docs/adr-ko/ADR-0044-eval-gemm-harness.md create mode 100644 docs/adr/ADR-0043-eval-allreduce-harness.md create mode 100644 docs/adr/ADR-0044-eval-gemm-harness.md diff --git a/docs/adr-ko/ADR-0024-par-sip-tp-launcher.md b/docs/adr-ko/ADR-0024-par-sip-tp-launcher.md index b321e84..56fa705 100644 --- a/docs/adr-ko/ADR-0024-par-sip-tp-launcher.md +++ b/docs/adr-ko/ADR-0024-par-sip-tp-launcher.md @@ -168,6 +168,36 @@ placement = resolve_dp_policy( Post-hoc `pe_index` shifting 없음 — ShardSpec이 `(sip, cube, pe)` 구조적 좌표를 직접 보유. ShardSpec 상세는 ADR-0026. +### D5. SIP 그리드 크기 — 명시적 `sips.w/h` 해석 + +2D inter-SIP topology (`torus_2d`, `mesh_2d_no_wrap`)의 SIP 그리드 형태 +(width × height)는 `system.sips.w` / `system.sips.h`에서 해석한다. D1이 +`sips.count`로 `world_size`를 해석하는 것과 같은 방식이다. 우선순위: +명시적 `w/h` (`w*h == count` 검증) > 정사각 fallback +(`w/h` 미지정 시에만 `round(sqrt(count))²`) > error. + +```python +sips = spec.get("system", {}).get("sips", {}) +if sip_topo == "ring_1d": + w, h = 0, 0 # 1D sentinel (no grid) +elif sips.get("w") is not None and sips.get("h") is not None: + w, h = int(sips["w"]), int(sips["h"]) + if w * h != n_sips: + raise ValueError(f"sip layout {w}x{h} != sips.count ({n_sips})") +else: + side = int(round(math.sqrt(n_sips))) + if side * side != n_sips: + raise ValueError("non-square sips.count requires explicit sips.w/h") + w, h = side, side +``` + +이로써 2D SIP 그리드가 완전 정사각이어야 한다는 기존 가정을 제거한다: +6-SIP `torus_2d` / `mesh_2d_no_wrap`은 이제 `w: 3, h: 2`(또는 `2x3`)로 +표현 가능하다. 도출된 `(w, h)`는 알고리즘의 inter-SIP exchange로 전달된다 +(ADR-0032 D5에서 소비). 이전 코드 경로는 ring이 아닌 모든 topology에서 +`round(sqrt(count))²`를 조용히 취해 잘못된 그리드(예: 6 SIP에 2×2)를 +만들었다. fail-loud fallback을 갖춘 명시적 `w/h` 경로가 이를 대체한다. + --- ## Dependencies diff --git a/docs/adr-ko/ADR-0032-algo-intercube-allreduce.md b/docs/adr-ko/ADR-0032-algo-intercube-allreduce.md index c5e5689..9cb8809 100644 --- a/docs/adr-ko/ADR-0032-algo-intercube-allreduce.md +++ b/docs/adr-ko/ADR-0032-algo-intercube-allreduce.md @@ -135,21 +135,24 @@ system: ``` - `ring_1d`: n_sips-1 라운드의 `send global_E / recv global_W`. -- `torus_2d`: sqrt(n_sips)×sqrt(n_sips) 랩핑 메시. `global_E/W`에서 - row ring, 이어서 `global_S/N`에서 col ring. -- `mesh_2d_no_wrap`: 랩어라운드 없는 정사각형 메시. 차원별 chain +- `torus_2d`: `w × h` 랩핑 메시. `global_E/W`에서 row ring, 이어서 + `global_S/N`에서 col ring. +- `mesh_2d_no_wrap`: 랩어라운드 없는 `w × h` 메시. 차원별 chain reduce + 브로드캐스트. -2D 변형은 `n_sips`가 완전 제곱수여야 한다. +2D 그리드 크기 `(w, h)`는 `system.sips.w/h`에서 온다 (ADR-0024 D5). +정사각 fallback (`round(sqrt(n_sips))²`)은 `w/h`가 생략된 경우에만 +적용되므로, 직사각형 그리드(예: 6 SIP을 `3×2`로)는 명시적 `w/h`로 +지원된다. ### D5. 프로세스-그룹 통합 — `AhbmCCLBackend` `init_process_group` 시점에 백엔드는: 1. `ccl.yaml` + `topology.yaml`을 로드한다. -2. 알고리즘 모듈의 `TOPO_NAME_TO_KIND`를 사용하여 - `system.sips.topology`로부터 `sip_topo_kind, sip_topo_w, sip_topo_h`를 - 도출한다. +2. `system.sips.topology`로부터 알고리즘 모듈의 `TOPO_NAME_TO_KIND`를 + 통해 `sip_topo_kind`를 도출하고, `sip_topo_w, sip_topo_h`는 + `system.sips.w/h`에서 정사각 fallback과 함께 도출한다 (ADR-0024 D5). 3. `configure_sfr_intercube_multisip(engine, spec, cfg)`를 호출한다 — 일회성 SFR 와이어링, NCCL 커뮤니케이터 생성을 모방한다. @@ -221,8 +224,10 @@ sip: - **PE별 allreduce** (큐브 내 PE-PE reduce). 범위 밖 — 본 알고리즘의 워크로드는 큐브당 DP이다. -- **비대칭 SIP 토폴로지** (정사각형이 아닌 메시/토러스). - `torus_2d`와 `mesh_2d_no_wrap`은 `n_sips = k²`를 요구한다. +- **정사각 그리드 fallback은 `n_sips = k²`를 요구**: 직사각형 SIP + 그리드(정사각형이 아닌 메시/토러스)는 지원되지만, `system.sips.w/h`를 + 명시적으로 줄 때만 가능하다 (ADR-0024 D5). `w/h` 생략 시 2D 토폴로지는 + 정사각 그리드로 fallback하며 여전히 `n_sips = k²`를 요구한다. - **파이프라인 청크**: 큐브당 단일 타일, 아직 파이프라이닝 없음. - **루트 큐브의 런타임 선출**: 커널은 현재 SIP 내부 임계 경로를 최소화하기 위해 기하학적 중심인 @@ -269,7 +274,6 @@ sip: | `ccl.yaml` | 단일 `lrab_hierarchical_allreduce` 항목 | | `topology.yaml` | `system.sips.topology` 추가 | | `benches/ccl_allreduce.py` | Row-wise 큐브-메시 텐서 레이아웃 | -| `tests/test_allreduce_multidevice.py` (신규) | 구성 기반 ring/torus/mesh | -| `tests/test_distributed_lrab_hierarchical_allreduce.py` (신규) | 전체 `dist.all_reduce` 경로 | -| `tests/test_intercube_sfr_config.py` (신규) | SFR 와이어링 검증 | +| `tests/sccl/` (테스트 패키지) | 구성 기반 ring/torus/mesh 정확성 + 전체 `dist.all_reduce` 경로 + latency/buffer-kind 스윕 (평가 하니스 — ADR-0043) | +| `tests/test_intercube_sfr_config.py` | SFR 와이어링 검증 | | 제거 | `ring_allreduce.py`, `mesh_allreduce.py`, `tree_allreduce.py`, `hierarchical_allreduce.py`, `hello_send.py`, `testing.py` 및 그 테스트 | diff --git a/docs/adr-ko/ADR-0043-eval-allreduce-harness.md b/docs/adr-ko/ADR-0043-eval-allreduce-harness.md new file mode 100644 index 0000000..e0722ac --- /dev/null +++ b/docs/adr-ko/ADR-0043-eval-allreduce-harness.md @@ -0,0 +1,126 @@ +# ADR-0043: Allreduce 평가 하니스 — `tests/sccl/` + +## Status + +Accepted + +`tests/sccl/` 평가 하니스를 문서화한다; 구현과 대조 검증 완료 +(상수, 파일 집합, 스윕 차원을 교차 확인). + +## Context + +ADR-0032는 intercube all-reduce *알고리즘*을 정의하고, ADR-0023/0024/0027은 +IPCQ 백엔드, rank=SIP launcher, `mp.spawn`을 정의한다. 그러나 어느 것도 +**allreduce를 어떻게 구동하고 특성화하는가** — 정확성 테스트, latency/ +buffer-kind 스윕, 파생 플롯 — 는 기술하지 않는다. ADR-0013(verification +strategy)이 일반 정책이라면, 본 ADR은 구체적 allreduce 하니스를 고정하여 +작업의 "평가" 절반이 구현과 함께 문서화되도록 한다. + +하니스는 `tests/sccl/`(allreduce 테스트 통합 시 생성된 패키지)에 위치한다. +이전의 평면적 `tests/test_allreduce_multidevice.py` + +`tests/test_distributed_*` 레이아웃을 대체한다. + +## Decision + +### D1. 평가를 공개 `torch.distributed` 경로로 구동 + +정확성과 스윕은 collective를 실제 DDP 형태 경로 — +`init_process_group(backend="ahbm") → mp.spawn → dist.all_reduce` +(ADR-0024/0027) — 로 실행하며, 하위 레벨 `ctx.launch`를 쓰지 않는다. +`tests/sccl/_allreduce_helpers.py`의 공유 헬퍼 +`_run_distributed(tmp_path, monkeypatch, topo_path, corr_id, n_elem)`가 +엔진을 빌드하고 워커를 실행하고 `(engine, n_cubes)`를 반환한다. +`monkeypatch.chdir`이 백엔드의 `load_ccl_config()`(cwd 조회)를 케이스별 +임시 `ccl.yaml`로 향하게 한다. + +직접 launch 레퍼런스(`run_allreduce`)는 같은 헬퍼 모듈에 유지된다 — +distributed 테스트용이 아니라, `tests/`의 IPCQ buffer-kind / root-center +마이크로 테스트가 import하기 때문이다. + +### D2. 평가 관심사별 파일 하나 + +| 파일 | 관심사 | `torch.distributed`? | +|---|---|---| +| `test_allreduce_ring_torus_mesh.py` | ring_1d / torus_2d (2×3) / mesh_2d_no_wrap (2×3) 정확성 | yes | +| `test_distributed_default_topology.py` | `topology.yaml` 그대로의 전체 경로 | yes | +| `test_plot_latency_sweep.py` | latency 스윕 행 (n_elem × topology) | yes | +| `test_plot_buffer_kind_sweep.py` | TCM/SRAM/HBM 스윕 행 | yes | +| `test_plot_topology_diagram.py` | topology.png (순수 matplotlib) | no | +| `test_plot_comparison_fsim.py` | broken-axis 모델 vs FSIM 비교 | no | +| `test_intercube_root_center.py` | ADR-0032 center-root latency 가드 (직접 경로) | no | + +`_allreduce_helpers.py`는 공유 plumbing(드라이버, config writer, 스윕/ +buffer-kind 상수, 플롯 aggregator, topology-diagram + FSIM 비교 emitter)을 +보유한다. 수집되지 않는다(`test_` 접두사 없음). + +### D3. Latency 메트릭 — critical-path `pe_exec_ns` + +config별 보고 latency는 `engine._results`에 대한 +`crit_ns = max(pe_exec_ns)` — 가장 느린 rank의 PE 실행 시간 — 이다. +모든 latency 차트에 그려지고 `summary.csv`에 기록되는 값이다. + +### D4. 스윕 차원 + +- **Latency 스윕**: `n_elem ∈ {8, 32, 64, 128, 512, 1024, 2048, 4096, + 8192, 16384, 32768, 49152}` (16 제외 — `n_cubes`와 충돌) × topology ∈ + {ring_1d (6), torus_2d 2×3 (6), mesh_2d_no_wrap 2×3 (6)}. +- **Buffer-kind 스윕**: `buffer_kind ∈ {tcm, sram, hbm}` × 더 작은 + `n_elem` 그리드, torus_2d 6-SIP (3×2)에서. buffer_kind는 임시 + `ccl.yaml`에 설정되며(백엔드가 `init_process_group` 시점에 읽음, + ADR-0023 D6) 적용된다. + +2×3 / 3×2 그리드는 명시적 `w/h` SIP 해석(ADR-0024 D5)을 행사한다. + +### D5. `pytest_sessionfinish` aggregator를 통한 파생 플롯 + +스윕 테스트는 xdist 친화적이다: 각 parametrized 케이스가 staging 디렉터리에 +JSON 행 하나를 쓴다. conftest `pytest_sessionfinish` 훅(controller 노드 +전용)이 `_allreduce_helpers.py`의 aggregator를 호출한다: + +- `_aggregate_sweep_plots()` → topology별 PNG + `summary.csv` +- `aggregate_buffer_kind_plot()` → TCM/SRAM/HBM 비교 PNG + csv + +topology-diagram 및 FSIM-비교 figure는 각자의 `test_plot_*` 테스트가 +직접 emit한다(행 staging 없음 — 각각 `topology.yaml`과 `summary.csv`의 +순수 함수). 모든 출력은 `docs/diagrams/allreduce_latency_plots/`에 떨어지며 +CLAUDE.md에 따라 **파생 아티팩트**다(ADR과 일관, Phase-2 게이트 없음). + +### D6. FSIM 비교 레퍼런스는 하드코딩 상수 + +`emit_comparison_fsim_plot()`은 모델 곡선을 외부 FSIM single-device +레퍼런스(`366 µs`) 하나와 겹쳐 그리며, 이는 리터럴로 보유된다 — 외부 데이터 +파일 없음. "measured" 시리즈는 시뮬레이터(`op_log` GEMM 카운트, +`composite_window_ns`)에서, "theoretical" 시리즈는 손으로 도출한 해석적 +모델(ADR-0044 D5가 ADR-미검증으로 표시한 동일 모델)에서 온다. + +## Consequences + +### Positive + +- allreduce가 실제 DDP 스크립트와 같은 API로 평가되므로, 하니스가 + ADR-0024/0027의 통합 테스트 역할도 겸한다. +- figure는 매 `pytest` 실행마다 committed 데이터로 재생성된다; 수동 플롯 + 단계 없음. +- 직사각형 그리드 스윕이 ADR-0024 D5 `w/h` 수정을 드러낸 회귀 커버리지를 + 제공했다. + +### Negative / limitations + +- 전체 latency 스윕은 기본 `pytest`에서 실행된다(~분 단위); `slow`로 + 표시되지 않는다. (ADR-0044는 GEMM 스윕을 `slow`로 표시하는 것과 대조.) +- `test_intercube_root_center.py`는 latency *임계값* assertion(ADR-0032 + center-root 가드)을 보유한다 — 스위트에서 유일한 절대-latency + assertion이며 latency 모델 변경(ADR-0033)에 민감하다. + +## Dependencies + +- **ADR-0013**: verification strategy (본 ADR이 특수화하는 일반 정책). +- **ADR-0023 / ADR-0024 / ADR-0027**: IPCQ 백엔드, rank=SIP launcher, + `mp.spawn` — D1이 구동하는 경로. +- **ADR-0032**: 평가 대상 알고리즘; D4 그리드가 그 topology 분기를 행사. +- **ADR-0044**: 형제 격인 GEMM 평가 하니스. + +## Open questions + +- GEMM 스윕과의 일관성을 위해 latency 스윕을 `slow`로 표시할 것인가? +- FSIM 레퍼런스를 하드코딩 상수에서 버전 관리되는 데이터 파일로 옮길 것인가? diff --git a/docs/adr-ko/ADR-0044-eval-gemm-harness.md b/docs/adr-ko/ADR-0044-eval-gemm-harness.md new file mode 100644 index 0000000..db21a47 --- /dev/null +++ b/docs/adr-ko/ADR-0044-eval-gemm-harness.md @@ -0,0 +1,127 @@ +# ADR-0044: GEMM 평가 하니스 — `scripts/gemm_sweep.py` + `tests/gemm/` + +## Status + +Accepted + +GEMM 평가/특성화 하니스를 문서화한다; 구현과 대조 검증 완료 +(상수, tile 크기, figure 집합, script↔test 분할을 교차 확인). D5/D6 +caveat은 부정확이 아니라 기록된 한계다. + +## Context + +ADR-0014(PE pipeline)와 ADR-0042(tile-plan generator)는 GEMM *구현*을 +정의하고, ADR-0033은 latency 모델을 정의한다. 그러나 어느 것도 **GEMM +성능을 어떻게 스윕하고 특성화하는가** — 타이밍 데이터를 만드는 shape/variant +스윕과 이를 해석하는 figure — 는 기술하지 않는다. 본 ADR이 그 하니스를 +고정한다. + +allreduce 하니스(ADR-0043)와 달리 GEMM 스윕은 **무겁다**(24 sim 실행: +8 shape × 3 operand-staging variant; `512` shape 하나가 2048 tile). 이 +무게가 아래 분할을 결정한다. + +## Decision + +### D1. 두 계층 분할 — 무거운 데이터 생성(script) vs. 빠른 figure(test) + +- **데이터 생성은 수동 script로 유지**: `scripts/gemm_sweep.py`가 + `matmul-composite`(ADR-0042 plan)를 CLI와 동일한 `run_bench` 경로로 + shape × variant에 걸쳐 실행하고, `result.engine.op_log`를 수확하여 + `docs/diagrams/gemm_sweep.json`(stage별/engine별 wall-clock + occupancy + + record count + pe/composite window)을 쓴다. +- **figure 렌더링은 test 생성**: `tests/gemm/`이 committed `gemm_sweep.json`을 + 읽어 matplotlib PNG를 `docs/diagrams/gemm_plots/`에 렌더링한다. 이 + 테스트는 빠르고 기본 실행된다. + +근거: 슬라이드덱 규모의 sim 스윕은 매 `pytest` 실행에 속하지 않지만, +figure(저렴·결정적)는 자유롭게 재생성되고 CI로 가드되어야 한다. 이는 +CLAUDE.md의 script-vs-test 분할(무거운/수동 생성은 script; 빠른 assertion은 +test)을 반영한다. + +### D2. Slow regenerator 테스트가 script를 감싼다 + +`tests/gemm/test_gemm_sweep.py`는 `@pytest.mark.slow`로 표시된다(기본 +`addopts: -m "not slow"`에서 제외). 이는 `scripts/gemm_sweep.py`를 +subprocess로 호출하여 `gemm_sweep.json`을 on-demand로 재생성한다 +(`pytest -m slow tests/gemm/test_gemm_sweep.py`). 스윕 로직은 단일 +home(script)을 가지며 테스트는 이를 감싸기만 하므로 sim 구동 코드의 +중복이 없다. + +### D3. Figure 집합 (3개 차트, `load_ref` variant) + +| 테스트 | PNG | 내용 | +|---|---|---| +| `test_plot_gemm_stage_breakdown.py` | `gemm_stage_breakdown.png` | stage별 engine wall-clock (DMA in / Fetch / GEMM / DMA out) | +| `test_plot_gemm_mac_utilization.py` | `gemm_mac_utilization_measured.png` | GEMM util % + useful eff % | +| `test_plot_gemm_mac_utilization.py` | `gemm_mac_utilization_theoretical_vs_measured.png` | theoretical vs 시뮬레이터-measured util/eff | + +`tests/gemm/_gemm_plot_helpers.py`가 공유 renderer를 보유한다(시리즈 로직은 +`scripts/build_overview_slides.py`의 GEMM `_render_*` 함수를 미러링하며, +그쪽은 여전히 PPTX에 네이티브로 그린다). 수집되지 않음(`test_` 접두사 +없음). 각 `test_plot_*`는 `gemm_sweep.json`이 없으면 skip한다. + +### D4. Tile 크기는 데이터 기반; under-tile shape는 표시 + +Tile 크기는 `gemm_sweep.json`(`tile_sizes`)에서 읽으며, 이는 스윕이 +`PeSchedulerComponent.TILE_M/K/N = 32/64/32` — 권위 소스 — 에서 기록한 +값이다. `M square fallback +(`round(sqrt(count))²`, used only when no `w/h` is given) > error. + +```python +sips = spec.get("system", {}).get("sips", {}) +if sip_topo == "ring_1d": + w, h = 0, 0 # 1D sentinel (no grid) +elif sips.get("w") is not None and sips.get("h") is not None: + w, h = int(sips["w"]), int(sips["h"]) + if w * h != n_sips: + raise ValueError(f"sip layout {w}x{h} != sips.count ({n_sips})") +else: + side = int(round(math.sqrt(n_sips))) + if side * side != n_sips: + raise ValueError("non-square sips.count requires explicit sips.w/h") + w, h = side, side +``` + +This lifts the earlier assumption that 2D SIP grids must be perfect +squares: a 6-SIP `torus_2d` / `mesh_2d_no_wrap` is now expressible as +`w: 3, h: 2` (or `2x3`). The derived `(w, h)` feed the algorithm's +inter-SIP exchange (consumed in ADR-0032 D5). The prior code path silently +took `round(sqrt(count))²` for any non-ring topology, which produced a +wrong grid (e.g. 2×2 for 6 SIPs); the explicit-`w/h` path with a +fail-loud fallback replaces that. + --- ## Dependencies diff --git a/docs/adr/ADR-0032-algo-intercube-allreduce.md b/docs/adr/ADR-0032-algo-intercube-allreduce.md index 3171cd7..3fcd450 100644 --- a/docs/adr/ADR-0032-algo-intercube-allreduce.md +++ b/docs/adr/ADR-0032-algo-intercube-allreduce.md @@ -138,20 +138,24 @@ system: ``` - `ring_1d`: n_sips-1 rounds of `send global_E / recv global_W`. -- `torus_2d`: sqrt(n_sips)×sqrt(n_sips) wrapping mesh. Row ring on - `global_E/W` then col ring on `global_S/N`. -- `mesh_2d_no_wrap`: square mesh without wrap-around. Chain reduce + +- `torus_2d`: `w × h` wrapping mesh. Row ring on `global_E/W` then col + ring on `global_S/N`. +- `mesh_2d_no_wrap`: `w × h` mesh without wrap-around. Chain reduce + broadcast per dimension. -2D variants require `n_sips` to be a perfect square. +2D grid dims `(w, h)` come from `system.sips.w/h` (ADR-0024 D5). A square +fallback (`round(sqrt(n_sips))²`) applies **only** when `w/h` are omitted, +so rectangular grids (e.g. 6 SIPs as `3×2`) are supported by giving +explicit `w/h`. ### D5. Process-group integration — `AhbmCCLBackend` At `init_process_group` time the backend: 1. Loads `ccl.yaml` + `topology.yaml`. -2. Derives `sip_topo_kind, sip_topo_w, sip_topo_h` from - `system.sips.topology` using the algorithm module's `TOPO_NAME_TO_KIND`. +2. Derives `sip_topo_kind` from `system.sips.topology` via the algorithm + module's `TOPO_NAME_TO_KIND`, and `sip_topo_w, sip_topo_h` from + `system.sips.w/h` with a square-only fallback (ADR-0024 D5). 3. Calls `configure_sfr_intercube_multisip(engine, spec, cfg)` — one-time SFR wiring, mirrors NCCL communicator creation. @@ -222,8 +226,10 @@ Modules loaded via `cfg["module"]` must export: - **Per-PE allreduce** (intra-cube PE-to-PE reduce). Out of scope — the workload for this algorithm is per-cube DP. -- **Asymmetric SIP topologies** (non-square mesh/torus). `torus_2d` and - `mesh_2d_no_wrap` require `n_sips = k²`. +- **Square-grid fallback requires `n_sips = k²`**: rectangular SIP grids + (non-square mesh/torus) are supported, but only when `system.sips.w/h` + are given explicitly (ADR-0024 D5). With `w/h` omitted, 2D topologies + fall back to a square grid and still require `n_sips = k²`. - **Pipelined chunks**: single-tile per cube, no pipelining yet. - **Root cube runtime election**: the kernel currently uses `root_cube = (mesh_h // 2) * mesh_w + (mesh_w // 2)` — the geometric @@ -270,7 +276,6 @@ Modules loaded via `cfg["module"]` must export: | `ccl.yaml` | Single `lrab_hierarchical_allreduce` entry | | `topology.yaml` | Added `system.sips.topology` | | `benches/ccl_allreduce.py` | Row-wise cube-mesh tensor layout | -| `tests/test_allreduce_multidevice.py` (new) | Config-driven ring/torus/mesh | -| `tests/test_distributed_lrab_hierarchical_allreduce.py` (new) | Full `dist.all_reduce` path | -| `tests/test_intercube_sfr_config.py` (new) | SFR wiring verification | +| `tests/sccl/` (test package) | Config-driven ring/torus/mesh correctness + full `dist.all_reduce` path + latency/buffer-kind sweeps (evaluation harness — ADR-0043) | +| `tests/test_intercube_sfr_config.py` | SFR wiring verification | | Removed | `ring_allreduce.py`, `mesh_allreduce.py`, `tree_allreduce.py`, `hierarchical_allreduce.py`, `hello_send.py`, `testing.py` and their tests | diff --git a/docs/adr/ADR-0043-eval-allreduce-harness.md b/docs/adr/ADR-0043-eval-allreduce-harness.md new file mode 100644 index 0000000..5ab0ebb --- /dev/null +++ b/docs/adr/ADR-0043-eval-allreduce-harness.md @@ -0,0 +1,130 @@ +# ADR-0043: Allreduce Evaluation Harness — `tests/sccl/` + +## Status + +Accepted + +Documents the `tests/sccl/` evaluation harness; verified against the +implementation (constants, file set, and sweep dimensions cross-checked). + +## Context + +ADR-0032 defines the intercube all-reduce *algorithm*; ADR-0023/0024/0027 +define the IPCQ backend, the rank=SIP launcher, and `mp.spawn`. None of +them describe **how the allreduce is exercised and characterized** — the +correctness tests, the latency/buffer-kind sweeps, and the derived plots. +ADR-0013 (verification strategy) is the general policy; this ADR pins the +concrete allreduce harness so the "evaluation" half of the work is +documented, not just the implementation. + +The harness lives under `tests/sccl/` (the package created when the +allreduce tests were consolidated). It supersedes the earlier flat +`tests/test_allreduce_multidevice.py` + `tests/test_distributed_*` layout. + +## Decision + +### D1. Drive evaluation through the public `torch.distributed` path + +Correctness and the sweeps run the collective through the real DDP-shaped +path — `init_process_group(backend="ahbm") → mp.spawn → dist.all_reduce` +(ADR-0024/0027) — not the lower-level `ctx.launch`. A shared helper +`_run_distributed(tmp_path, monkeypatch, topo_path, corr_id, n_elem)` in +`tests/sccl/_allreduce_helpers.py` builds the engine, runs the workers, and +returns `(engine, n_cubes)`. `monkeypatch.chdir` points the backend's +`load_ccl_config()` (cwd lookup) at a per-case temp `ccl.yaml`. + +A direct-launch reference (`run_allreduce`) is retained in the same helper +module — not for the distributed tests, but because the IPCQ buffer-kind / +root-center micro-tests under `tests/` import it. + +### D2. One file per evaluation concern + +| File | Concern | `torch.distributed`? | +|---|---|---| +| `test_allreduce_ring_torus_mesh.py` | correctness across ring_1d / torus_2d (2×3) / mesh_2d_no_wrap (2×3) | yes | +| `test_distributed_default_topology.py` | full path on `topology.yaml` as-is | yes | +| `test_plot_latency_sweep.py` | latency sweep rows (n_elem × topology) | yes | +| `test_plot_buffer_kind_sweep.py` | TCM/SRAM/HBM sweep rows | yes | +| `test_plot_topology_diagram.py` | topology.png (pure matplotlib) | no | +| `test_plot_comparison_fsim.py` | broken-axis model-vs-FSIM comparison | no | +| `test_intercube_root_center.py` | ADR-0032 center-root latency guard (direct path) | no | + +`_allreduce_helpers.py` holds the shared plumbing (driver, config writers, +sweep/buffer-kind constants, plot aggregators, topology-diagram + FSIM +comparison emitters). It is not collected (no `test_` prefix). + +### D3. Latency metric — critical-path `pe_exec_ns` + +The reported latency per config is `crit_ns = max(pe_exec_ns)` over +`engine._results` — the slowest rank's PE execution time. This is the +number plotted on every latency chart and recorded in `summary.csv`. + +### D4. Sweep dimensions + +- **Latency sweep**: `n_elem ∈ {8, 32, 64, 128, 512, 1024, 2048, 4096, + 8192, 16384, 32768, 49152}` (16 excluded — collides with `n_cubes`) × + topology ∈ {ring_1d (6), torus_2d 2×3 (6), mesh_2d_no_wrap 2×3 (6)}. +- **Buffer-kind sweep**: `buffer_kind ∈ {tcm, sram, hbm}` × a smaller + `n_elem` grid, on torus_2d 6-SIP (3×2). buffer_kind is set in the temp + `ccl.yaml` (read by the backend at `init_process_group`, ADR-0023 D6). + +The 2×3 / 3×2 grids exercise the explicit-`w/h` SIP resolution +(ADR-0024 D5). + +### D5. Derived plots via `pytest_sessionfinish` aggregators + +Sweep tests are xdist-friendly: each parametrized case writes one JSON row +to a staging dir. The conftest `pytest_sessionfinish` hook (controller node +only) calls the aggregators in `_allreduce_helpers.py`: + +- `_aggregate_sweep_plots()` → per-topology PNGs + `summary.csv` +- `aggregate_buffer_kind_plot()` → the TCM/SRAM/HBM comparison PNG + csv + +The topology-diagram and FSIM-comparison figures are emitted directly by +their own `test_plot_*` tests (no row staging — they are pure functions of +`topology.yaml` and `summary.csv` respectively). All outputs land in +`docs/diagrams/allreduce_latency_plots/` and are **derived artifacts** per +CLAUDE.md (consistent-with-ADRs, no Phase-2 gate). + +### D6. The FSIM comparison reference is a hardcoded constant + +`emit_comparison_fsim_plot()` overlays the model curves against a single +external FSIM single-device reference (`366 µs`), held as a literal — there +is no external data file. The "measured" series comes from the simulator +(`op_log` GEMM count, `composite_window_ns`); the "theoretical" series is a +hand-derived analytical model (the same one ADR-0044 D5 flags as +ADR-unverified). + +## Consequences + +### Positive + +- The allreduce is evaluated through the same API a real DDP script uses, + so the harness doubles as an integration test of ADR-0024/0027. +- Figures regenerate on every `pytest` run from committed data; no manual + plot step. +- Rectangular-grid sweeps gave the regression coverage that surfaced the + ADR-0024 D5 `w/h` fix. + +### Negative / limitations + +- The full latency sweep runs in the default `pytest` (~minutes); it is not + marked `slow`. (Contrast ADR-0044, where the GEMM sweep is `slow`.) +- `test_intercube_root_center.py` carries a latency *threshold* assertion + (ADR-0032 center-root guard) — the only absolute-latency assertion in the + suite; it is sensitive to latency-model changes (ADR-0033). + +## Dependencies + +- **ADR-0013**: verification strategy (general policy this specializes). +- **ADR-0023 / ADR-0024 / ADR-0027**: IPCQ backend, rank=SIP launcher, + `mp.spawn` — the path D1 drives. +- **ADR-0032**: the algorithm under evaluation; D4 grids exercise its + topology branches. +- **ADR-0044**: the sibling GEMM evaluation harness. + +## Open questions + +- Should the latency sweep be marked `slow` for parity with the GEMM sweep? +- Should the FSIM reference move from a hardcoded constant to a versioned + data file? diff --git a/docs/adr/ADR-0044-eval-gemm-harness.md b/docs/adr/ADR-0044-eval-gemm-harness.md new file mode 100644 index 0000000..058c851 --- /dev/null +++ b/docs/adr/ADR-0044-eval-gemm-harness.md @@ -0,0 +1,130 @@ +# ADR-0044: GEMM Evaluation Harness — `scripts/gemm_sweep.py` + `tests/gemm/` + +## Status + +Accepted + +Documents the GEMM evaluation/characterization harness; verified against the +implementation (constants, tile sizes, figure set, and the script↔test +split cross-checked). The D5/D6 caveats are recorded limitations, not +inaccuracies. + +## Context + +ADR-0014 (PE pipeline) and ADR-0042 (tile-plan generators) define the GEMM +*implementation*; ADR-0033 defines the latency model. None of them describe +**how GEMM performance is swept and characterized** — the shape/variant +sweep that produces the timing data, and the figures that interpret it. +This ADR pins that harness. + +Unlike the allreduce harness (ADR-0043), the GEMM sweep is **heavy** (24 +sim runs: 8 shapes × 3 operand-staging variants; the `512` shape alone is +2048 tiles). That weight drives the split below. + +## Decision + +### D1. Two-layer split — heavy data generation (script) vs. fast figures (tests) + +- **Data generation stays a manual script**: `scripts/gemm_sweep.py` runs + `matmul-composite` (ADR-0042 plans) across shapes × variants via the same + `run_bench` path the CLI uses, harvests `result.engine.op_log`, and + writes `docs/diagrams/gemm_sweep.json` (per-stage / per-engine wall-clock + + occupancy + record counts + pe/composite windows). +- **Figure rendering is test-generated**: `tests/gemm/` reads the committed + `gemm_sweep.json` and renders matplotlib PNGs into + `docs/diagrams/gemm_plots/`. These tests are fast and run by default. + +Rationale: a slide-deck-scale sim sweep does not belong in every `pytest` +run, but the figures (cheap, deterministic) should regenerate freely and be +guarded by CI. This mirrors CLAUDE.md's script-vs-test split (scripts for +heavy/manual generation; tests for fast assertions). + +### D2. Slow regenerator test wraps the script + +`tests/gemm/test_gemm_sweep.py` is marked `@pytest.mark.slow` (excluded by +the default `addopts: -m "not slow"`). It invokes `scripts/gemm_sweep.py` +via subprocess to regenerate `gemm_sweep.json` on demand +(`pytest -m slow tests/gemm/test_gemm_sweep.py`). The sweep logic has a +single home (the script); the test only wraps it, so there is no duplicated +sim-driving code. + +### D3. Figure set (3 charts, `load_ref` variant) + +| Test | PNG | Content | +|---|---|---| +| `test_plot_gemm_stage_breakdown.py` | `gemm_stage_breakdown.png` | per-stage engine wall-clock (DMA in / Fetch / GEMM / DMA out) | +| `test_plot_gemm_mac_utilization.py` | `gemm_mac_utilization_measured.png` | GEMM util % + useful eff % | +| `test_plot_gemm_mac_utilization.py` | `gemm_mac_utilization_theoretical_vs_measured.png` | theoretical vs simulator-measured util/eff | + +`tests/gemm/_gemm_plot_helpers.py` holds the shared renderers (series logic +mirrors the GEMM `_render_*` functions in `scripts/build_overview_slides.py`, +which still draws these natively in the PPTX). Not collected (no `test_` +prefix). Each `test_plot_*` skips if `gemm_sweep.json` is absent. + +### D4. Tile sizes are data-driven; under-tile shapes are flagged + +Tile sizes are read from `gemm_sweep.json` (`tile_sizes`), which the sweep +records from `PeSchedulerComponent.TILE_M/K/N = 32/64/32` — the authoritative +source. Shapes with `M