Compare commits
109 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| faf011dae5 | |||
| 1dade267ff | |||
| 24c705419e | |||
| cd6f0ed91d | |||
| e9a5c438e3 | |||
| bb668a3ac3 | |||
| 0662c76fa7 | |||
| 37dc48fd27 | |||
| 7c45047e93 | |||
| 73e0b315fe | |||
| 1552028c25 | |||
| 56c51b184a | |||
| 8102ddbe30 | |||
| 84bb418e1e | |||
| dd3337f2e4 | |||
| b6315c3c90 | |||
| 821bbf26a2 | |||
| 2d8271c981 | |||
| 0c6ca0aaed | |||
| b64c43b947 | |||
| e8b7f4f064 | |||
| 23992548f7 | |||
| 92b9221533 | |||
| 694e0cc9b9 | |||
| 6b6e29968a | |||
| 77eece81c4 | |||
| 443ede99c7 | |||
| e45626c036 | |||
| 359a0eaa44 | |||
| 1cf8dd7868 | |||
| b715002b5a | |||
| 22f3968b51 | |||
| e3f08972da | |||
| 7c346dec1b | |||
| 1fbe833992 | |||
| 7f437a20bd | |||
| 756680f4e6 | |||
| ddee28a499 | |||
| 3c155be8e6 | |||
| 0ef4fde5d8 | |||
| ae942f6959 | |||
| 5672c8f3ef | |||
| 65c365f858 | |||
| c164645aee | |||
| 5b4d9cb597 | |||
| 9e1242039b | |||
| 9270d3435a | |||
| d5267ec718 | |||
| a965db7056 | |||
| 724a40453d | |||
| 1a4ecb7aac | |||
| 30a0451481 | |||
| f0c5b9c33f | |||
| e9d62b908c | |||
| 5bbe293bea | |||
| 3d4a3d43c4 | |||
| 4089e18770 | |||
| e8d6c283d8 | |||
| dd525bfcb7 | |||
| 4a55ae5c0b | |||
| 35453cc4fe | |||
| 17fb94086e | |||
| 55f025c4b1 | |||
| 184f654295 | |||
| 47e2c78c66 | |||
| 79ddb12b42 | |||
| 3647e2d8f5 | |||
| a8d04750e6 | |||
| 95cecccd8e | |||
| 7fad0371c5 | |||
| a8c50238c6 | |||
| 91cdfebb67 | |||
| 5a76ed4f6a | |||
| d282144339 | |||
| b3730a33eb | |||
| ab324c155c | |||
| 9103661098 | |||
| c699e2ddfc | |||
| 973a452b77 | |||
| 8de13efb9d | |||
| 9c554e3f64 | |||
| c6bc286740 | |||
| 444810c6e3 | |||
| 943626d758 | |||
| 8176cdf287 | |||
| a61fed3ce0 | |||
| f009ba7b1f | |||
| 39fc2e953f | |||
| 4859149392 | |||
| e2fe33180d | |||
| 80510b89a9 | |||
| d0f904c57e | |||
| 7a9d4ec47b | |||
| 6d24b9306f | |||
| fb600b3cdd | |||
| 1742600641 | |||
| f4f55b2c1c | |||
| 189794510a | |||
| b3ca532023 | |||
| e748a62264 | |||
| 222815d374 | |||
| d9e767d048 | |||
| 313dee503c | |||
| b1d6fafd3a | |||
| cc1bbd0ab7 | |||
| e33e76f2d1 | |||
| bd49c93703 | |||
| 9a02955770 | |||
| 5f8dd688f5 |
@@ -0,0 +1,417 @@
|
|||||||
|
---
|
||||||
|
description: Generate the 1H HW-SW codesign technical paper (PDF) — propose/confirm a TOC, then synthesize ADRs + live KernBench benchmark results into a sectioned LaTeX paper compiled with Tectonic.
|
||||||
|
---
|
||||||
|
|
||||||
|
# `/paper` — HW-SW Codesign Technical Report Generator
|
||||||
|
|
||||||
|
Produces a **technical-paper-style PDF** at
|
||||||
|
`docs/report/1H-codesign-paper/build/main.pdf`, synthesizing the
|
||||||
|
ADR corpus, SPEC.md, and **live KernBench benchmark results** into a
|
||||||
|
publication-shaped report on the three 1H optimizations:
|
||||||
|
|
||||||
|
1. **GEMM Acceleration** using the composite command
|
||||||
|
2. **All-Reduce Acceleration** using PE_IPCQ
|
||||||
|
3. **Fused Grouped Query Attention** using both composite command and PE_IPCQ
|
||||||
|
|
||||||
|
plus an explanation of the simulation platform itself (why KernBench
|
||||||
|
exists, its execution model, and how it computes latency / how accurate
|
||||||
|
that is).
|
||||||
|
|
||||||
|
Audience is the **internal team** (engineers + architects), but the
|
||||||
|
report reads like a real technical paper: **do not cite `ADR-NNNN` or
|
||||||
|
`SPEC §X.Y` anywhere** — not in prose, not in a reference list. ADRs and
|
||||||
|
SPEC are *grounding sources only* (use their content; never name them).
|
||||||
|
The **References** section, if used, lists **external literature only**
|
||||||
|
(e.g. FlashAttention, Megatron-LM, GPT-3, Llama 3) — actual papers, not
|
||||||
|
internal docs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Operating principles (read first)
|
||||||
|
|
||||||
|
- **Confirm the TOC before writing.** The first thing `/paper` does is
|
||||||
|
present the outline (saved `toc.md` if present, else the default
|
||||||
|
below) and **stop for the user's confirmation / adjustments**. Do not
|
||||||
|
generate sections until the user approves the TOC.
|
||||||
|
- **Ground every number.** Every quantitative result in prose, tables,
|
||||||
|
or captions MUST trace to (a) a committed bench artifact
|
||||||
|
(`*.csv` / `*.json` / `*.png` under `1H_milestone_output/` or
|
||||||
|
`docs/diagrams/`), (b) a freshly-run bench output, or (c) a specific
|
||||||
|
ADR/SPEC quotation. **Never invent latencies, speedups, or
|
||||||
|
utilizations.** If a needed number does not exist, generate it via a
|
||||||
|
bench run (below) or state the gap — do not guess.
|
||||||
|
- **Reuse existing results by default.** The three milestone benches
|
||||||
|
already have committed outputs. Do **not** re-run a bench unless the
|
||||||
|
user asks to regenerate, or the artifact is missing/stale.
|
||||||
|
- **Design / proposal status honesty.** Some designs are `Proposed`,
|
||||||
|
not yet implemented (e.g. ADR-0061, ADR-0065). Present these as
|
||||||
|
proposed and clearly separate "modeled & measured" from
|
||||||
|
"designed, not yet measured." Do not report measurements for
|
||||||
|
unimplemented paths.
|
||||||
|
- **English prose.** Architectural terms (composite command, PE_IPCQ,
|
||||||
|
GQA, TCM, SIP, CUBE, PE) used as-is.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Invocation
|
||||||
|
|
||||||
|
- `/paper` — **default**. Resolve the period, present the TOC
|
||||||
|
(`toc.md` if it exists, else the default TOC), and **stop** for
|
||||||
|
confirmation. Once the user approves (possibly after editing the TOC
|
||||||
|
in conversation), persist `toc.md` and proceed to full build.
|
||||||
|
- `/paper toc` — only (re)negotiate and persist the TOC. No sections,
|
||||||
|
no build.
|
||||||
|
- `/paper build` — assume the saved `toc.md` is approved; run the full
|
||||||
|
pipeline (sections → figures → compile PDF) without re-confirming.
|
||||||
|
- `/paper section <id>` — regenerate a single section (`<id>` = a
|
||||||
|
filename stem under `sections/`, e.g. `04-allreduce`) and recompile.
|
||||||
|
- `/paper figures` — (re)generate only the benchmark figures/data
|
||||||
|
(runs/refreshes benches), no prose changes, then recompile.
|
||||||
|
- `/paper pdf` — only recompile the existing `.tex` to PDF (Tectonic).
|
||||||
|
|
||||||
|
If `toc.md` does not yet exist, any mode except `/paper`/`/paper toc`
|
||||||
|
first falls back to presenting the default TOC for confirmation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Output layout
|
||||||
|
|
||||||
|
```
|
||||||
|
docs/report/1H-codesign-paper/
|
||||||
|
toc.md # the agreed outline (persisted; human-editable)
|
||||||
|
main.tex # preamble + \input of each section
|
||||||
|
refs.bib # ADR/SPEC bibliography (optional; or inline refs)
|
||||||
|
sections/
|
||||||
|
00-exec-summary.tex
|
||||||
|
01-introduction.tex
|
||||||
|
02-platform.tex # includes 3.4 Modeled Hardware Configuration
|
||||||
|
03-gemm.tex
|
||||||
|
04-allreduce.tex
|
||||||
|
05-gqa.tex
|
||||||
|
06-discussion.tex
|
||||||
|
07-conclusion.tex
|
||||||
|
08-future-work.tex
|
||||||
|
figures/ # PNGs copied in from bench outputs (portable)
|
||||||
|
build/ # Tectonic output; main.pdf lives here
|
||||||
|
```
|
||||||
|
|
||||||
|
Report-specific benchmark/plot harnesses (when a figure must be
|
||||||
|
generated, see *Generating missing results*) live **isolated** under:
|
||||||
|
|
||||||
|
```
|
||||||
|
scripts/paper/ # report-only harnesses — NOT registered benches
|
||||||
|
```
|
||||||
|
|
||||||
|
This directory is intentionally separate from `src/kernbench/benches/`
|
||||||
|
so the report's harnesses never collide with other people's benches
|
||||||
|
(the bench registry only audits `src/kernbench/benches/`). Treat
|
||||||
|
`scripts/paper/*` and everything under `docs/report/1H-codesign-paper/`
|
||||||
|
as **derived artifacts** (like `docs/diagrams/`): creating/updating them
|
||||||
|
does not require Phase 2 approval, but they MUST stay consistent with
|
||||||
|
SPEC.md and ADRs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Default TOC
|
||||||
|
|
||||||
|
If `toc.md` is absent, propose this and ask the user to confirm or edit:
|
||||||
|
|
||||||
|
1. **Executive Summary** — the attention-optimization goal, the two
|
||||||
|
enabling optimizations (GEMM composite command + PE_IPCQ communication),
|
||||||
|
the fused GQA capstone, the headline results, and the bottom-line
|
||||||
|
recommendation on which HW changes are worth keeping. May run a
|
||||||
|
paragraph or two longer than a terse abstract; front-loads conclusions.
|
||||||
|
2. **Introduction** — the 1H focus is **attention-kernel optimization**:
|
||||||
|
FlashAttention-style tiling and Grouped Query Attention (GQA), building
|
||||||
|
on prior work on Multi-Head Attention (MHA, studied earlier — reference
|
||||||
|
it as the established baseline, not re-derived here). Optimizing a fused
|
||||||
|
attention kernel requires two enabling optimizations, each studied in
|
||||||
|
its own right and then **combined inside the fused kernel**:
|
||||||
|
(a) **GEMM optimization** (the composite command) and
|
||||||
|
(b) **communication optimization** (PE_IPCQ). Frame the three result
|
||||||
|
sections as *two enablers → the fused GQA capstone that uses both*.
|
||||||
|
Motivate why HW-SW codesign (not software alone) is required to realize
|
||||||
|
the gains.
|
||||||
|
3. **The KernBench Platform**
|
||||||
|
1. *Why KernBench* — source-level kernel execution with **no compiler /
|
||||||
|
software-stack dependency**; isolate algorithm-level optimization from
|
||||||
|
other layers so a kernel can fully exploit the hardware without E2E
|
||||||
|
cross-layer entanglement.
|
||||||
|
2. *Execution model* — discrete-event graph; `runtime API → sim_engine →
|
||||||
|
components`; PE pipeline + composite commands; 2-pass data/timing model.
|
||||||
|
3. *Latency model & accuracy* — traversal-based golden invariants
|
||||||
|
(every hop pays > 0, deterministic, explicit connectivity); the
|
||||||
|
structural CPU-dispatch cost model; per-edge BW occupancy, HBM
|
||||||
|
pseudo-channel parallelism, flit streaming; known simplifications &
|
||||||
|
what calibration anchors the constants.
|
||||||
|
4. *Modeled Hardware Configuration* — the shared, platform-wide
|
||||||
|
quantitative config used across **all** experiments, read from
|
||||||
|
`topology.yaml`: SIP/CUBE/PE hierarchy (counts & nesting), PE clock &
|
||||||
|
compute throughput, TCM/SRAM/HBM capacities & bandwidths, NOC /
|
||||||
|
inter-CUBE / inter-SIP link bandwidths & latencies, and the cost-model
|
||||||
|
constants. Per-experiment workload parameters (GEMM shapes, All-Reduce
|
||||||
|
topologies, GQA seq/head/user counts) stay in each evaluation section,
|
||||||
|
not here.
|
||||||
|
4. **GEMM Acceleration via Composite Command** — necessity · design · results · analysis.
|
||||||
|
5. **All-Reduce Acceleration via PE_IPCQ** — necessity · design · results · analysis.
|
||||||
|
6. **Fused Grouped Query Attention** (composite + PE_IPCQ) — necessity · design · results · analysis.
|
||||||
|
7. **Discussion** — which HW changes are meaningful, and why (cross-cutting).
|
||||||
|
8. **Conclusion**.
|
||||||
|
9. **Future Work — 2H** — add the FFN/MoE layer toward full LLM decoding;
|
||||||
|
analyze how compute & data should be distributed for agentic / MoE
|
||||||
|
workloads.
|
||||||
|
10. **References** *(optional)* — **external literature only** (e.g.
|
||||||
|
FlashAttention, Megatron-LM, GPT-3, Llama 3). **No ADR/SPEC entries.**
|
||||||
|
|
||||||
|
Each of §4/§5/§6 follows the same four-beat structure:
|
||||||
|
**(a) why the optimization is needed**, **(b) the design (how it works)**,
|
||||||
|
**(c) experimental results** (run/plot/measure), **(d) analysis & meaning**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Source map (ADR / SPEC → section)
|
||||||
|
|
||||||
|
Use these as **grounding sources only** — read them for content, but
|
||||||
|
**never name an ADR or SPEC in the paper**. Verify each quantitative
|
||||||
|
claim against the actual file before asserting it (CLAUDE.md: no
|
||||||
|
asserting architectural numbers from memory).
|
||||||
|
|
||||||
|
- **Modeled Hardware Configuration (§3.4)**: `topology.yaml`
|
||||||
|
(authoritative for counts, clocks, BW, link params, cost-model
|
||||||
|
constants) is the **primary** source — read it directly and build the
|
||||||
|
HW table from it. Cross-check against ADR-0003 (hierarchy/scope),
|
||||||
|
ADR-0004 (memory BW guarantee), ADR-0033 (latency model), ADR-0064
|
||||||
|
(cost-model defaults). If `topology.yaml` and an ADR disagree, the
|
||||||
|
loaded `topology.yaml` is what the sim actually runs — report that
|
||||||
|
value and flag the mismatch.
|
||||||
|
|
||||||
|
- **Platform — why/execution/latency**: `SPEC §0`, `SPEC §0.1`
|
||||||
|
(golden invariants), `SPEC §1 R1/R2/R3`, `SPEC §2.1` (graph execution);
|
||||||
|
ADR-0003 (hierarchy/scope), ADR-0007 (runtime↔sim_engine boundary),
|
||||||
|
ADR-0014 (PE pipeline / composite), ADR-0020 (2-pass data/timing),
|
||||||
|
ADR-0033 (latency model assumptions & simplifications),
|
||||||
|
ADR-0064 (structural CPU-dispatch cost model: `FIXED + logical_bytes×R`).
|
||||||
|
- **GEMM / composite**: ADR-0014 (composite command D3.2/D3.3),
|
||||||
|
ADR-0042 (tile-plan generators), ADR-0044 (GEMM eval harness),
|
||||||
|
ADR-0064 (cost model — composite's per-command BW reduction),
|
||||||
|
ADR-0065 (flat-ops `CompositeCmd`, *Proposed*).
|
||||||
|
- **All-Reduce / PE_IPCQ**: ADR-0023 (PE_IPCQ — control plane, ring
|
||||||
|
buffers, VC split), ADR-0025 (direction addressing), ADR-0032
|
||||||
|
(intercube all-reduce), ADR-0043 (allreduce eval harness),
|
||||||
|
ADR-0047 (AHBM CCL backend), ADR-0050 (CCL algorithm contract).
|
||||||
|
- **GQA / fused attention**: ADR-0060 (fused GQA kernel),
|
||||||
|
ADR-0061 (`tl.broadcast`/`tl.repeat`, *Proposed*),
|
||||||
|
ADR-0062 (lazy `tl.load`), ADR-0063 (`tl.scratch_scope`),
|
||||||
|
ADR-0064 (cost model), ADR-0065 (flat-ops composite + `softmax_merge`,
|
||||||
|
*Proposed*). KO mirrors in `docs/adr-ko/`; DDD-0060/0065 design docs if present.
|
||||||
|
- **Eval/milestone pattern**: ADR-0054 (self-contained milestone benches);
|
||||||
|
ADR-0045 (bench contract).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Result map (bench → artifact → section) + run commands
|
||||||
|
|
||||||
|
All runs use the repo's `topology.yaml` unless a section needs a
|
||||||
|
specific topology. Reuse committed artifacts unless told to regenerate.
|
||||||
|
|
||||||
|
### §4 GEMM — `milestone-1h-gemm`
|
||||||
|
- Artifacts (committed): `src/kernbench/benches/1H_milestone_output/gemm/`
|
||||||
|
→ `gemm_sweep.json`, `gemm_stage_breakdown.png`,
|
||||||
|
`gemm_mac_utilization_measured.png`,
|
||||||
|
`gemm_mac_utilization_theoretical_vs_measured.png`.
|
||||||
|
- Regenerate: `kernbench run --topology topology.yaml --bench milestone-1h-gemm`
|
||||||
|
(full 24-shape sweep ≈ minutes; `MILESTONE_FAST=1` reuses committed JSON).
|
||||||
|
|
||||||
|
### §5 All-Reduce — `milestone-1h-ccl`
|
||||||
|
- Artifacts (committed): `src/kernbench/benches/1H_milestone_output/ccl/`
|
||||||
|
→ `summary.csv`, `topology.png`, per-topology
|
||||||
|
`AllReduce_LRAB_*` PNGs, buffer-kind sweep
|
||||||
|
`AllReduce_LRAB_2Dtorus_..._with_TCM_SRAM_HBM.{png,csv}`,
|
||||||
|
`comparison_mesh_vs_ring_vs_2DTorus_vs_theoretical_vs_fsim.png`.
|
||||||
|
- Regenerate: `kernbench run --topology topology.yaml --bench milestone-1h-ccl`.
|
||||||
|
|
||||||
|
### §6 GQA — `milestone-gqa-headline`
|
||||||
|
- Artifact (committed): `src/kernbench/benches/1H_milestone_output/gqa_headline/sweep.json`.
|
||||||
|
**Caveat:** this currently holds only `op_log_summary` **op-counts**
|
||||||
|
(`gemm_count`, `ipcq_copy_count`, `dma_read_count`, `dma_write_count`)
|
||||||
|
for 4 panels (single/multi-user × prefill/decode) — **no latency,
|
||||||
|
no figures**.
|
||||||
|
- Regenerate counts:
|
||||||
|
`GQA_HEADLINE_RUN=1 kernbench run --topology topology.yaml --bench milestone-gqa-headline`.
|
||||||
|
- For a real GQA **results** section, generate latency + figures via an
|
||||||
|
isolated report harness — see next.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Generating missing results (isolation rule)
|
||||||
|
|
||||||
|
When a section needs an artifact that does not yet exist (today: GQA
|
||||||
|
latency and any GQA figure), create a **report-dedicated harness** under
|
||||||
|
`scripts/paper/` — never under `src/kernbench/benches/`. Rules:
|
||||||
|
|
||||||
|
- One file per concern, prefixed `paper_`, e.g.
|
||||||
|
`scripts/paper/paper_gqa_latency.py` (drives the engine, emits
|
||||||
|
latency + a baseline-vs-optimized comparison),
|
||||||
|
`scripts/paper/paper_plot_gqa.py` (renders figures from that data).
|
||||||
|
- Follow the **ADR-0054 self-contained pattern**: the harness builds its
|
||||||
|
own `GraphEngine`(s), imports the existing GQA kernel modules
|
||||||
|
(`_gqa_attention_{prefill,decode}_{short,long}.py`,
|
||||||
|
`milestone_gqa_headline.py`), runs them, and reads latency from the
|
||||||
|
engine's completion timestamps / op log. It does **not** register a
|
||||||
|
bench and does **not** import or mutate other people's benches.
|
||||||
|
- Output figures land in `docs/report/1H-codesign-paper/figures/` and,
|
||||||
|
if useful as a shared diagram, mirror into `docs/diagrams/gqa_plots/`.
|
||||||
|
- **Only measure what the current code actually runs.** ADR-0065's GQA
|
||||||
|
opt2 (`softmax_merge`, two-composite decode) is *Proposed*; if it is
|
||||||
|
not wired in production, present it as designed-not-measured and base
|
||||||
|
the measured results on the implemented path. Verify implementation
|
||||||
|
status by reading the code before claiming a measurement.
|
||||||
|
- These harnesses are derived-artifact tooling (no Phase 2 needed). **But**
|
||||||
|
if the report ever requires changing a *registered production bench*
|
||||||
|
under `src/kernbench/benches/` or any production module, that is a
|
||||||
|
non-trivial production change — STOP and follow CLAUDE.md Phase 1 →
|
||||||
|
approval → Phase 2.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Procedure
|
||||||
|
|
||||||
|
### Step 0 — Toolchain (Tectonic)
|
||||||
|
|
||||||
|
Ensure a working Tectonic before any compile step. In order:
|
||||||
|
|
||||||
|
1. If `tectonic` is on PATH (`tectonic --version` succeeds), use it.
|
||||||
|
2. Else try, in order, whichever succeeds (capture output, don't hang):
|
||||||
|
- `winget install --id TectonicProject.Tectonic --silent --accept-package-agreements --accept-source-agreements`
|
||||||
|
- `scoop install tectonic`
|
||||||
|
- Direct download (no admin): fetch the latest Windows `x86_64-pc-windows-msvc`
|
||||||
|
release zip from `https://github.com/tectonic-typesetting/tectonic/releases`
|
||||||
|
via PowerShell `Invoke-WebRequest`, expand it, and place `tectonic.exe`
|
||||||
|
under `docs/report/1H-codesign-paper/build/.tools/`. Use that absolute
|
||||||
|
path for compilation.
|
||||||
|
3. Re-verify with `tectonic --version`. If all methods fail, write the
|
||||||
|
`.tex` files anyway, **report the failure**, and tell the user the
|
||||||
|
exact command to compile manually (or to use Overleaf). Do not block
|
||||||
|
the rest of the pipeline.
|
||||||
|
|
||||||
|
Tectonic fetches LaTeX packages on first compile (needs network once).
|
||||||
|
|
||||||
|
### Step 1 — Period + TOC
|
||||||
|
|
||||||
|
- Period from system date: month 1–6 → `{YYYY}-1H`, else `{YYYY}-2H`.
|
||||||
|
(Report dir is fixed at `1H-codesign-paper` for this half; adjust the
|
||||||
|
title page text with the period.)
|
||||||
|
- If `toc.md` exists, present it; else present the **Default TOC**.
|
||||||
|
- **Stop and ask the user to confirm or edit.** Incorporate edits.
|
||||||
|
Persist the agreed outline to `toc.md`. (In `/paper build` mode, skip
|
||||||
|
the stop and trust the saved `toc.md`.)
|
||||||
|
|
||||||
|
### Step 2 — Platform section (§3, incl. §3.4 HW config)
|
||||||
|
|
||||||
|
Write `sections/02-platform.tex` from the platform source map, covering
|
||||||
|
all four sub-parts:
|
||||||
|
|
||||||
|
- §3.1–3.3 (why / execution model / latency model + accuracy). Include
|
||||||
|
the cost-model formula `dispatch_cycles = FIXED + logical_bytes×R` with
|
||||||
|
the committed defaults (verify against ADR-0064 before quoting), and
|
||||||
|
explicitly address **how accurate** the latency is: what is modeled
|
||||||
|
precisely (per-edge BW occupancy, HBM pseudo-channels, flit streaming,
|
||||||
|
per-component overhead) and the known simplifications.
|
||||||
|
- §3.4 *Modeled Hardware Configuration*: read `topology.yaml` directly
|
||||||
|
and build a compact, table-driven summary — SIP/CUBE/PE hierarchy
|
||||||
|
(counts & nesting), PE clock & compute throughput, TCM/SRAM/HBM
|
||||||
|
capacity & bandwidth, NOC / inter-CUBE / inter-SIP link bandwidth &
|
||||||
|
latency, and the cost-model constants (`FIXED_PER_CMD`, `R`, composite
|
||||||
|
cap). Every number from `topology.yaml`. This is the shared config;
|
||||||
|
later sections say "the modeled hardware" and only add their own
|
||||||
|
workload params.
|
||||||
|
|
||||||
|
Do not name the source ADRs/SPEC in the prose.
|
||||||
|
|
||||||
|
### Step 3 — The three result sections (§4 GEMM, §5 All-Reduce, §6 GQA)
|
||||||
|
|
||||||
|
For each, in order, produce the four beats:
|
||||||
|
|
||||||
|
- **(a) Necessity** — from the ADR Context: the bottleneck the
|
||||||
|
optimization targets (e.g. per-command dispatch overhead for GEMM;
|
||||||
|
collective comm cost / HoL blocking for All-Reduce; KV-load-bound
|
||||||
|
decode + softmax merge for GQA).
|
||||||
|
- **(b) Design** — from the ADR Decisions: composite command tile
|
||||||
|
pipeline (ADR-0014); PE_IPCQ control/data split + ring buffers + VC
|
||||||
|
(ADR-0023); fused attention with online-softmax, lazy load, scratch
|
||||||
|
recycling (ADR-0060/0062/0063). Note Proposed-vs-Accepted status.
|
||||||
|
- **(c) Results** — reuse committed artifacts (GEMM, All-Reduce). For
|
||||||
|
GQA, generate latency + figures via the isolated `scripts/paper/`
|
||||||
|
harness (above). Copy chosen PNGs into `figures/`, `\includegraphics`
|
||||||
|
them, and build result tables from the CSV/JSON — quoting actual
|
||||||
|
numbers, with a one-line note of the run command and source artifact.
|
||||||
|
- **(d) Analysis** — interpret: what the curve/table shows, why the HW
|
||||||
|
feature produces it, where it saturates or fails to help, and the
|
||||||
|
cost (area/complexity) implied. Tie back to the cost model.
|
||||||
|
|
||||||
|
### Step 4 — Discussion, Conclusion, Future Work (§6/§7/§8)
|
||||||
|
|
||||||
|
- **Discussion**: synthesize across the three — which HW changes
|
||||||
|
(composite command, PE_IPCQ, the cost-model-exposed BW reduction)
|
||||||
|
carry their weight, and under what regimes.
|
||||||
|
- **Conclusion**: the codesign thesis, supported by the measured
|
||||||
|
results; state plainly which HW changes are meaningful.
|
||||||
|
- **Future Work (2H)**: FFN / MoE layer toward full LLM decoding; how
|
||||||
|
compute & data should be distributed for agentic / MoE workloads
|
||||||
|
(expert routing, token dispersion, all-to-all vs all-reduce, capacity).
|
||||||
|
Frame as the next codesign questions, not claims.
|
||||||
|
|
||||||
|
### Step 5 — Assemble & compile
|
||||||
|
|
||||||
|
- `main.tex`: article/IEEEtran-style preamble, title with the period,
|
||||||
|
author, executive-summary input, then `\input{sections/...}` in TOC
|
||||||
|
order, then references. Keep the preamble minimal (graphicx, booktabs,
|
||||||
|
hyperref, amsmath) so Tectonic compiles cleanly.
|
||||||
|
- Compile: `tectonic main.tex --outdir build` (or the resolved Tectonic
|
||||||
|
path) from `docs/report/1H-codesign-paper/`. Re-run if references need
|
||||||
|
a second pass. Confirm `build/main.pdf` exists and report its size.
|
||||||
|
|
||||||
|
### Step 6 — Chat report
|
||||||
|
|
||||||
|
Emit to chat (not to any file):
|
||||||
|
|
||||||
|
```
|
||||||
|
## /paper — Build Summary
|
||||||
|
**PDF:** docs/report/1H-codesign-paper/build/main.pdf (<N> pages, <size>)
|
||||||
|
**Toolchain:** tectonic <version | how installed>
|
||||||
|
**Sections written:** <list>
|
||||||
|
**Benches: reused vs regenerated**
|
||||||
|
- GEMM: <reused committed | regenerated via milestone-1h-gemm>
|
||||||
|
- All-Reduce: <reused | regenerated>
|
||||||
|
- GQA: <generated via scripts/paper/paper_gqa_latency.py | counts-only>
|
||||||
|
**Figures included:** <list of figures/*.png>
|
||||||
|
**Grounding check:** every result number traced to <artifact list>
|
||||||
|
**Proposed-not-measured items flagged:** <e.g. ADR-0065 opt2, ADR-0061>
|
||||||
|
**Open gaps / suggested next runs:** <…>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Constraints (do not violate)
|
||||||
|
|
||||||
|
1. **TOC first.** Never write sections before the TOC is confirmed
|
||||||
|
(default mode). `toc.md` is the contract.
|
||||||
|
2. **No fabricated numbers.** Every quantitative claim traces to a
|
||||||
|
committed artifact, a fresh bench run, or an ADR/SPEC quote. Verify
|
||||||
|
architectural constants against the source file, not memory.
|
||||||
|
3. **Reuse by default.** Do not re-run a bench unless asked or the
|
||||||
|
artifact is missing/stale.
|
||||||
|
4. **Isolation.** Report harnesses live only in `scripts/paper/`; never
|
||||||
|
register a bench, never touch other people's benches.
|
||||||
|
5. **Production-change gate.** Any change to a registered bench or
|
||||||
|
production module triggers CLAUDE.md Phase 1 → approval → Phase 2.
|
||||||
|
Report tooling and `docs/report/...` are derived artifacts (exempt).
|
||||||
|
6. **Status honesty.** Mark Proposed designs as proposed; never present
|
||||||
|
an unimplemented path as measured.
|
||||||
|
7. **No internal references.** Never name `ADR-NNNN` or `SPEC §X.Y` in
|
||||||
|
prose, captions, or the reference list — they are grounding sources
|
||||||
|
only. A References section, if present, holds **external literature
|
||||||
|
only**. Hardware numbers come from `topology.yaml`.
|
||||||
|
8. **Determinism.** Sections in TOC order; same inputs → same paper.
|
||||||
|
9. **English prose**, internal-audience tone.
|
||||||
|
```
|
||||||
@@ -371,6 +371,13 @@ Concrete forms that Part 1's *Verification Plan* MUST take in this repo:
|
|||||||
- `kernbench run --device <id>` runs the benchmark on a single device.
|
- `kernbench run --device <id>` runs the benchmark on a single device.
|
||||||
- Omitting `--device` runs the benchmark on all devices discovered in the topology (logically parallel).
|
- Omitting `--device` runs the benchmark on all devices discovered in the topology (logically parallel).
|
||||||
- Device enumeration is handled by the CLI only; benchmarks MUST remain single-device.
|
- Device enumeration is handled by the CLI only; benchmarks MUST remain single-device.
|
||||||
|
- **Eval-bench exception (ADR-0054)**: a *milestone / eval bench*
|
||||||
|
(`milestone-1h-*`) may drive many configurations and build its own
|
||||||
|
per-config engines to regenerate a domain's full result + figure set; it
|
||||||
|
ignores `--device` and submits a sentinel tensor to satisfy the
|
||||||
|
"must submit ≥1 request" contract (ADR-0045 D4). This is the eval-harness
|
||||||
|
carve-out to the single-device rule, alongside the ADR-0024 multi-SIP CCL
|
||||||
|
exception.
|
||||||
|
|
||||||
## Derived Artifacts (Clarification)
|
## Derived Artifacts (Clarification)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Accepted (Revision 5 — Phase 2 landed 2026-04-14, 523 passed + 1 strict xfail)
|
Accepted (Revision 6 — cube_start 추가 2026-06-04; Revision 5 landed 2026-04-14)
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
@@ -30,9 +30,11 @@ class DPPolicy:
|
|||||||
pe: Literal["replicate", "column_wise", "row_wise"] = "replicate"
|
pe: Literal["replicate", "column_wise", "row_wise"] = "replicate"
|
||||||
num_pes: int | None = None
|
num_pes: int | None = None
|
||||||
num_cubes: int | None = None
|
num_cubes: int | None = None
|
||||||
|
cube_start: int = 0 # Revision 6 — SIP 내 첫 cube 인덱스; D8 참조
|
||||||
```
|
```
|
||||||
|
|
||||||
제거되는 필드: `sip`, `num_sips`.
|
제거되는 필드: `sip`, `num_sips`.
|
||||||
|
추가되는 필드 (Revision 6): `cube_start` — D8 참조.
|
||||||
|
|
||||||
### D2. `ShardSpec` — structural (sip, cube, pe) 좌표, `pe_index` 완전 제거
|
### D2. `ShardSpec` — structural (sip, cube, pe) 좌표, `pe_index` 완전 제거
|
||||||
|
|
||||||
@@ -114,7 +116,7 @@ def resolve_dp_policy(
|
|||||||
for ls in local_shards:
|
for ls in local_shards:
|
||||||
all_shards.append(ShardSpec(
|
all_shards.append(ShardSpec(
|
||||||
sip=target_sip, # from caller (current_device)
|
sip=target_sip, # from caller (current_device)
|
||||||
cube=cube_id, # local within SIP
|
cube=policy.cube_start + cube_id, # Rev 6: D8 cube_start만큼 shift; 기본 0 = local within SIP
|
||||||
pe=ls.local_pe, # local within cube (explicit name)
|
pe=ls.local_pe, # local within cube (explicit name)
|
||||||
offset_bytes=cube_offset + ls.offset_bytes,
|
offset_bytes=cube_offset + ls.offset_bytes,
|
||||||
nbytes=ls.nbytes,
|
nbytes=ls.nbytes,
|
||||||
@@ -224,6 +226,47 @@ KernBench는 사내 프로젝트로 call site가 한정되어 있어 한 번에
|
|||||||
**Silent drift 차단**이 property 완전 제거의 주된 이점: global flat을 기대한
|
**Silent drift 차단**이 property 완전 제거의 주된 이점: global flat을 기대한
|
||||||
코드가 SIP-local 결과를 받아 조용히 잘못된 인덱싱을 할 가능성 제거.
|
코드가 SIP-local 결과를 받아 조용히 잘못된 인덱싱을 할 가능성 제거.
|
||||||
|
|
||||||
|
### D8. SIP 내 분리된 cube sub-mesh를 위한 `cube_start: int = 0` 추가
|
||||||
|
|
||||||
|
**Revision 6 추가 사항 (2026-06-04)**.
|
||||||
|
|
||||||
|
**목적**: 한 SIP 내에서 분리된 cube sub-mesh 지정 (예: 0..7과 8..15을 함께
|
||||||
|
사용). GQA Llama-70B 8-KV-group 헤드라인 타겟 — SIP당 2×4 KV-group 2개 ×
|
||||||
|
4 SIP = 64 cubes — 의 두 번째 KV-group을 기본값 0..7이 아닌 cubes 8..15에
|
||||||
|
배치하기 위해 필요.
|
||||||
|
|
||||||
|
**Semantics**: `resolve_dp_policy`가
|
||||||
|
`cube = policy.cube_start + cube_id`로 ShardSpec 생성 (cube_id는 launch 내에서
|
||||||
|
`0..num_cubes-1` 순회). 선택된 cube는 target SIP 내의
|
||||||
|
`[cube_start, cube_start + num_cubes)` 범위.
|
||||||
|
|
||||||
|
**기본값**: `cube_start = 0`은 기존 모든 호출 사이트 동작을 bit-for-bit
|
||||||
|
보존 (이전처럼 `ShardSpec.cube ∈ [0, num_cubes)`). CCL milestone bench의
|
||||||
|
full-SIP `DPPolicy(num_cubes=16)`은 그대로 cubes 0..15 생성.
|
||||||
|
|
||||||
|
**제약**: intra-device 불변성 보존.
|
||||||
|
`cube_start ∈ [0, cubes_per_sip)`이며
|
||||||
|
`cube_start + num_cubes ≤ cubes_per_sip`. SIP 경계 교차는 여전히
|
||||||
|
`ahbm.set_device(rank)` (ADR-0024)의 책임.
|
||||||
|
|
||||||
|
**왜 scalar인가** (2D `cube_mesh_origin`이나 임의 `cube_ids` list가 아닌):
|
||||||
|
consumer 커널 (예: `_attention_mesh_mlo_2d`)이 row-major contiguous cube를
|
||||||
|
가정; scalar `cube_start`는 `num_cubes`와 자연스럽게 쌍을 이룸 (범위 =
|
||||||
|
`[start, start + count)`); 기본값 0에서 마이그레이션 부담 없음. 비연속
|
||||||
|
케이스가 필요해지면 향후 더 일반적인 설계를 위에 얹을 수 있음.
|
||||||
|
|
||||||
|
**하위 호환성**: 기본값이 있는 가산적 변경. 기존 호출 사이트의 변경을
|
||||||
|
강제하지 않음. D7의 "breaking change" 입장은 원래 sip/num_sips 제거에만
|
||||||
|
해당하며, `cube_start`는 기존 호출자를 깨지 않음.
|
||||||
|
|
||||||
|
**커널 측 참고**: kernbench의 `tl.program_id(axis=1)`은 launch-local rank가
|
||||||
|
아닌 물리 cube id를 반환 (ADR-0022). `program_id(axis=1)`에서 ring 위치를
|
||||||
|
도출하는 커널은 `cube_start > 0`일 때 launch-local rank를 복원하기 위해
|
||||||
|
`cube_start`를 빼야 함 — 그렇지 않으면 out-of-bounds sub-mesh 위치를
|
||||||
|
계산하여 deadlock 발생. 참고 패턴은 `_attention_mesh_mlo_2d.py` 참조.
|
||||||
|
ADR-0022의 향후 개정에서 `program_id(axis=1)`을 launch-local rank semantics로
|
||||||
|
이동하면 커널 측 명시적 차감이 제거될 수 있음.
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
- **ADR-0024** (launcher): `set_device(rank)` 및 current-device scoping이
|
- **ADR-0024** (launcher): `set_device(rank)` 및 current-device scoping이
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ Accepted
|
|||||||
`tests/sccl/` 평가 하니스를 문서화한다; 구현과 대조 검증 완료
|
`tests/sccl/` 평가 하니스를 문서화한다; 구현과 대조 검증 완료
|
||||||
(상수, 파일 집합, 스윕 차원을 교차 확인).
|
(상수, 파일 집합, 스윕 차원을 교차 확인).
|
||||||
|
|
||||||
|
**ADR-0054로 개정됨**: 드라이버 코어, sweep, renderer가 `milestone-1h-ccl`
|
||||||
|
bench(단일 home)로 이동했다; `tests/sccl/_allreduce_helpers.py`는 이제 거기서
|
||||||
|
re-export한다(pytest 전용 param 빌더 + `_run_distributed` wrapper는 로컬
|
||||||
|
유지). figure 테스트는 변경 없음.
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
ADR-0032는 intercube all-reduce *알고리즘*을 정의하고, ADR-0023/0024/0027은
|
ADR-0032는 intercube all-reduce *알고리즘*을 정의하고, ADR-0023/0024/0027은
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ GEMM 평가/특성화 하니스를 문서화한다; 구현과 대조 검증 완
|
|||||||
(상수, tile 크기, figure 집합, script↔test 분할을 교차 확인). D5/D6
|
(상수, tile 크기, figure 집합, script↔test 분할을 교차 확인). D5/D6
|
||||||
caveat은 부정확이 아니라 기록된 한계다.
|
caveat은 부정확이 아니라 기록된 한계다.
|
||||||
|
|
||||||
|
**ADR-0054로 개정됨**: sweep + renderer가 `milestone-1h-gemm` bench(단일
|
||||||
|
home)로 이동했다; `scripts/gemm_sweep.py`와 `tests/gemm/`는 이제 거기서
|
||||||
|
re-export한다. D1/D2의 "데이터 생성은 수동 script / 무거운 작업은 opt-in"은
|
||||||
|
평가-bench 패턴으로 대체된다(하나의 bench가 전부 재생성;
|
||||||
|
`MILESTONE_FAST=1`은 committed JSON 재사용).
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
ADR-0014(PE pipeline)와 ADR-0042(tile-plan generator)는 GEMM *구현*을
|
ADR-0014(PE pipeline)와 ADR-0042(tile-plan generator)는 GEMM *구현*을
|
||||||
|
|||||||
@@ -0,0 +1,265 @@
|
|||||||
|
# ADR-0045: Bench Module Contract — registration, dispatch, and authoring
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted (2026-05-21).
|
||||||
|
|
||||||
|
`src/kernbench/benches/` 패키지의 등록 메커니즘(@bench), CLI 디스패치 경로
|
||||||
|
(`kernbench run/list`), 그리고 새 bench 모듈 작성 시 따라야 할 계약을 통합
|
||||||
|
정의한다. ADR-0010 (CLI surface)이 `kernbench list/run` 인터페이스를 명세하나,
|
||||||
|
**bench가 어떻게 등록되고 어떤 함수 시그너처를 따라야 하는가**는 ADR 레벨에
|
||||||
|
없었음.
|
||||||
|
|
||||||
|
**ADR-0054로 확장됨**: D5의 단일 구성 규칙에 세 번째 패턴이 추가된다 —
|
||||||
|
*평가 bench*(예: `milestone-1h-*`)는 여러 구성을 구동하고, 구성별 자체 엔진을
|
||||||
|
빌드하며, D4를 만족시키기 위해 sentinel 텐서를 제출한다.
|
||||||
|
|
||||||
|
## First action (제일 처음에 하는 일)
|
||||||
|
|
||||||
|
`kernbench.benches` 패키지가 임포트되면 `__init__.py` 가 즉시
|
||||||
|
`_eager_import_and_audit(__path__, __name__)` 를 호출한다. 이 함수의 첫 일은
|
||||||
|
패키지 디렉터리 안의 모든 형제 모듈을 `pkgutil.iter_modules(__path__)`로 나열한
|
||||||
|
뒤, 다음 두 조건을 만족하지 않는 모듈을 모두 `importlib.import_module(...)`로
|
||||||
|
**즉시 로드**하는 것이다:
|
||||||
|
|
||||||
|
- 이름이 `registry` 인 경우 (인프라 자체)
|
||||||
|
- 이름이 `_` 로 시작하는 경우 (helper 모듈)
|
||||||
|
|
||||||
|
임포트 시점에 각 모듈 안의 `@bench(name=..., description=...)` 데코레이터가
|
||||||
|
실행되어 `_PENDING` 리스트에 `(name, description, fn)` 튜플이 append 되고,
|
||||||
|
`_REGISTERED_MODULES` 셋에 `fn.__module__` 가 추가된다.
|
||||||
|
|
||||||
|
전체 임포트가 끝나면 `_audit_modules(imported, _REGISTERED_MODULES)` 가 호출되어,
|
||||||
|
**임포트는 되었지만 @bench를 한 번도 호출하지 않은 모듈**이 있으면
|
||||||
|
`RuntimeError("Bench module(s) missing @bench decorator: ...")` 가 즉시 발생한다.
|
||||||
|
이 audit이 통과한 시점에 인덱스 할당은 아직 일어나지 않은 상태이며, 첫
|
||||||
|
`list_all()` / `resolve(...)` 호출 시 `_finalize()` 가 이름 알파벳 정렬 순으로
|
||||||
|
1-based index를 부여한다.
|
||||||
|
|
||||||
|
즉, **bench 인프라의 첫 일은 "패키지 디렉터리의 모든 비-helper 모듈을 임포트
|
||||||
|
하고, 각 모듈이 최소 한 번 @bench를 호출했는지 감사하는 것"** 이다.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
`src/kernbench/benches/` 는 현재 8개의 bench 모듈을 보유한다 (`ccl_allreduce`,
|
||||||
|
`gemm_single_pe`, `gpt3_qkv`, `ipcq_allreduce`, `matmul_composite`, `qkv_gemm`,
|
||||||
|
`qkv_gemm_multi_pe`, `va_offset_verify`). 모든 bench는 다음 통합 흐름을 따른다:
|
||||||
|
|
||||||
|
```
|
||||||
|
kernbench run --topology <T> --bench <N>
|
||||||
|
↓
|
||||||
|
cli/main.py::cmd_run
|
||||||
|
↓ resolve_topology(T) + resolve(N) + resolve_device(device_arg)
|
||||||
|
↓
|
||||||
|
runtime_api/bench_runner.py::run_bench(topology, bench_fn, device, engine_factory)
|
||||||
|
↓ engine_factory(topology, device) → GraphEngine
|
||||||
|
↓ RuntimeContext(engine, target_device, correlation_id, spec)
|
||||||
|
↓
|
||||||
|
bench_fn(ctx) ← bench가 정의한 run(torch) 가 호출됨
|
||||||
|
↓ ctx.empty/zeros/from_numpy/launch/distributed.* 등을 통해 submit
|
||||||
|
↓
|
||||||
|
ctx.wait_all() ← 미완료 핸들이 있으면 drain
|
||||||
|
↓
|
||||||
|
BenchResult(completion, correlation_id, trace, traces, engine)
|
||||||
|
```
|
||||||
|
|
||||||
|
ADR-0010 은 CLI 표면만 다루고 (`run/list/probe/web`), ADR-0007 은 runtime API ↔
|
||||||
|
sim_engine 책임 경계만 다룬다. 정작 "새 bench 파일을 추가하려면 어떤 모양으로
|
||||||
|
써야 하는가"는 코드 컨벤션만으로 추적해야 한다. 결과적으로:
|
||||||
|
|
||||||
|
- @bench 데코레이터의 호출 규약 (kebab-case 이름, non-empty description)이
|
||||||
|
코드에만 존재.
|
||||||
|
- bench 함수 시그너처 (`def run(torch)`) 가 사실상 컨벤션인데, CLI 디스패치 측이
|
||||||
|
`spec.run` 을 호출한다는 사실로 강제되고 있음.
|
||||||
|
- 신규 bench 추가자가 "helper 모듈은 `_` 접두로 분리해야 한다"는 것을 audit
|
||||||
|
RuntimeError를 받아본 뒤에야 학습.
|
||||||
|
- single-device 컨벤션 (CLAUDE.md Part 2 CLI Semantics)이 bench 작성자 관점에서
|
||||||
|
어디까지 적용되는지 (CCL 멀티-SIP bench는 예외인가?) 명확하지 않음.
|
||||||
|
|
||||||
|
이 ADR이 이런 모호함을 한 곳에 정리한다.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### D1. @bench 데코레이터 계약
|
||||||
|
|
||||||
|
```python
|
||||||
|
from kernbench.benches.registry import bench
|
||||||
|
|
||||||
|
@bench(name="my-bench", description="Short, complete-sentence description.")
|
||||||
|
def run(torch):
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
- `name`: kebab-case 문자열. 정규식 `^[a-z][a-z0-9]*(-[a-z0-9]+)*$` 통과 필요.
|
||||||
|
소문자/숫자/대시만 허용; 밑줄(`_`) 금지; 알파벳으로 시작.
|
||||||
|
- `description`: non-empty 문자열 (strip 후 길이 > 0). CLI `list` 출력에 그대로
|
||||||
|
표시됨.
|
||||||
|
- 데코레이터는 **fn을 변형 없이 반환**한다 — 즉 직접 호출도 가능. 부수효과로
|
||||||
|
`_PENDING` 에 등록만 추가한다.
|
||||||
|
|
||||||
|
위 두 규칙 위반은 즉시 `ValueError`. duplicate name은 `_finalize()` 시점에
|
||||||
|
`RuntimeError("duplicate bench name: ...")` 로 잡힌다.
|
||||||
|
|
||||||
|
### D2. 모듈 파일 컨벤션
|
||||||
|
|
||||||
|
`src/kernbench/benches/<slug>.py` 는 다음 중 하나여야 한다:
|
||||||
|
|
||||||
|
- **bench 모듈**: 최상위 임포트 경로에서 적어도 한 번 `@bench(...)` 가 실행되어
|
||||||
|
최소 하나의 bench를 등록한다.
|
||||||
|
- **helper 모듈**: 파일명이 `_` 로 시작 (예: `_shared_helpers.py`). `iter_modules`
|
||||||
|
순회에서 스킵된다.
|
||||||
|
|
||||||
|
audit (`_audit_modules`) 는 helper가 아닌데도 @bench를 호출하지 않은 모듈을
|
||||||
|
허용하지 않는다. 의도된 결과: 새 파일을 `benches/` 에 추가하기만 하면 자동
|
||||||
|
등록되며, helper와의 구분은 **파일명 접두사** 하나로 명확하게 표시된다.
|
||||||
|
|
||||||
|
### D3. bench 함수 시그너처는 `def run(torch)` 다
|
||||||
|
|
||||||
|
데코레이터는 함수 이름을 강제하지 않지만, **CLI 디스패치는 `spec_entry.run`
|
||||||
|
(즉 데코레이트된 callable) 을 호출**한다. 따라서 컨벤션은:
|
||||||
|
|
||||||
|
- 함수 이름: `run`. 다른 이름으로 데코레이트해도 동작은 하지만 readability /
|
||||||
|
grep-ability 측면에서 항상 `run`.
|
||||||
|
- 인자: 단일 위치 인자 `torch`. 실제로는 `RuntimeContext` 인스턴스이며 PyTorch
|
||||||
|
스타일의 namespace (zeros/empty/launch/distributed/...)를 노출한다 (ADR-0024 D3).
|
||||||
|
- 반환값: 임의 (`Any`). 현재 `run_bench` 는 반환값을 무시하고 `ctx.handles()` /
|
||||||
|
`engine.get_completion()` 로 완료를 추적한다.
|
||||||
|
|
||||||
|
`torch` 이름은 PyTorch 호환 idiom을 흉내내기 위함이며, 실제로 PyTorch 모듈이
|
||||||
|
들어오는 것은 아니다 (ADR-0024 의 "rank = SIP" launcher 컨벤션과 정렬).
|
||||||
|
|
||||||
|
### D4. bench는 최소 한 번의 submit을 수행해야 한다
|
||||||
|
|
||||||
|
`run_bench` 는 `ctx.handles()` 가 비어 있는 경우 BenchResult.completion 을
|
||||||
|
`ok=False, error_code="NO_REQUESTS"` 로 반환한다. 따라서 의미 있는 bench는
|
||||||
|
다음 중 하나 이상을 호출해야 한다:
|
||||||
|
|
||||||
|
- 텐서 생성 API: `torch.zeros(...)`, `torch.empty(...)` — 내부적으로
|
||||||
|
`MmuMapMsg` 와 (zeros 의 경우) `MemoryWriteMsg` 가 submit 됨.
|
||||||
|
- 커널 실행 API: `torch.launch(name, fn, *args)` — `KernelLaunchMsg` 를 SIP 별로
|
||||||
|
submit.
|
||||||
|
- (예외) 빈 placeholder bench: `ipcq_allreduce.py` 처럼 `print(...)` 만 하는
|
||||||
|
스텁은 NO_REQUESTS 결과를 받게 됨. CI 측에서 placeholder임을 인지하고 별도
|
||||||
|
처리하는 것을 가정한다.
|
||||||
|
|
||||||
|
### D5. 단일-디바이스 컨벤션 + 멀티-SIP 예외 (ADR-0024/0027)
|
||||||
|
|
||||||
|
CLAUDE.md Part 2 CLI Semantics 가 명시하는 **"benchmarks MUST remain
|
||||||
|
single-device"** 컨벤션은 다음과 같이 해석된다:
|
||||||
|
|
||||||
|
- **일반 bench (single-SIP 사용)**: `dp = DPPolicy(...)` 로 텐서 placement를
|
||||||
|
정의하고 `torch.launch(...)` 로 커널 발사. SIP 인덱스는 `--device` 가
|
||||||
|
결정한다 (CLI 측 책임).
|
||||||
|
- **CCL bench (멀티-SIP 사용)**: 예외적으로 `torch.distributed.init_process_group
|
||||||
|
(backend="ahbm")` + `torch.multiprocessing.spawn(_worker, ..., nprocs=ws)` 로
|
||||||
|
rank = SIP 패턴 (ADR-0024 D3) 을 따른다. `--device` 는 무시되며 (또는
|
||||||
|
`all` 로 가정), 각 spawned worker가 `torch.ahbm.set_device(rank)` 로 자신의
|
||||||
|
SIP를 바인딩한다.
|
||||||
|
|
||||||
|
이 두 패턴 외의 멀티-디바이스 호출 (예: 한 bench 함수가 동일 process에서 여러
|
||||||
|
SIP을 직접 launch) 은 본 ADR이 금지한다. CLI 가 `--device all` 로 호출되어도
|
||||||
|
bench는 한 번만 실행되며, 그 안에서 멀티-SIP을 다루려면 D5의 두 번째 패턴을
|
||||||
|
사용한다.
|
||||||
|
|
||||||
|
### D6. 이름·인덱스 해석 (`resolve`)
|
||||||
|
|
||||||
|
`resolve(identifier: str)` 는 다음 순서로 BenchSpec을 반환한다:
|
||||||
|
|
||||||
|
1. `identifier.isdigit()` → 정수 변환 후 `_REGISTRY` 의 entries에서 `index ==`
|
||||||
|
인 spec 반환. 없으면 `ValueError("No bench with index ..."`)`.
|
||||||
|
2. `identifier in _REGISTRY` → 직접 lookup.
|
||||||
|
3. 그 외 → `ValueError("Unknown bench ...")`.
|
||||||
|
|
||||||
|
빈/공백 identifier 는 `ValueError("bench identifier must be a non-empty string.")`.
|
||||||
|
|
||||||
|
CLI 는 `--bench` 의 인자를 그대로 `resolve` 에 넘긴다. 따라서 사용자는
|
||||||
|
`kernbench run --bench gemm-single-pe` 또는 `kernbench run --bench 2` 형식 모두
|
||||||
|
사용 가능.
|
||||||
|
|
||||||
|
### D7. 인덱스는 안정 API가 아니다
|
||||||
|
|
||||||
|
`_finalize()` 가 `_PENDING` 을 **이름 알파벳 정렬** 후 1-based index를 부여하므로,
|
||||||
|
새 bench 가 추가되면 기존 bench의 index가 밀릴 수 있다. 따라서:
|
||||||
|
|
||||||
|
- 사람-친화적 인터랙티브 사용: 인덱스 OK.
|
||||||
|
- 스크립트 / CI 자동화: 반드시 이름을 사용한다.
|
||||||
|
|
||||||
|
이 사실은 `registry.py` 모듈 docstring 에 명시되어 있다.
|
||||||
|
|
||||||
|
### D8. RuntimeContext 가 bench에 노출하는 표면
|
||||||
|
|
||||||
|
bench 함수가 `torch` 파라미터를 통해 정상적으로 사용할 수 있는 표면:
|
||||||
|
|
||||||
|
- **텐서 생성**: `torch.empty(shape, dtype=..., dp=DPPolicy(...), name=...)`,
|
||||||
|
`torch.zeros(...)`, `torch.from_numpy(arr)`. 모두 host-side 메타 + 디바이스
|
||||||
|
배포 (MmuMap + MemoryWrite) 를 submit 한다.
|
||||||
|
- **커널 발사**: `torch.launch(kernel_name, kernel_fn, *args)` —
|
||||||
|
`(Tensor, int, float)` 위치 인자를 `TensorArg` / `ScalarArg` 로 변환하여
|
||||||
|
SIP 별 `KernelLaunchMsg` 발행 후 drain.
|
||||||
|
- **동기화**: `torch.wait(handle)`, `torch.wait_all()` (run_bench 가 자동 호출).
|
||||||
|
- **분산**: `torch.distributed.init_process_group(backend="ahbm")`,
|
||||||
|
`torch.distributed.get_world_size()`, `torch.distributed.all_reduce(t, op=...)`
|
||||||
|
(ADR-0024/0027).
|
||||||
|
- **멀티-프로세스 (rank=SIP)**: `torch.multiprocessing.spawn(_worker, ..., nprocs=ws)`
|
||||||
|
(ADR-0024 D3 / ADR-0027).
|
||||||
|
- **디바이스 바인딩**: `torch.ahbm.set_device(rank)` 또는
|
||||||
|
`torch.accelerator.set_device_index(rank)` (둘 다 같은 namespace를 가리킴).
|
||||||
|
- **IPCQ 설치**: `torch.install_ipcq(algorithm=..., ccl_yaml=...)` (ADR-0023 D10).
|
||||||
|
- **스펙 조회**: `torch.spec` — 토폴로지 빌더가 만든 dict (시스템·cube_mesh·HBM
|
||||||
|
파라미터 등). bench가 toplogy.yaml 파라미터에 의존하지 않게 짜기 위함.
|
||||||
|
|
||||||
|
bench는 위에 열거되지 않은 RuntimeContext 의 private 멤버 (`_handles`, `_traces`,
|
||||||
|
`_allocators` 등) 에 직접 접근해선 안 된다. ADR-0007 의 layer boundary 정신과
|
||||||
|
정렬: bench → runtime API → sim_engine 한 방향만 허용.
|
||||||
|
|
||||||
|
### D9. 환경 변수로 파라미터화는 허용된다
|
||||||
|
|
||||||
|
`matmul_composite.py` 처럼 `os.environ.get("MATMUL_M", ...)` 등으로 bench
|
||||||
|
파라미터를 외부에서 주입하는 패턴은 허용한다. 이유:
|
||||||
|
|
||||||
|
- bench 함수 시그너처는 D3 에 의해 `def run(torch)` 로 고정되어 있어 위치/키워드
|
||||||
|
인자로 파라미터를 받기 곤란.
|
||||||
|
- 환경 변수 패턴은 `MATMUL_VARIANT` 같은 운영-시 스윕을 위한 자연스러운 hook.
|
||||||
|
- `scripts/gemm_sweep.py` 같은 외부 드라이버 (ADR-0044) 가 이 hook을 사용한다.
|
||||||
|
|
||||||
|
단, 환경 변수가 bench의 동작을 바꾼다면 모듈 docstring 에 모든 변수를 명시할 것
|
||||||
|
(matmul_composite.py 가 그 예시).
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
### A1. 명시적 manifest 파일 (YAML)에 bench 목록 두기
|
||||||
|
|
||||||
|
기각. @bench 데코레이터 + audit 패턴은 "파일 추가 = 자동 등록" 을 보장하여 신규
|
||||||
|
bench 작성자의 인지 비용을 한 곳 (파일 작성)으로 집중시킨다. 별도 manifest는
|
||||||
|
유지보수 측에서 drift 위험이 크고, helper 분리는 이미 `_` 접두로 명확하다.
|
||||||
|
|
||||||
|
### A2. bench 함수 이름을 데코레이터 인자로 받기 (`@bench(name=..., entry="run_xxx")`)
|
||||||
|
|
||||||
|
기각. 디스패치 측에서 `spec.run` 하나만 호출하면 되는 단순함을 깬다. `run` 컨벤션
|
||||||
|
하나로 충분하며, 변종이 필요하면 같은 모듈에 여러 함수를 등록하면 된다 (각각
|
||||||
|
@bench 데코레이트).
|
||||||
|
|
||||||
|
### A3. CCL bench를 위한 별도 `@multi_device_bench` 데코레이터
|
||||||
|
|
||||||
|
기각. D5에서 명시한 두 패턴 (single + ADR-0024 멀티-SIP) 만으로 현재 8개 bench가
|
||||||
|
모두 표현 가능. 별도 데코레이터는 디스패치 측에서 분기를 강제하여 복잡도를 늘리며,
|
||||||
|
멀티-SIP 사용 의도는 bench 함수 본문의 `init_process_group(...)` 호출로 충분히
|
||||||
|
드러난다.
|
||||||
|
|
||||||
|
### A4. 인덱스를 안정 API로 만들기 (등록 순서 / explicit index= 인자)
|
||||||
|
|
||||||
|
기각. D7에서 명시한 trade-off — 사용자 친화성 (알파벳 정렬된 인덱스가 list 출력
|
||||||
|
에서 자연스럽게 1, 2, 3...) 우선. 스크립트는 이름으로 지정하면 충분.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- "bench 추가 방법" 이 한 ADR로 정리됨 → 신규 작성자가 코드 grep 없이 D1-D3,
|
||||||
|
D8 만 따르면 됨.
|
||||||
|
- helper 모듈을 `_` 접두로 분리하는 패턴이 ADR-level에서 정당화되어, 향후
|
||||||
|
`benches/_*.py` 식의 공유 helper 작성이 자유로워짐.
|
||||||
|
- CLAUDE.md Part 2 CLI Semantics 의 single-device 컨벤션이 멀티-SIP CCL bench
|
||||||
|
와 모순되지 않음을 D5 가 명시 — 둘은 직교한다.
|
||||||
|
- ADR-0044 (GEMM eval harness) 의 `scripts/gemm_sweep.py` 가 환경 변수 hook을
|
||||||
|
사용하는 근거 (D9) 가 본 ADR에 굳어짐.
|
||||||
|
- 인덱스가 불안정함 (D7) 이 명시되어, CI 측 `kernbench run --bench 3` 같은
|
||||||
|
코드는 본 ADR 수락 직후 점검 대상.
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
# ADR-0046: TLContext — Kernel-side `tl.*` API Contract
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted (2026-05-22).
|
||||||
|
|
||||||
|
`src/kernbench/triton_emu/` 의 `TLContext` 가 노출하는 `tl.*` primitive
|
||||||
|
집합과 그 의미, 그리고 두 실행 모드 (command-list / greenlet runner) 의
|
||||||
|
계약을 명시한다. ADR-0014/0020 가 PE 파이프라인과 2-pass 실행 모델을
|
||||||
|
정의하나, **bench 의 kernel 함수가 호출하는 `tl.*` 표면 자체**는 ADR-level
|
||||||
|
에 정리되어 있지 않았다.
|
||||||
|
|
||||||
|
## First action (제일 처음에 하는 일)
|
||||||
|
|
||||||
|
`TLContext(pe_id, num_programs, dispatch_cycles, runner, cube_id, num_cubes,
|
||||||
|
scratch_base, scratch_size)` 생성 시 가장 먼저 다음 6개 필드를 초기화한다:
|
||||||
|
|
||||||
|
- `self._pe_id`, `self._num_programs`, `self._cube_id`, `self._num_cubes` —
|
||||||
|
`tl.program_id` / `tl.num_programs` 가 반환할 값.
|
||||||
|
- `self._dispatch_cycles` — 모든 `tl.*` API 호출 시작에서 자동으로 발행될
|
||||||
|
`PeCpuOverheadCmd(cycles)` 의 cycle 수.
|
||||||
|
- `self._runner` — `KernelRunner` 인스턴스 (있으면 greenlet 모드, 없으면
|
||||||
|
command-list 모드).
|
||||||
|
- `self._commands: list[PeCommand] = []` — command-list 모드에서 누적할
|
||||||
|
command 시퀀스.
|
||||||
|
- `self._handle_counter = 0`, `self._completion_counter = 0` — 새 TensorHandle /
|
||||||
|
CompletionHandle id 생성용.
|
||||||
|
- `self._scratch_base`, `self._scratch_size`, `self._scratch_cursor = 0` —
|
||||||
|
PE-로컬 scratch 영역 (math/dot/composite 의 output handle 주소 할당용).
|
||||||
|
|
||||||
|
즉, **TLContext 의 첫 일은 "이 kernel 인스턴스가 어디서 (sip/cube/pe) 어떤
|
||||||
|
규모 (num_programs/num_cubes) 로 실행되며, 어느 모드 (runner 유무) 로
|
||||||
|
명령을 발사할지 메타데이터를 채우는 것"** 이다. 이 시점에 SimPy event 는
|
||||||
|
없으며 command 도 발사되지 않는다.
|
||||||
|
|
||||||
|
런타임 첫 동작은 kernel 함수가 `tl.<api>()` 를 처음 호출할 때 발생한다.
|
||||||
|
모든 `tl.*` API 의 표준 entry 동작은:
|
||||||
|
|
||||||
|
1. `self._emit_dispatch_overhead()` 호출 — `dispatch_cycles > 0` 인 경우
|
||||||
|
`PeCpuOverheadCmd(dispatch_cycles)` 를 즉시 `_emit`.
|
||||||
|
2. API 별 처리 (TensorHandle 생성, command 구성).
|
||||||
|
3. `self._emit(cmd)` — runner 모드면 greenlet.switch 로 SimPy 측에 cmd 전달,
|
||||||
|
아니면 `self._commands` 에 append.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
`tl.*` 표면은 `TLContext` 가 노출하는 메소드들로 구성되며, kernel 함수가
|
||||||
|
받는 `tl` 매개변수가 이 객체다. 사용자(bench 작성자) 입장에서 보이는
|
||||||
|
contract:
|
||||||
|
|
||||||
|
- 어떤 primitive 가 있는가
|
||||||
|
- 각 primitive 가 어떤 데이터 흐름을 발생시키는가 (DMA / compute / IPCQ /
|
||||||
|
metadata-only)
|
||||||
|
- TensorHandle 의 `space` 와 `addr` 가 어떻게 결정되는가
|
||||||
|
- command-list 모드와 greenlet 모드의 차이
|
||||||
|
|
||||||
|
ADR-0014 (PE pipeline) 가 PE_SCHEDULER 가 받는 PeCommand 들을 정의하나,
|
||||||
|
`tl.*` 가 이들을 어떻게 emit 하는지는 코드 컨벤션에만 존재한다. 또한
|
||||||
|
ADR-0020 (2-pass data execution) 가 greenlet 모드의 존재를 D3 에서
|
||||||
|
언급하나, runner / non-runner 두 경로의 시그너처 차이 (return value 처리)
|
||||||
|
는 ADR-level 에 명시되어 있지 않다. 이 ADR 이 그 빈자리를 채운다.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### D1. `tl` 매개변수는 `TLContext` 인스턴스다
|
||||||
|
|
||||||
|
bench 의 kernel 함수는 다음 시그너처를 따른다:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _kernel(arg1, arg2, ..., tl, **kwargs):
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
`tl` 의 정체는 `kernbench.triton_emu.tl_context.TLContext` 인스턴스이다.
|
||||||
|
real Triton 의 `triton.language` 모듈을 흉내내기 위한 이름이며, real
|
||||||
|
Triton 모듈이 들어오는 것은 아니다.
|
||||||
|
|
||||||
|
kernel 함수는 일반 Python 함수이며 `yield` / `async` 가 없다. `tl.*`
|
||||||
|
호출이 SimPy event 를 발생시키지만, 호출자(kernel) 쪽에서는 동기 호출처럼
|
||||||
|
보인다 — greenlet 모드에서 KernelRunner 가 SimPy ↔ kernel 사이를 중계
|
||||||
|
하기 때문 (ADR-0020 D3).
|
||||||
|
|
||||||
|
### D2. 두 실행 모드 — command-list / greenlet runner
|
||||||
|
|
||||||
|
- **command-list 모드 (`runner is None`)**: `tl.*` 호출이 `self._commands`
|
||||||
|
리스트에 PeCommand 를 누적. DMA / GEMM / Math 가 실제 SimPy 시간을
|
||||||
|
소비하지 않으며, return value 가 metadata-only TensorHandle (data=None) 다.
|
||||||
|
이후 PE_SCHEDULER / sim_engine 가 command 시퀀스를 시간상 재생.
|
||||||
|
|
||||||
|
- **greenlet runner 모드 (`runner is not None`)**: `tl.*` 호출이
|
||||||
|
`self._emit(cmd)` 를 통해 `runner.switch_to_simpy(cmd)` 로 부모 greenlet
|
||||||
|
(SimPy) 으로 컨트롤을 넘김. 부모는 cmd 를 컴포넌트에 분배하여 SimPy 시간을
|
||||||
|
소비한 뒤, DMA read 의 경우 실제 numpy 데이터를 반환. kernel 은 그
|
||||||
|
결과를 받아 다음 line 으로 진행 (ADR-0020 D3 의 데이터 인지 실행 모델).
|
||||||
|
|
||||||
|
mode 선택은 KernelRunner 인스턴스를 TLContext 에 주입하는지 여부로 결정
|
||||||
|
되며, `tl.*` 메소드들은 이 차이를 인지하지 않고 `_emit()` 헬퍼를 통해
|
||||||
|
일관되게 동작한다.
|
||||||
|
|
||||||
|
### D3. Primitive 카테고리
|
||||||
|
|
||||||
|
#### D3.1. Reference (no DMA, metadata only)
|
||||||
|
|
||||||
|
- `tl.ref(ptr, shape, dtype="f16") -> TensorHandle`: HBM 데이터를 참조하는
|
||||||
|
핸들만 만들고 DMA 는 발행하지 않음. composite scheduler 가 per-tile 로
|
||||||
|
스트리밍할 때 사용 (예: GEMM 의 b 피연산자).
|
||||||
|
|
||||||
|
#### D3.2. Data movement (blocking, DMA engine)
|
||||||
|
|
||||||
|
- `tl.load(ptr, shape, dtype="f16") -> TensorHandle`: HBM → 결과 핸들.
|
||||||
|
`DmaReadCmd` 발행. greenlet 모드에서는 결과 핸들의 `.data` 에 실제
|
||||||
|
numpy 배열 첨부; command-list 모드에서는 placeholder. 반환 핸들의
|
||||||
|
`space="hbm"`, `pinned=True`.
|
||||||
|
- `tl.store(ptr, handle) -> None`: TCM → HBM. `DmaWriteCmd` 발행. greenlet
|
||||||
|
모드에서는 `handle.data` 가 있을 때만 `_store.write("hbm", ptr, data)` 를
|
||||||
|
먼저 호출 (visibility = issue time, ADR-0020 D3).
|
||||||
|
|
||||||
|
#### D3.3. GEMM / compute (blocking)
|
||||||
|
|
||||||
|
- `tl.dot(a, b) -> TensorHandle`: `a @ b`. 두 피연산자는 TCM 이어야 하며,
|
||||||
|
shape (M,K) × (K,N) → (M,N). `GemmCmd` 발행, output handle 은
|
||||||
|
`_make_compute_out(shape, dtype)` 로 PE-로컬 scratch 에 할당.
|
||||||
|
- `tl.composite(op, a, b=None, out_ptr=0, math_op=None, epilogue=None,
|
||||||
|
acc_dtype=None, tile_shape=None) -> CompletionHandle`: 비차단(non-blocking)
|
||||||
|
tiled pipeline. `CompositeCmd` 발행. `epilogue` 는 dict list, 각 dict 는
|
||||||
|
`"op"` 키 + op-specific 필드 + 옵션 `"scope"` (k_tile / output_tile);
|
||||||
|
unknown op 나 missing field 는 즉시 ValueError. 반환된 CompletionHandle 은
|
||||||
|
`tl.wait(h)` 로 동기화.
|
||||||
|
|
||||||
|
#### D3.4. Math: unary (blocking)
|
||||||
|
|
||||||
|
- `tl.exp(x)`, `tl.log(x)`, `tl.sqrt(x)`, `tl.abs(x)`, `tl.sigmoid(x)`,
|
||||||
|
`tl.cos(x)`, `tl.sin(x)` — 모두 `MathCmd(op=<name>, inputs=(x,), out=)`
|
||||||
|
발행. `out` 은 동일 shape/dtype 의 scratch 할당.
|
||||||
|
|
||||||
|
#### D3.5. Math: binary (blocking)
|
||||||
|
|
||||||
|
- `tl.maximum(a, b)`, `tl.minimum(a, b)` — `_binary_math`.
|
||||||
|
- `tl.fma(a, b, c)` — `a*b + c`. inputs 3개.
|
||||||
|
- `tl.clamp(x, min, max)` — `MathCmd(op="clamp", inputs=(x, min, max))`.
|
||||||
|
- `tl.where(cond, a, b)` — `MathCmd(op="where", inputs=(cond, a, b))`.
|
||||||
|
- `tl.softmax(x, axis=-1)` — 단일 MathCmd(op="softmax") 로 시간 회계는
|
||||||
|
한 번에. Phase 2 DataExecutor 가 canonical (x-max → exp → sum → div) 로
|
||||||
|
expand 한다.
|
||||||
|
|
||||||
|
#### D3.6. Reduction (blocking)
|
||||||
|
|
||||||
|
- `tl.sum(x, axis)`, `tl.max(x, axis)`, `tl.min(x, axis)` — 해당 axis 의
|
||||||
|
크기를 1 로 줄인 output handle 을 반환. `MathCmd(op=<name>, inputs=(x,),
|
||||||
|
out=, axis=axis)` 발행.
|
||||||
|
|
||||||
|
#### D3.7. Index / scalar (PE_CPU, no engine)
|
||||||
|
|
||||||
|
- `tl.program_id(axis=0) -> int`: `axis==0` → pe_id (cube-local PE 인덱스),
|
||||||
|
`axis==1` → cube_id (ADR-0022).
|
||||||
|
- `tl.num_programs(axis=0) -> int`: `axis==0` → num_programs (cube 당
|
||||||
|
PE 수), `axis==1` → num_cubes.
|
||||||
|
- `tl.arange(start, end, dtype="i32") -> TensorHandle`: TCM 의 인덱스
|
||||||
|
range. command 발사 없이 metadata 만.
|
||||||
|
- `tl.zeros(shape, dtype="f16") -> TensorHandle`, `tl.full(shape, value,
|
||||||
|
dtype="f16") -> TensorHandle`: TCM 에 placeholder. command 발사 없음.
|
||||||
|
|
||||||
|
#### D3.8. Scalar helpers (no command, no engine)
|
||||||
|
|
||||||
|
- `TLContext.cdiv(a, b) -> int` (static): ceiling division
|
||||||
|
`-(-a // b)`. real Triton 의 `tl.cdiv` 모방.
|
||||||
|
|
||||||
|
#### D3.9. Metadata-only (no compute, no DMA)
|
||||||
|
|
||||||
|
- `tl.trans(x) -> TensorHandle`: shape 의 마지막 두 dim 을 swap 한 새
|
||||||
|
핸들. 같은 addr/data 를 공유, command 발사 없음.
|
||||||
|
|
||||||
|
#### D3.10. IPCQ (CCL) primitives (ADR-0023 D4)
|
||||||
|
|
||||||
|
- `tl.send(dir, src=None, *, src_addr=None, nbytes=None, shape=None,
|
||||||
|
dtype="f16", space="tcm") -> None`: blocking send. handle 형태 또는
|
||||||
|
raw 주소 형태 둘 다 허용. `IpcqSendCmd` 발행. handle 의 `.data` 스냅샷이
|
||||||
|
명령에 실리는 경우, recv 측에서 받은 데이터의 race 회피.
|
||||||
|
- `tl.recv(dir=None, shape=(), dtype="f16", space="tcm", dst_addr=None,
|
||||||
|
dst_space=None) -> TensorHandle`: blocking recv. `dst_addr/dst_space`
|
||||||
|
둘 다 주면 "copy_to_dst" 모드, 아니면 "return_slot" 모드. greenlet
|
||||||
|
모드에서 핸들의 `.data` 에 실제 데이터 첨부.
|
||||||
|
- `tl.recv_no_consume(dir=None, shape=(), dtype="f16") -> TensorHandle`:
|
||||||
|
**DIAGNOSTIC ONLY**. recv blocking 동기화는 그대로 적용되나 slot-read
|
||||||
|
latency (slot-IO + PE↔bank fabric drain) 는 건너뛴다. pe2pe overview
|
||||||
|
플롯에서 `tl.store` 와의 apples-to-apples 비교용. production kernel 은
|
||||||
|
사용 금지 — `consume=False` 라는 별도 명령 분기로 격리되어 있어 실수
|
||||||
|
flag 가 작동하지 않는다.
|
||||||
|
- `tl.recv_async(dir, shape=(), dtype="f16") -> RecvFuture`: non-blocking
|
||||||
|
recv. `RecvFuture` 를 반환; 이후 `tl.wait(future)` 로 결과 수령.
|
||||||
|
|
||||||
|
#### D3.11. Composite + control
|
||||||
|
|
||||||
|
- `tl.composite(...)`: D3.3 에서 설명.
|
||||||
|
- `tl.wait(handle=None)`: `CompletionHandle` (composite) 또는 `RecvFuture`
|
||||||
|
(async recv) 또는 `None` (모든 pending composite) 대기.
|
||||||
|
- `tl.cycles(n)`: PE_CPU scalar 실행 overhead 를 명시적으로 선언.
|
||||||
|
`PeCpuOverheadCmd(cycles=n)` 발행.
|
||||||
|
|
||||||
|
### D4. TensorHandle 산술 연산자 — thread-local TLContext
|
||||||
|
|
||||||
|
`tl_context.py` 모듈 로드 시점에 `_enable_tensor_ops()` 가 호출되어
|
||||||
|
`TensorHandle.__add__`, `__sub__`, `__mul__`, `__truediv__` 를 patch한다.
|
||||||
|
각 연산자는 thread-local `_ctx` (모듈 변수) 에 저장된 active TLContext 의
|
||||||
|
`_binary_math` 를 호출한다.
|
||||||
|
|
||||||
|
따라서 kernel 안에서 `c = a + b` 는 `MathCmd(op="add", inputs=(a,b),
|
||||||
|
out=)` 발행 + new TensorHandle 반환 패턴과 동일하다.
|
||||||
|
|
||||||
|
active TLContext 관리:
|
||||||
|
|
||||||
|
- `TLContext._set_active(ctx)`: 현재 thread/greenlet 의 active ctx 설정.
|
||||||
|
- `TLContext._get_active()`: 조회 (없으면 RuntimeError).
|
||||||
|
- `run_kernel(kernel_fn, tl_ctx, *args, **kwargs)`: helper. 진입 시
|
||||||
|
active 설정, kernel 실행, 종료 시 None 으로 복원.
|
||||||
|
|
||||||
|
`KernelRunner` 는 매 cmd 분배 시 `_switch_kernel` 가 직접 `_set_active(tl)`
|
||||||
|
를 호출하여, 같은 thread 안의 다른 PE runner 가 active 를 덮어쓴 경우에도
|
||||||
|
복원되도록 한다.
|
||||||
|
|
||||||
|
### D5. Scratch allocator — compute output handles
|
||||||
|
|
||||||
|
`tl.dot`, `tl.exp`, `tl.add` (TensorHandle `__add__`) 등 결과를 만드는 op 는
|
||||||
|
`_make_compute_out(shape, dtype)` 를 호출하여 16-byte aligned scratch
|
||||||
|
주소를 할당한다. 이 주소는 `space="tcm"` 로 발행되며, 이후 `tl.send` /
|
||||||
|
`tl.store` 가 이 handle 을 source 로 사용할 수 있다.
|
||||||
|
|
||||||
|
`_scratch_base == 0` (command-list 모드 등) 이면 할당 주소가 0으로
|
||||||
|
반환되어 handle 은 send/store 의 source 로 사용 불가 (이 경우 `tl.load`
|
||||||
|
로 받은 핸들만 source 가 될 수 있다).
|
||||||
|
|
||||||
|
cursor 가 `_scratch_size` (default 1 MiB) 를 초과하면 RuntimeError.
|
||||||
|
cursor 는 매 kernel invocation 시작 시 0 으로 리셋되어야 하나 (현재 코드는
|
||||||
|
KernelRunner 가 새 TLContext 를 매번 생성하여 자연스럽게 리셋됨).
|
||||||
|
|
||||||
|
### D6. Dispatch overhead — `PeCpuOverheadCmd(dispatch_cycles)`
|
||||||
|
|
||||||
|
모든 non-metadata `tl.*` 호출의 entry 에서 `_emit_dispatch_overhead()` 가
|
||||||
|
호출되며 `dispatch_cycles > 0` 일 때 `PeCpuOverheadCmd(dispatch_cycles)`
|
||||||
|
를 발행한다. PE_CPU 가 명령 dispatch 자체에 소비하는 cycle 비용을
|
||||||
|
모델링하기 위함이다.
|
||||||
|
|
||||||
|
기본값:
|
||||||
|
|
||||||
|
- `TLContext.__init__` 의 `dispatch_cycles` 매개변수 기본값: 1 cycle.
|
||||||
|
- `KernelRunner` 가 만드는 TLContext: 0 cycles (greenlet 모드는 cycle
|
||||||
|
회계가 별도, ADR-0020 D3 정신).
|
||||||
|
|
||||||
|
### D7. Kernel registry (`triton_emu/registry.py`)
|
||||||
|
|
||||||
|
별도의 `_kernels: dict[str, Callable]` 가 kernel 이름 → 함수 매핑을 보유:
|
||||||
|
|
||||||
|
- `register_kernel(name, fn)`: duplicate 등록 시 ValueError.
|
||||||
|
- `get_kernel(name)`: 미등록 시 KeyError.
|
||||||
|
- `clear_registry()`: 테스트 전용.
|
||||||
|
|
||||||
|
`RuntimeContext.launch(kernel_name, kernel_fn, *args)` 가 매 호출마다
|
||||||
|
`_kernels[kernel_name] = kernel_fn` 으로 idempotent 덮어쓴다 (last call
|
||||||
|
wins). 이는 ADR-0045 D8 의 launch 동작과 정합된다.
|
||||||
|
|
||||||
|
PE_CPU 는 `KernelRef.name` 으로 registry 에서 kernel 함수를 lookup 한 뒤
|
||||||
|
KernelRunner 로 실행한다.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
### A1. tl.* 를 ADR-0014 / ADR-0020 안으로 통합
|
||||||
|
|
||||||
|
기각. ADR-0014 는 PE pipeline (PeCommand 의 sim_engine 측 소비) 를, ADR-0020
|
||||||
|
은 2-pass 실행 (Phase 1 timing / Phase 2 data) 을 다룬다. `tl.*` 는 kernel
|
||||||
|
작성자가 만나는 API 표면이라 독립 분리하는 것이 검색성·온보딩 측면에서
|
||||||
|
낫다.
|
||||||
|
|
||||||
|
### A2. command-list 모드 deprecation
|
||||||
|
|
||||||
|
기각 (현재). 단순한 unit test 와 kernel verification 에서 command-list
|
||||||
|
모드가 가볍게 동작한다. greenlet 의존성 없이 PeCommand 시퀀스를 검사할 수
|
||||||
|
있는 출입구로 유지한다. greenlet 모드만의 의미 (실데이터, Phase 2) 가
|
||||||
|
필요하면 D2 의 mode 선택으로 명시적으로 들어간다.
|
||||||
|
|
||||||
|
### A3. TensorHandle 산술 연산자 제거
|
||||||
|
|
||||||
|
기각. real Triton 의 kernel 코드 가독성을 흉내내기 위함이며 (예: `c = a +
|
||||||
|
b`), thread-local active ctx 패턴이 깔끔하게 작동 중. 명시적 `tl.add(a, b)`
|
||||||
|
도 D3.5 에 노출되어 있어, 연산자가 헷갈리면 함수형 호출로 대체 가능.
|
||||||
|
|
||||||
|
### A4. softmax 를 명시적 시퀀스 (max → exp → sum → div) 로 expand
|
||||||
|
|
||||||
|
부분 채택. `tl.softmax` 는 단일 `MathCmd(op="softmax")` 로 timing 회계는
|
||||||
|
한 번에 처리한다 (D3.5). 실 데이터 expansion 은 Phase 2 DataExecutor 가
|
||||||
|
canonical 시퀀스로 풀어준다. 즉, 시간 모델은 atomic, 데이터 모델은
|
||||||
|
expansion — 두 마리 토끼를 의도적으로 분리.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- bench 작성자가 만나는 모든 `tl.*` primitive 가 한 ADR 에 분류·정의됨.
|
||||||
|
ADR-0045 D8 의 host-side surface (torch.empty 등) 와 짝을 이루어 "kernel
|
||||||
|
안 / 밖" 양쪽 작성 가이드가 완성.
|
||||||
|
- command-list / greenlet 두 모드의 차이가 D2 에 명시되어, 새로운 `tl.*`
|
||||||
|
primitive 추가 시 `_emit()` 패턴만 따르면 양쪽 자동 호환됨.
|
||||||
|
- thread-local active ctx 패턴 (D4) 이 ADR-level 에서 정당화되어, 향후
|
||||||
|
multi-PE 동일-thread 실행 시 reset 책임이 어디인지 명확해짐
|
||||||
|
(`_switch_kernel` 가 cmd 분배 시 active 복원 — KernelRunner.run 의
|
||||||
|
contract).
|
||||||
|
- `tl.recv_no_consume` 의 진단 전용 격리(D3.10) 가 ADR 에 굳어져, 실수로
|
||||||
|
production kernel 에서 사용되는 것을 막는 layer 가 명확.
|
||||||
|
- registry (D7) 가 별도 D 항목으로 분리되어, kernel 이름 충돌 / 동적
|
||||||
|
재등록 동작의 사양이 명시.
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
# ADR-0047: AHBM CCL Backend — `torch.distributed`-compat shim
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted (2026-05-22).
|
||||||
|
|
||||||
|
`runtime_api/distributed.py` 의 `AhbmCCLBackend` + `DistributedContext` —
|
||||||
|
즉 `torch.distributed.init_process_group(backend="ahbm")` 진입점이 실제로
|
||||||
|
무엇을 설치하고 어떤 의미로 `all_reduce`/`barrier`/`get_rank` 등을
|
||||||
|
구현하는지를 명시한다. ADR-0023 D11 이 "torch.distributed compatibility"
|
||||||
|
의도를 언급하나, **backend 자체의 동작 모델**은 ADR-level 에 없었다.
|
||||||
|
|
||||||
|
## First action (제일 처음에 하는 일)
|
||||||
|
|
||||||
|
`RuntimeContext.__post_init__` 가 자동으로 `DistributedContext()` 인스턴스를
|
||||||
|
만들어 `self.distributed` 에 attach 한다. 그 시점의 첫 일은:
|
||||||
|
|
||||||
|
1. `self._backend: AhbmCCLBackend | None = None` 으로 초기화 (아직 init
|
||||||
|
되지 않은 상태).
|
||||||
|
2. `self._rank_by_greenlet: dict = {}` 로 greenlet-local rank 레지스트리
|
||||||
|
초기화 (ADR-0024 D2).
|
||||||
|
3. 호출자(RuntimeContext) 측에서 `dc._ctx_ref = self` 로 back-reference 를
|
||||||
|
심어, 이후 `init_process_group` 가 `ctx.engine` / `ctx.spec` / `ctx.launch`
|
||||||
|
에 도달할 수 있게 한다.
|
||||||
|
|
||||||
|
즉, **DistributedContext 의 첫 일은 "RuntimeContext 에 자기 자신을
|
||||||
|
back-reference 와 함께 부착하고 backend 슬롯을 비워두는 것"**. 실제 backend
|
||||||
|
설치(IPCQ install, world_size 산출, 알고리즘 모듈 로드)는 사용자 코드의
|
||||||
|
`torch.distributed.init_process_group(backend="ahbm")` 호출 시점에 비로소
|
||||||
|
일어난다.
|
||||||
|
|
||||||
|
해당 시점의 `init_process_group` 의 첫 일은:
|
||||||
|
|
||||||
|
1. `backend != "ahbm"` 이면 즉시 `ValueError("Unsupported backend ...")`.
|
||||||
|
2. `getattr(self, "_ctx_ref", None)` 가 None 이면
|
||||||
|
`RuntimeError("DistributedContext not bound to a RuntimeContext")`.
|
||||||
|
3. `self._backend = AhbmCCLBackend(torch_ctx=ctx)` — 이 생성자 안에서
|
||||||
|
ccl.yaml load + 알고리즘 모듈 import + world_size 산출 + SFR 설정 +
|
||||||
|
IPCQ install 이 모두 일어난다.
|
||||||
|
4. `self._backend._dist_ctx = self` — backend 가 거꾸로
|
||||||
|
`_rank_by_greenlet` 에 접근할 수 있게 함.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
PyTorch DDP 의 collective 호출 (`init_process_group`, `all_reduce` 등) 을
|
||||||
|
그대로 사용할 수 있게 만들어, bench 코드가 "진짜 DDP training script" 와
|
||||||
|
동일한 모습이 되도록 하는 것이 `AhbmCCLBackend` 의 목적이다 (ADR-0024 +
|
||||||
|
ADR-0027 의 launcher 모델과 정렬).
|
||||||
|
|
||||||
|
이 backend 가 책임지는 것:
|
||||||
|
|
||||||
|
- `init_process_group` 시점에 **IPCQ neighbor table 을 한 번 설치** (real
|
||||||
|
NCCL communicator creation 과 유사).
|
||||||
|
- `all_reduce(tensor, op="sum")` 호출 시 **설정된 algorithm 의 kernel 함수
|
||||||
|
를 `ctx.launch(...)` 로 발사**.
|
||||||
|
- `get_world_size` / `get_rank` 를 greenlet-local rank 레지스트리와
|
||||||
|
ccl.yaml/topology 로부터 일관되게 답함.
|
||||||
|
|
||||||
|
ADR-0023 D10 (IPCQ install plan), ADR-0024 (SIP launcher) 가 부분적으로
|
||||||
|
이를 다루나, **`AhbmCCLBackend` 자체의 책임 범위와 의사결정 순서**는
|
||||||
|
어디에도 명시되어 있지 않다. 본 ADR 이 채운다.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### D1. backend 는 `init_process_group(backend="ahbm")` 시점에만 생성된다
|
||||||
|
|
||||||
|
`DistributedContext` 는 `__init__` 시점에 `_backend = None` 으로 시작한다.
|
||||||
|
backend 객체는 사용자가 `dist.init_process_group(backend="ahbm")` 를
|
||||||
|
호출하기 전까지 존재하지 않으며, 그 외 API (`is_initialized`,
|
||||||
|
`get_world_size`, `all_reduce`, `barrier`) 가 backend 가 None 인 채로
|
||||||
|
호출되면 `RuntimeError("Default process group has not been initialized...")`
|
||||||
|
를 던진다 (`_ensure_initialized` 헬퍼).
|
||||||
|
|
||||||
|
`backend != "ahbm"` 은 즉시 `ValueError`. 다른 backend 명 (nccl, gloo
|
||||||
|
등) 은 인식하지 않는다.
|
||||||
|
|
||||||
|
### D2. world_size 산출 우선순위 — algorithm > defaults > topology
|
||||||
|
|
||||||
|
`AhbmCCLBackend._resolve_world_size` (ADR-0024 D1) 의 결정 순서:
|
||||||
|
|
||||||
|
1. `ccl.yaml` 의 algorithm entry 에 `world_size` 가 있으면 그 값.
|
||||||
|
2. `defaults.world_size` 가 있으면 그 값.
|
||||||
|
3. 둘 다 없으면 `spec.system.sips.count` (=topology 의 SIP 개수).
|
||||||
|
|
||||||
|
기본 의미는 **rank = SIP** (ADR-0024). cube/PE-level parallelism 은 각
|
||||||
|
rank 안에서 DPPolicy 로 표현되며 world_size 에 영향을 주지 않는다. 명시적
|
||||||
|
`ccl.yaml` 의 world_size override 가 있으면 legacy "rank = flat PE 인덱스"
|
||||||
|
테스트 경로를 위해 그대로 존중된다.
|
||||||
|
|
||||||
|
`init_process_group(world_size=..., rank=...)` 의 사용자 인자는 **수신하나
|
||||||
|
무시**된다 (real PyTorch 의 `RANK` / `WORLD_SIZE` env var 와 같은 의미).
|
||||||
|
|
||||||
|
### D3. `init_process_group` 가 즉시 하는 4가지 설치 작업
|
||||||
|
|
||||||
|
`AhbmCCLBackend.__init__` 안에서 다음이 순차 실행된다:
|
||||||
|
|
||||||
|
1. **ccl.yaml 로딩**: `kernbench.ccl.install.load_ccl_config()` →
|
||||||
|
`resolve_algorithm_config(_cfg_all)` 로 `defaults.algorithm` (또는
|
||||||
|
사용자가 지정한 알고리즘) 의 merged config 산출.
|
||||||
|
2. **알고리즘 모듈 import**: `importlib.import_module(self._merged["module"])`.
|
||||||
|
이 모듈은 `kernel` 함수, `kernel_args(world_size, n_elem, cube_w, cube_h)`
|
||||||
|
helper, optional `TOPO_NAME_TO_KIND` 매핑을 노출해야 한다.
|
||||||
|
3. **world_size 산출** (D2).
|
||||||
|
4. **topology 메타 수집**: `spec` 으로부터 `n_sips`, `sip_topo` (`ring_1d`
|
||||||
|
기본), `cube_w`/`cube_h`, `sips.w`/`sips.h`. SIP topology 가 ring_1d 가
|
||||||
|
아니면 explicit `w`/`h` 또는 square root 로 (`w*h == n_sips` 보장)
|
||||||
|
`_sip_topo_w/h` 산출. 불일치 시 `ValueError`.
|
||||||
|
5. **SFR + IPCQ 설치**: `kernbench.ccl.sfr_config.configure_sfr_intercube_multisip
|
||||||
|
(engine, spec, self._merged)` 를 호출. 이 함수가 모든 SIP/cube 의 pe0 에
|
||||||
|
IPCQ neighbor table 을 푸시 (real NCCL communicator 의 일회성 설정에
|
||||||
|
해당).
|
||||||
|
|
||||||
|
이 순서가 변하면 (예: SFR 전에 algorithm 모듈 load 가 실패하면) 부분 초기화
|
||||||
|
상태가 발생할 수 있다. 따라서 D3 는 atomic 한 4-단계로 본다 — 실패 시
|
||||||
|
backend 는 미설치 상태로 남는다.
|
||||||
|
|
||||||
|
### D4. greenlet-local rank 등록 (ADR-0024 D2)
|
||||||
|
|
||||||
|
`DistributedContext._rank_by_greenlet: dict[greenlet, int]` 은 spawn 된
|
||||||
|
worker greenlet 각각에 rank 를 매핑한다. bench launcher (예:
|
||||||
|
`torch.multiprocessing.spawn`) 가 worker 를 띄울 때
|
||||||
|
`dc._bind_rank(g, rank)` 를 호출하여 등록한다.
|
||||||
|
|
||||||
|
`get_rank()` 는 `getcurrent()` 의 greenlet 을 lookup. 미등록 greenlet은
|
||||||
|
fallback 으로 0 을 반환 — single-driver / 테스트 호환성 유지.
|
||||||
|
|
||||||
|
backend 는 `_dist_ctx._rank_by_greenlet` 를 통해 `all_reduce` 시 현재
|
||||||
|
greenlet 의 rank 를 가져온다 (D5).
|
||||||
|
|
||||||
|
### D5. `all_reduce(tensor, op="sum")` 동작
|
||||||
|
|
||||||
|
검증 단계:
|
||||||
|
|
||||||
|
- `op != "sum"` → `NotImplementedError`. 현재 kernel 들은 add reduction만 구현.
|
||||||
|
- `tensor._handle is None` → `RuntimeError("not deployed")`.
|
||||||
|
- `tensor._handle.shards` 가 비면 `RuntimeError("no shards")`.
|
||||||
|
|
||||||
|
준비 단계:
|
||||||
|
|
||||||
|
- `n_elem = shards[0].nbytes // tensor.itemsize` — 단일 shard 의 element 수.
|
||||||
|
- `kernel_fn = self._algo_module.kernel` — D3 에서 import 된 알고리즘 모듈의
|
||||||
|
진입 함수.
|
||||||
|
- effective cube dims 결정: 첫 번째 SIP 의 cube 갯수가 1 이면 (1,1) 으로
|
||||||
|
scalar 처리, 아니면 토폴로지의 `cube_w`/`cube_h` 사용. TP 가 일부 cube
|
||||||
|
만 쓰는 경우를 자연스럽게 흡수.
|
||||||
|
- `kernel_args = self._algo_module.kernel_args(world_size, n_elem, cube_w,
|
||||||
|
cube_h)` — 알고리즘이 자기 kernel 에 넘길 인자 셋을 결정.
|
||||||
|
|
||||||
|
dispatch:
|
||||||
|
|
||||||
|
- 현재 greenlet 의 rank 를 `_rank_by_greenlet.get(g, 0)` 로 lookup.
|
||||||
|
- `extra_args = (sip_rank, sip_topo_kind, sip_topo_w, sip_topo_h)` 를 append.
|
||||||
|
- `pending = self.ctx.launch(algorithm_name, kernel_fn, tensor, *kernel_args,
|
||||||
|
*extra_args, _defer_wait=True)` — `_defer_wait=True` 로 collective drain
|
||||||
|
을 메인 scheduler 에 위임 (ADR-0027 D0.4).
|
||||||
|
|
||||||
|
drain:
|
||||||
|
|
||||||
|
- 부모 greenlet 이 살아있으면 (multi-greenlet 모드) `_pending_collective_handles`
|
||||||
|
에 enqueue 한 뒤 부모로 switch. 메인 scheduler 가 모든 rank 의 launch 후
|
||||||
|
일괄 drain.
|
||||||
|
- 단일-driver 모드면 inline 으로 `for h, _sip_id, meta in pending:
|
||||||
|
self.ctx.wait(h, _meta=meta)` 즉시 drain.
|
||||||
|
|
||||||
|
### D6. `barrier()` 는 no-op 이다 (single-driver 모델)
|
||||||
|
|
||||||
|
kernbench 는 하나의 Python process 안에서 모든 rank 를 greenlet 으로 다룬다.
|
||||||
|
process 간 동기화가 필요한 상황이 없으므로 `barrier()` 는 호출 가능하지만
|
||||||
|
실제 어떤 동기화도 수행하지 않는다. real PyTorch DDP 와의 API 호환성을
|
||||||
|
위해 유지 (호출자가 NotImplementedError 를 받지 않도록).
|
||||||
|
|
||||||
|
장래에 multi-process kernbench (예: SimPy event loop 가 process 별로
|
||||||
|
독립) 가 도입되면 D6 를 supersede 하는 새 ADR 이 필요.
|
||||||
|
|
||||||
|
### D7. `get_rank` / `get_world_size` / `get_backend` 의 의미
|
||||||
|
|
||||||
|
- `get_rank()` (D4): 현재 greenlet 의 bound rank. 미등록은 0.
|
||||||
|
- `get_world_size()` (D2): backend 가 D3 에서 산출한 world_size.
|
||||||
|
- `get_backend()`: 항상 `"ahbm"` 문자열. backend 객체가 존재하지 않으면
|
||||||
|
`_ensure_initialized` 에서 RuntimeError.
|
||||||
|
|
||||||
|
real PyTorch 와의 차이:
|
||||||
|
|
||||||
|
- real PyTorch `get_rank()` 는 process global 값이지만, kernbench 는
|
||||||
|
greenlet-local. spawn 된 worker 안에서 호출하면 rank, main thread 에서
|
||||||
|
호출하면 0. bench 작성자는 worker 함수 안에서만 의미 있는 rank 를 기대해야
|
||||||
|
한다.
|
||||||
|
|
||||||
|
### D8. 지원하는 API 표면 (final)
|
||||||
|
|
||||||
|
`DistributedContext` 가 노출하는 API:
|
||||||
|
|
||||||
|
- `init_process_group(backend="ahbm", world_size=None, rank=None, **kwargs)`
|
||||||
|
- `is_initialized() -> bool`
|
||||||
|
- `get_world_size() -> int`
|
||||||
|
- `get_rank() -> int`
|
||||||
|
- `get_backend() -> str`
|
||||||
|
- `all_reduce(tensor, op="sum") -> None`
|
||||||
|
- `barrier() -> None`
|
||||||
|
- (internal) `_bind_rank(g, rank)`
|
||||||
|
|
||||||
|
이외의 PyTorch distributed API (broadcast, reduce, all_gather, gather,
|
||||||
|
scatter, send/recv 등) 는 **아직 구현되어 있지 않다**. kernel 레벨에서는
|
||||||
|
`tl.send`/`tl.recv` (ADR-0046 D3.10) 로 직접 표현 가능하나, dist.* surface
|
||||||
|
로는 노출되지 않는다. 추가 collective 가 필요해질 시 별도 알고리즘 모듈
|
||||||
|
+ `DistributedContext` 메소드 한 쌍을 추가하여 D8 를 확장한다.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
### A1. backend 를 `RuntimeContext.__init__` 에서 즉시 생성
|
||||||
|
|
||||||
|
기각. ccl.yaml 이 없거나 알고리즘 모듈을 import 할 수 없는 경우, bench 가
|
||||||
|
distributed 기능을 안 쓰는데도 RuntimeContext 생성 자체가 실패하게 된다.
|
||||||
|
"호출 시점에 비로소 설치" (D1) 가 lazy 의미상 옳다.
|
||||||
|
|
||||||
|
### A2. world_size 를 항상 topology 로부터 자동 산출 (override 금지)
|
||||||
|
|
||||||
|
기각. ADR-0024 D1 의 "explicit override" 경로가 legacy 테스트에서 사용 중.
|
||||||
|
한 SIP 안에서 PE-level rank 를 따로 정의해야 하는 진단 시나리오를 위해
|
||||||
|
유지.
|
||||||
|
|
||||||
|
### A3. `op != "sum"` 을 silent fallback 으로 처리
|
||||||
|
|
||||||
|
기각. 사용자가 `op="prod"` / `"max"` / `"avg"` 를 의도했는데 silently sum
|
||||||
|
이 실행되면 결과 검증이 매우 어렵다. 명시적 `NotImplementedError` 가 안전.
|
||||||
|
|
||||||
|
### A4. `barrier` 를 SimPy event 로 구현
|
||||||
|
|
||||||
|
기각 (현재). single-driver 모델에서 cross-process 동기화 의미가 없으므로
|
||||||
|
no-op 가 의미적으로 정확. SimPy fake-barrier 는 의미 없이 코드 복잡도만
|
||||||
|
높임. multi-process kernbench 도입 시 재평가.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- `torch.distributed.init_process_group(backend="ahbm")` 의 4-단계 설치
|
||||||
|
(D3) 가 ADR-level 에서 굳어져, 향후 새 collective 알고리즘이 어디에
|
||||||
|
훅을 걸어야 하는지 명확.
|
||||||
|
- D2 의 우선순위 (algorithm > defaults > topology) 가 명시되어, ccl.yaml
|
||||||
|
변경 시 영향 범위를 빠르게 가늠 가능.
|
||||||
|
- D6 의 barrier no-op 결정이 ADR-level 에 굳어져, multi-process kernbench
|
||||||
|
도입 시 별도 ADR 로 supersede 해야 함이 분명.
|
||||||
|
- D8 의 미지원 API 목록이 명시되어, 사용자가 `dist.broadcast(...)` 를
|
||||||
|
호출하려 할 때의 명확한 거절 근거 제공.
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
# ADR-0048: Memory Allocator Algorithms — VirtualAllocator + PEMemAllocator
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted (2026-05-22).
|
||||||
|
|
||||||
|
`policy/address/allocator.py` 의 `_FreeList` / `PEMemAllocator` 와
|
||||||
|
`va_allocator.py` 의 `VirtualAllocator` 가 사용하는 free-list 알고리즘,
|
||||||
|
페이지 정렬, coalescing 규칙을 명시한다. ADR-0001 (PhysAddr 레이아웃) 과
|
||||||
|
ADR-0011 (PA/VA/LA 모델) 이 주소 스킴을 정의하나, **할당 알고리즘**은 별도
|
||||||
|
ADR 이 없었다.
|
||||||
|
|
||||||
|
## First action (제일 처음에 하는 일)
|
||||||
|
|
||||||
|
### `_FreeList(capacity)`
|
||||||
|
|
||||||
|
생성 즉시 `self._capacity = capacity`, `self._used = 0`, `self._free =
|
||||||
|
[(0, capacity)]` 로 초기화. 첫 일은 **전 영역을 single free block 으로
|
||||||
|
세우는 것** — 즉 `(offset=0, size=capacity)` 한 튜플이 free list 의 유일한
|
||||||
|
원소다.
|
||||||
|
|
||||||
|
### `PEMemAllocator(sip_id, die_id, pe_id, cfg)`
|
||||||
|
|
||||||
|
생성 즉시 두 개의 `_FreeList` 를 만든다:
|
||||||
|
|
||||||
|
- `self._hbm = _FreeList(cfg.hbm_slice_bytes)` — 이 PE 가 소유한 HBM
|
||||||
|
slice 의 바이트 크기 (`hbm_bytes_per_cube // hbm_slices_per_cube`) 만큼.
|
||||||
|
- `self._tcm = _FreeList(cfg.tcm_allocatable_bytes)` — `tcm_bytes_per_pe -
|
||||||
|
tcm_scheduler_reserved_bytes` 만큼 (scheduler 예약분은 사전 분리).
|
||||||
|
|
||||||
|
따라서 PEMemAllocator 의 첫 일은 **이 PE 의 HBM slice 와 사용자
|
||||||
|
TCM 영역을 각각 단일 free block 으로 세우는 것**.
|
||||||
|
|
||||||
|
### `VirtualAllocator(va_base, va_size, page_size=2*1024*1024)`
|
||||||
|
|
||||||
|
생성 즉시 `self._va_base = va_base`, `self._va_size = va_size`,
|
||||||
|
`self._page_size = page_size`, `self._used = 0`, `self._free = [(va_base,
|
||||||
|
va_size)]`. 첫 일은 **VA base 부터 size 까지 single block 으로 세우고
|
||||||
|
page_size 를 회수**.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
`runtime_api/context.py::_ensure_allocators` 는 다음 단계로 allocator 세트를
|
||||||
|
구성한다:
|
||||||
|
|
||||||
|
1. spec 으로부터 `hbm_total_gb_per_cube`, `hbm_slices_per_cube`,
|
||||||
|
`tcm_size_mb`, target_device 별 SIP 범위 등을 읽음.
|
||||||
|
2. `AddressConfig` 로 모든 파라미터를 frozen 하게 패킹.
|
||||||
|
3. target SIP 범위 × cube × PE 의 모든 조합에 대해
|
||||||
|
`PEMemAllocator(sip, cube, pe, cfg)` 인스턴스를 1개씩 생성.
|
||||||
|
4. `VirtualAllocator(va_base=0x1_0000_0000, va_size=64 GiB,
|
||||||
|
page_size=pe_mmu.page_size)` 를 1개 생성.
|
||||||
|
|
||||||
|
allocator 들의 책임:
|
||||||
|
|
||||||
|
- **PEMemAllocator**: PE-로컬 HBM slice / TCM 의 PA-공간 할당 (PhysAddr
|
||||||
|
encoding 까지 포함).
|
||||||
|
- **VirtualAllocator**: device-wide VA 공간을 페이지 정렬로 할당. 이후
|
||||||
|
`RuntimeContext._create_tensor` 가 VA → PA 매핑을 `MmuMapMsg` 로 fabric
|
||||||
|
에 push.
|
||||||
|
|
||||||
|
이 알고리즘들은:
|
||||||
|
|
||||||
|
- **first-fit** 으로 단순.
|
||||||
|
- 자유 블록 리스트는 **offset 정렬 (sorted by start)** 유지.
|
||||||
|
- `free()` 시 **양쪽 인접 블록과 coalesce**.
|
||||||
|
|
||||||
|
이런 결정의 근거가 어디에도 없으므로, 향후 누군가 "왜 best-fit 이 아닌가",
|
||||||
|
"왜 buddy allocator 가 아닌가", "왜 partial overlap free 가 silently
|
||||||
|
허용되는가" 라는 질문에 답할 기준이 필요. 본 ADR 이 그 기준을 마련한다.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### D1. `_FreeList` — offset-기반 first-fit + coalescing
|
||||||
|
|
||||||
|
`policy/address/allocator.py::_FreeList`:
|
||||||
|
|
||||||
|
- 내부 표현: `list[tuple[int, int]]` = `[(start_offset, size), ...]` —
|
||||||
|
start offset 으로 정렬된 자유 블록의 sorted list.
|
||||||
|
- `alloc(nbytes)`:
|
||||||
|
1. free list 를 앞에서부터 순회 (first-fit).
|
||||||
|
2. 처음 만나는 `size >= nbytes` 인 블록에서 앞부분을 잘라 사용.
|
||||||
|
3. 정확히 일치하면 블록 통째로 제거; 아니면 `(start+nbytes, size-nbytes)`
|
||||||
|
로 축소.
|
||||||
|
4. `_used += nbytes`, 잘라낸 `start` 반환.
|
||||||
|
5. 맞는 블록이 없으면 `AllocationError("overflow ... largest free block
|
||||||
|
...")`.
|
||||||
|
- `free(offset, nbytes)`:
|
||||||
|
1. `_used -= nbytes`.
|
||||||
|
2. `bisect_left(self._free, (offset,))` 로 삽입 위치 결정.
|
||||||
|
3. 직전 블록과 인접 (`prev_start + prev_size == offset`) 하면 흡수.
|
||||||
|
4. 직후 블록과 인접 (`offset+nbytes == next_start`) 하면 흡수.
|
||||||
|
5. coalesced range 를 정렬 위치에 insert.
|
||||||
|
|
||||||
|
이 알고리즘은 fragmentation 에 약점이 있으나 (best-fit / buddy 대비), 본
|
||||||
|
시뮬레이터의 워크로드 특성상 (deploy/free 패턴이 거의 stack-like) 충분
|
||||||
|
하다는 것이 디자인 가정이다. 워크로드가 변하면 D1 supersede 후보.
|
||||||
|
|
||||||
|
### D2. partial overlap free 는 **검사하지 않는다**
|
||||||
|
|
||||||
|
`_FreeList.free(offset, nbytes)` 는 호출자가 정확한 (offset, nbytes) 를
|
||||||
|
넘긴다고 신뢰한다. 다음을 검증하지 않는다:
|
||||||
|
|
||||||
|
- 그 range 가 실제로 alloc 된 것인지.
|
||||||
|
- 그 range 가 다른 alloc 된 영역과 겹치지 않는지.
|
||||||
|
|
||||||
|
이유: 시뮬레이터 컨텍스트에서 호출자는 항상 `alloc()` 의 반환값을 그대로
|
||||||
|
저장했다가 `free()` 에 넘기는 패턴이며, 외부 사용자 입력이 아니다. 안전성
|
||||||
|
검사를 추가하면 매 free 마다 O(N) 비용이 들어 시뮬 wall-clock 에 영향.
|
||||||
|
|
||||||
|
이 신뢰 모델이 깨지면 (예: 두 텐서가 같은 PA 를 가리키는 코드 경로 도입)
|
||||||
|
즉시 ADR-level 으로 재검토.
|
||||||
|
|
||||||
|
### D3. `PEMemAllocator` — HBM/TCM 두 채널 분리
|
||||||
|
|
||||||
|
`PEMemAllocator(sip_id, die_id, pe_id, cfg)` 는 두 `_FreeList` 를 보유:
|
||||||
|
|
||||||
|
- `_hbm`: `cfg.hbm_slice_bytes` 크기.
|
||||||
|
- `_tcm`: `cfg.tcm_allocatable_bytes` (= `tcm_bytes_per_pe -
|
||||||
|
tcm_scheduler_reserved_bytes`) 크기.
|
||||||
|
|
||||||
|
`alloc_hbm(nbytes) -> PhysAddr`:
|
||||||
|
|
||||||
|
- `_hbm.alloc(nbytes)` 로 offset 획득.
|
||||||
|
- `PhysAddr.pe_hbm_addr(sip_id, die_id, pe_id, pe_local_hbm_offset=offset,
|
||||||
|
slice_size_bytes=cfg.hbm_slice_bytes)` 로 PA 인코딩.
|
||||||
|
- 실패 시 `AllocationError("HBM overflow ...")`.
|
||||||
|
|
||||||
|
`free_hbm(pa, nbytes)`:
|
||||||
|
|
||||||
|
- `pa.hbm_offset - pe_id * cfg.hbm_slice_bytes` 로 PE-local offset 복원.
|
||||||
|
- `_hbm.free(offset, nbytes)`.
|
||||||
|
|
||||||
|
`alloc_tcm(nbytes) -> PhysAddr`: 유사하게 `PhysAddr.pe_tcm_addr` 로 인코딩.
|
||||||
|
|
||||||
|
`free_tcm(pa, nbytes)`: `pa.sub_offset` 을 그대로 사용 (TCM 은 PE-local
|
||||||
|
offset 이 곧 sub_offset).
|
||||||
|
|
||||||
|
scheduler-reserved TCM 영역 (`cfg.tcm_scheduler_reserved_bytes`) 은
|
||||||
|
allocator 가 인지하지 않는다 (`_tcm` 의 capacity 에서 사전 차감되어 있음).
|
||||||
|
이는 ADR-0014 의 PE_SCHEDULER 내부 buffer 예약과 정합된다.
|
||||||
|
|
||||||
|
### D4. `VirtualAllocator` — 페이지 정렬 first-fit + coalescing
|
||||||
|
|
||||||
|
`policy/address/va_allocator.py::VirtualAllocator`:
|
||||||
|
|
||||||
|
- 내부 표현: `_FreeList` 와 동일한 sorted `list[tuple[int, int]]`.
|
||||||
|
최초: `[(va_base, va_size)]`.
|
||||||
|
- `_align_up(nbytes) = ceil(nbytes / page_size) * page_size`.
|
||||||
|
- `alloc(nbytes) -> int`:
|
||||||
|
1. `aligned = _align_up(nbytes)`.
|
||||||
|
2. first-fit 으로 `size >= aligned` 인 블록 탐색.
|
||||||
|
3. 블록 앞부분 `aligned` 만큼 잘라 사용. 정확히 일치하면 제거.
|
||||||
|
4. `_used += aligned`. 블록 `start` (= aligned 된 VA) 반환.
|
||||||
|
5. 실패 시 `VaAllocationError`.
|
||||||
|
- `free(va, nbytes)`: `_align_up(nbytes)` 단위로 free. _FreeList 와 동일한
|
||||||
|
coalesce 알고리즘.
|
||||||
|
|
||||||
|
`page_size` 의 실제 값은 두 곳에서 다른 기본을 갖는다:
|
||||||
|
|
||||||
|
- `VirtualAllocator.__init__` 의 매개변수 기본값: `2 MiB`. 직접 호출하는
|
||||||
|
테스트가 그대로 받는다.
|
||||||
|
- `RuntimeContext._ensure_allocators` 가 인스턴스화할 때:
|
||||||
|
`pe_mmu.attrs.get("page_size", 4096)` — `topology.yaml` 의
|
||||||
|
`pe_mmu.attrs.page_size` 가 있으면 그 값, 없으면 fallback 4 KiB.
|
||||||
|
|
||||||
|
두 기본이 다른 이유: VirtualAllocator 의 standalone 기본은 ADR-0039 의
|
||||||
|
PE_MMU stopgap 기본 (2 MiB) 과 정합되어 직접 테스트가 자연스럽고, context
|
||||||
|
fallback 의 4 KiB 는 topology 미설정 시 안전한 minimum page 다. 실제 사용
|
||||||
|
경로는 항상 후자이며 (`_ensure_allocators` 가 인스턴스화하므로),
|
||||||
|
`topology.yaml` 에서 `page_size` 가 명시되면 그 값이 양쪽 (MMU + VA
|
||||||
|
allocator) 으로 일관되게 흐른다.
|
||||||
|
|
||||||
|
만약 이 일치가 깨지면 (예: VirtualAllocator 의 page_size 를 PE_MMU 와
|
||||||
|
다르게 인스턴스화) MMU `map()` 가 서브-페이지 region 모드 (ADR-0039 D3) 로
|
||||||
|
흐른다.
|
||||||
|
|
||||||
|
VA 기본 범위: `va_base = 0x1_0000_0000` (= 4 GiB), `va_size = 64 GiB`. 이
|
||||||
|
값은 `_ensure_allocators` 에 하드코딩되어 있으며 ADR-0011 의 VA 모델에서
|
||||||
|
직접적인 의미를 갖지는 않는다 — 단지 host 코드와 충돌하지 않을 만큼 큰
|
||||||
|
주소 공간을 device-wide 로 잡아둔 것.
|
||||||
|
|
||||||
|
### D5. allocator 인스턴스의 lifecycle
|
||||||
|
|
||||||
|
- `RuntimeContext._ensure_allocators` 가 lazy 하게 호출됨 (`_create_tensor`
|
||||||
|
의 첫 호출 시점).
|
||||||
|
- 한 번 생성된 allocator dict (`self._allocators`) 는 RuntimeContext 의
|
||||||
|
lifetime 동안 재사용. 같은 process 안의 두 번째 deploy 는 새 객체를
|
||||||
|
만들지 않는다.
|
||||||
|
- `RuntimeContext.cleanup()` 이 모든 living tensor 의 `_free_tensor()` 를
|
||||||
|
호출 → MMU unmap + `va_allocator.free` + `pemem_allocator.free_hbm` 으로
|
||||||
|
free list 가 원상복구. 다음 RuntimeContext 가 다시 만들면 초기 상태부터.
|
||||||
|
|
||||||
|
allocator 상태가 RuntimeContext 간에 공유되지 않는 점이 단일 process 안의
|
||||||
|
연속 실행에서 deploy → cleanup → deploy 의 결정성을 보장한다.
|
||||||
|
|
||||||
|
### D6. Allocator 실패는 raise 한다 (silent OOM 금지)
|
||||||
|
|
||||||
|
`_FreeList.alloc` / `VirtualAllocator.alloc` 모두 충분한 free block 이
|
||||||
|
없으면 `AllocationError` / `VaAllocationError` 를 던진다. 메시지에는
|
||||||
|
"required size + largest available block" 가 포함되어, fragmentation
|
||||||
|
인지 진짜 OOM 인지 진단 가능.
|
||||||
|
|
||||||
|
silent fallback (예: 가장 큰 블록만큼만 alloc) 는 절대 금지 — 부분 할당된
|
||||||
|
텐서가 SimPy 단계에 들어가면 라우팅·DMA 가 잘못된 PA 를 인지하여 시뮬
|
||||||
|
정확도가 깨진다.
|
||||||
|
|
||||||
|
### D7. address space 와 allocator 의 1:1 대응
|
||||||
|
|
||||||
|
물리 주소 공간 분리는 PhysAddr 의 sub-unit (ADR-0001 D2.3) 으로 표현되며,
|
||||||
|
각 sub-unit 마다 별도 allocator 인스턴스를 둔다:
|
||||||
|
|
||||||
|
- HBM slice → `PEMemAllocator._hbm`.
|
||||||
|
- PE TCM → `PEMemAllocator._tcm`.
|
||||||
|
- (현재 미사용) M_CPU local memory, CUBE SRAM → 별도 allocator 필요. 현재
|
||||||
|
구현은 아직 IPCQ-only slot 으로 처리 (ADR-0023 D9.7) 하며 PA 공간을
|
||||||
|
share 하지 않으므로 별도 free-list 가 없음.
|
||||||
|
|
||||||
|
cube-level SRAM allocator 가 필요해지면 `_FreeList(cfg.sram_bytes_per_cube)`
|
||||||
|
인스턴스를 cube 단위로 추가한다 (`cfg.sram_bytes_per_cube` 는 이미
|
||||||
|
`AddressConfig` 에 정의되어 있어 데이터 모델은 준비됨).
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
### A1. best-fit / buddy allocator
|
||||||
|
|
||||||
|
기각 (현재). 워크로드의 alloc/free 패턴이 stack-like (deploy 순서 = free
|
||||||
|
순서) 라 first-fit + coalescing 으로 fragmentation 이 충분히 통제된다.
|
||||||
|
LLM kernel sweep 에서 long-running fragmentation 이 관찰되면 buddy 로
|
||||||
|
교체하는 ADR 을 별도로 만든다.
|
||||||
|
|
||||||
|
### A2. partial overlap free 검증 추가
|
||||||
|
|
||||||
|
기각. D2 의 신뢰 모델 + O(N) 검사 비용. 단, 디버그 모드 (`KERNBENCH_DEBUG`
|
||||||
|
env var 등) 에서 활성화하는 옵션은 후속 작업으로 가능.
|
||||||
|
|
||||||
|
### A3. VA 와 PA 의 통합 allocator
|
||||||
|
|
||||||
|
기각. VA 공간 (64 GiB device-wide) 과 PA 공간 (slice 별 ~6 GiB) 는 의미
|
||||||
|
차원이 다르다. VA 는 host kernel 의 view, PA 는 device sub-unit 의 view.
|
||||||
|
ADR-0011 의 VA 모델 정신 (MMU 가 둘 사이를 매핑) 과 정합하기 위해
|
||||||
|
allocator 도 분리.
|
||||||
|
|
||||||
|
### A4. page_size 의 multi-tier 지원 (large page + small page)
|
||||||
|
|
||||||
|
기각 (현재). 단일 page_size (현재 2 MiB) 가 LLM kernel 의 텐서 단위 (수
|
||||||
|
MiB~수 GiB) 에 맞고, ADR-0039 D3 의 서브-페이지 region 으로 작은 매핑이
|
||||||
|
필요할 때 흡수된다. multi-tier page 는 MMU 자체 모델을 확장해야 하므로
|
||||||
|
별도 ADR 후보.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- allocator 알고리즘이 ADR-level 에서 굳어져 (D1·D3·D4), 새로운 시뮬
|
||||||
|
시나리오에서 fragmentation 이슈가 발생할 때 "여기서 first-fit + coalesce
|
||||||
|
를 쓰고 있다" 가 명확.
|
||||||
|
- D2 의 신뢰 모델이 명시되어, 향후 사용자 입력으로부터 직접 alloc/free 를
|
||||||
|
받는 경로가 도입되면 본 ADR supersede 가 필요함을 일찍 인지 가능.
|
||||||
|
- D7 의 sub-unit별 allocator 1:1 대응이 명시되어, M_CPU/SRAM 별도 영역이
|
||||||
|
필요해질 때 어디에 free-list 를 추가해야 하는지 명확.
|
||||||
|
- `VirtualAllocator` 의 page_size 가 PE_MMU 설정과 일치해야 함이 D4 에
|
||||||
|
적혀 있어, 향후 topology.yaml 의 page_size 변경 시 ADR-0039 stopgap 동작
|
||||||
|
과의 상호작용을 빠르게 가늠 가능.
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
# ADR-0049: `kernbench probe` Subcommand — Traffic-Pattern Verification Harness
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted (2026-05-22).
|
||||||
|
|
||||||
|
`probes/probe.py` 의 `run_probe(...)` 가 노출하는 traffic-pattern catalog,
|
||||||
|
formula vs actual 비교, 그리고 monotonicity / D2H≥H2D 같은 invariant
|
||||||
|
체크의 의미를 명시한다. ADR-0010 (CLI surface) 가 `kernbench probe`
|
||||||
|
subcommand 를 enumerate 하나, **probe 가 실제로 측정하는 것**과 **어떤
|
||||||
|
invariant 를 PASS/FAIL 로 판정하는가**는 ADR-level 에 없었다.
|
||||||
|
|
||||||
|
## First action (제일 처음에 하는 일)
|
||||||
|
|
||||||
|
`run_probe(topology_path, case_filter=None)` 의 첫 4가지 작업:
|
||||||
|
|
||||||
|
1. `Path(topology_path).expanduser().resolve()` 로 절대 경로 산출.
|
||||||
|
2. `load_topology(path)` → `TopologyGraph` 인스턴스 (그래프 + spec).
|
||||||
|
3. `_build_edge_map(graph)` → `{(src, dst): Edge}` 빠른 lookup 테이블.
|
||||||
|
4. `AddressResolver(graph)` + `PathRouter(graph)` 인스턴스화.
|
||||||
|
|
||||||
|
그 다음 `nbytes = 32768` (= 32 KiB, summary table 의 기준 데이터 크기) 와
|
||||||
|
`show_all = (case_filter is None or case_filter == "all")` 를 설정.
|
||||||
|
|
||||||
|
즉, **probe 의 첫 일은 "토폴로지를 한 번 로드하여 edge map / resolver /
|
||||||
|
router 를 준비하고, 32 KiB 라는 표준 측정 크기를 픽스하는 것"**. 그 이후
|
||||||
|
H2D → D2H → PE DMA 세 카테고리의 case 들이 각각 별도의 `GraphEngine`
|
||||||
|
인스턴스에서 실행된다 (case 간 cross-talk 차단).
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
`kernbench probe` 는 다음 의도로 도입된 verification 도구다:
|
||||||
|
|
||||||
|
- **수동 분석 ground truth**: 실 시뮬레이션 (`kernbench run --bench ...`)
|
||||||
|
결과의 latency 가 비정상으로 보일 때, 단순 traffic pattern 의 정답을 별도
|
||||||
|
로 얻어 비교.
|
||||||
|
- **formula vs actual 비교**: 분석 모델 (wire latency + overhead + drain)
|
||||||
|
과 시뮬레이션 결과 (`total_ns`) 가 일치하는지 확인. 일치하지 않으면 모델
|
||||||
|
단순화 가정 (ADR-0033) 어디가 빠진 것인지 단서.
|
||||||
|
- **monotonicity check**: hop 수가 늘면 latency 가 단조 증가해야 한다는
|
||||||
|
invariant 의 자동 확인.
|
||||||
|
- **utilization sweep**: 데이터 크기 (4 KiB ~ 1 MiB) 별 BW 활용률 표.
|
||||||
|
|
||||||
|
이 도구의 동작 사양이 ADR-level 에 없으면:
|
||||||
|
|
||||||
|
- 다른 형식의 traffic pattern (예: MCpuDma, IPCQ) 을 추가하려는 사람이 기존
|
||||||
|
카테고리의 표 포맷 / 측정 단위를 일관되게 따르기 어렵다.
|
||||||
|
- monotonicity 가 무엇을 기준으로 검사되는지 (hop 수? cube 거리? wire
|
||||||
|
길이?) 모호.
|
||||||
|
- 32 KiB 라는 기준 크기와 `[4 KiB, 16 KiB, 64 KiB, 256 KiB, 1 MiB]` sweep
|
||||||
|
의 의미가 코드 grep 으로만 확인 가능.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### D1. 세 가지 case category — H2D / D2H / PE DMA
|
||||||
|
|
||||||
|
각 category 는 토폴로지 상 별개의 데이터 경로를 가지며, 별도의 summary
|
||||||
|
table + sweep table + route detail block 으로 출력된다.
|
||||||
|
|
||||||
|
- **H2D (Host→Device Write)**: `MemoryWriteMsg(dst_sip=0, dst_cube,
|
||||||
|
dst_pe=0, pattern="zero")` 가 `pcie_ep → io_cpu → m_cpu → hbm_ctrl` 경로
|
||||||
|
를 흐른다. cube 인덱스로 hop 수가 증가:
|
||||||
|
- h2d-1hop: cube=0, hops=1
|
||||||
|
- h2d-2hop: cube=4, hops=2
|
||||||
|
- h2d-3hop: cube=8, hops=3
|
||||||
|
- h2d-4hop: cube=12, hops=4
|
||||||
|
- **D2H (Device→Host Read)**: `MemoryReadMsg(src_sip=0, src_cube, src_pe=0)`.
|
||||||
|
forward command path + reverse data path 의 합 latency. 같은 4 hops
|
||||||
|
카테고리.
|
||||||
|
- **PE DMA (PE-initiated)**: `PeDmaMsg(src_sip, src_cube, src_pe, dst_pa)`.
|
||||||
|
5 가지 케이스로 cube/PE 위치 변화:
|
||||||
|
- pe-local-hbm: same cube, same PE
|
||||||
|
- pe-same-half-hbm: same cube, different PE (PE 1)
|
||||||
|
- pe-cross-half-hbm: same cube, far PE (PE 4)
|
||||||
|
- pe-cross-cube-hbm-best: adjacent cube (cube 1)
|
||||||
|
- pe-cross-cube-hbm-worst: diagonal far cube (cube 15)
|
||||||
|
|
||||||
|
cube 인덱스가 4/8/12 (H2D), 1/4/15 (PE DMA) 같이 의미 있는 이유는
|
||||||
|
4x4 cube mesh (sip.cube_mesh.w=4, h=4) 에서의 거리 정의 — 추후 cube_mesh
|
||||||
|
크기 변경 시 이 값들이 같이 갱신되어야 한다.
|
||||||
|
|
||||||
|
### D2. 표준 측정 크기 — `nbytes = 32768` (32 KiB)
|
||||||
|
|
||||||
|
모든 case 의 summary table 은 `nbytes=32768` 로 한 번 실행한 결과를
|
||||||
|
보여준다. 32 KiB 가 선택된 이유:
|
||||||
|
|
||||||
|
- DMA overhead 와 BW drain 이 한쪽으로 치우치지 않는 적당한 크기.
|
||||||
|
- 다수 sub-unit (TCM, register file) 의 1회 transfer 단위와 비교 가능.
|
||||||
|
|
||||||
|
크기별 utilization 변화는 별도 sweep table 이 보여준다 (D3).
|
||||||
|
|
||||||
|
### D3. Utilization sweep — `[4 KiB, 16 KiB, 64 KiB, 256 KiB, 1 MiB]`
|
||||||
|
|
||||||
|
`SWEEP_SIZES = [4096, 16384, 65536, 262144, 1048576]`, `SWEEP_LABELS =
|
||||||
|
["4KB", "16KB", "64KB", "256KB", "1MB"]`. 매 size 마다 다음 공식:
|
||||||
|
|
||||||
|
```
|
||||||
|
drain = nbytes / bottleneck_bw
|
||||||
|
total = overhead + wire + drain
|
||||||
|
eff_bw = nbytes / total
|
||||||
|
util% = eff_bw / bottleneck_bw × 100
|
||||||
|
```
|
||||||
|
|
||||||
|
`bn_bw is None or <= 0` 이면 그 컬럼은 0.0 % 로 출력. 의미: hop 수가 늘
|
||||||
|
수록 작은 transfer 는 overhead-bound, 큰 transfer 는 drain-bound 가 되는
|
||||||
|
패턴을 한 표에서 확인.
|
||||||
|
|
||||||
|
### D4. 측정 항목 — actual / formula / breakdown
|
||||||
|
|
||||||
|
각 case 행에 표시되는 컬럼:
|
||||||
|
|
||||||
|
- `Actual` (total_ns): SimPy 실행 결과의 `trace["total_ns"]`.
|
||||||
|
- `Ovhd`: 경로상 모든 node 의 `node.attrs["overhead_ns"]` 합 (formula
|
||||||
|
breakdown).
|
||||||
|
- `Drain`: `nbytes / min(edge.bw_gbs over path)` (formula).
|
||||||
|
- `Wire`: `Σ edge.distance_mm * (ns_per_mm from spec)`.
|
||||||
|
- `Ovhd%` / `Drain%`: Ovhd/Drain 이 Actual 에서 차지하는 비율 (formula 의
|
||||||
|
Wire 는 통상 매우 작아 표시하지 않음).
|
||||||
|
- `Eff.BW`: `nbytes / total_ns` (실 측정 BW).
|
||||||
|
- `BN.BW`: bottleneck bandwidth (formula). path 상 모든 edge 의 BW 중 최소.
|
||||||
|
edge BW 가 없으면 "-".
|
||||||
|
- `Util%`: `Eff.BW / BN.BW × 100`. 100% 면 single-stream BW upper bound 에
|
||||||
|
도달.
|
||||||
|
|
||||||
|
formula 의 합 (`wire + ovhd + drain`) 과 actual 의 차이가 크면 모델
|
||||||
|
단순화가 잡지 못하는 요소가 있다는 신호 (ADR-0033 의 가정 점검).
|
||||||
|
|
||||||
|
### D5. Invariant 자동 체크 — PASS/FAIL
|
||||||
|
|
||||||
|
다음 invariant 들이 자동으로 확인되어 `[v] PASS` / `[x] FAIL` 로 출력:
|
||||||
|
|
||||||
|
- **H2D / D2H monotonic increase**: hop 수가 늘면 actual latency 가
|
||||||
|
단조 증가해야 함. `all(lats[i] < lats[i+1] for ...)`.
|
||||||
|
- **D2H ≥ H2D**: 같은 hop 인덱스에서 D2H ≥ H2D (D2H 는 forward command
|
||||||
|
+ reverse data 두 leg 이므로). `all(d2h[i].total >= h2d[i].total)`.
|
||||||
|
- **PE DMA best < worst**: cross-cube best (adjacent) latency < cross-cube
|
||||||
|
worst (diagonal) latency.
|
||||||
|
- **PE DMA local vs remote**: local BN BW vs remote BN BW 의 비교 출력
|
||||||
|
(PASS/FAIL 이 아닌 정보성).
|
||||||
|
|
||||||
|
체크가 FAIL 이면 사람이 즉시 모델/토폴로지 회귀를 인지할 수 있도록 한
|
||||||
|
줄로 분명하게 출력.
|
||||||
|
|
||||||
|
### D6. Route detail — per-hop timestamp trace
|
||||||
|
|
||||||
|
summary 와 sweep 표 이후 각 case 의 path 와 per-hop 누적 시간 (
|
||||||
|
`_hop_timestamps`) 가 별도 섹션에서 출력된다:
|
||||||
|
|
||||||
|
- H2D: leg1 (`pcie_ep → io_cpu`) + leg2 (`io_cpu → m_cpu`) + leg3
|
||||||
|
(`m_cpu → hbm_ctrl`) + per-hop trace.
|
||||||
|
- D2H: forward (cmd, no data) + reverse (data) trace 분리 표시.
|
||||||
|
- PE DMA: `pe_dma → router → hbm_ctrl` path + per-hop trace.
|
||||||
|
|
||||||
|
각 hop 의 timestamp 는 cumulative `wire_ns + overhead_ns` 누적. terminal
|
||||||
|
hop 의 annotation 에 `drain:Xns` 가 붙는다. bottleneck edge 는
|
||||||
|
`<BN:XXGB/s>` 로 표시되어 시각적으로 식별 가능.
|
||||||
|
|
||||||
|
### D7. case_filter 인자의 의미
|
||||||
|
|
||||||
|
- `None` 또는 `"all"`: 모든 case 실행 (default).
|
||||||
|
- 다른 문자열: 그 이름과 정확히 일치하는 case 만 실행. 예: `kernbench
|
||||||
|
probe --case h2d-2hop`.
|
||||||
|
|
||||||
|
각 카테고리 안에서 `name != case_filter` 면 skip 되며, 그 카테고리의
|
||||||
|
monotonicity / D2H≥H2D 비교는 데이터가 1개일 때 자연히 skip 된다.
|
||||||
|
|
||||||
|
CLI parser 의 `--case` 기본값은 `"all"`이라 인자 생략 시 전체 실행.
|
||||||
|
|
||||||
|
### D8. 매 case 별 fresh GraphEngine
|
||||||
|
|
||||||
|
H2D 4개, D2H 4개, PE DMA 5개의 case 가 각각 **새로운 GraphEngine**
|
||||||
|
인스턴스에서 실행된다 (`engine = GraphEngine(graph)`). 이유:
|
||||||
|
|
||||||
|
- case 간 누적 상태 (op_log, completion 추적, allocator 등) 가 cross-talk
|
||||||
|
하지 않도록 격리.
|
||||||
|
- 한 case 의 traffic 이 다른 case 의 BW 측정에 영향을 주지 않도록 보장.
|
||||||
|
|
||||||
|
이 격리는 probe 의 측정 결과를 **각 case 단독 single-flow** 의 latency 로
|
||||||
|
해석할 수 있게 한다. multi-flow contention 측정은 별도 도구 (예:
|
||||||
|
`pe2pe_overview` 플롯, ADR-0033 의 multi-flow merging 모델) 책임.
|
||||||
|
|
||||||
|
### D9. 출력 포맷의 안정성
|
||||||
|
|
||||||
|
probe 의 stdout 출력은 사람이 읽기 위함이며, 정확한 컬럼 폭/구분자/공백 은
|
||||||
|
machine-readable contract 가 아니다. 자동화된 도구가 probe 결과를 파싱
|
||||||
|
하려면 별도 JSON 출력 모드를 추가해야 한다 (현재 미구현).
|
||||||
|
|
||||||
|
PASS/FAIL 줄의 `[v]` / `[x]` 접두사는 CI grep 용 anchor 로 안정 보장.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
### A1. Probe 를 별도 bench 로 등록 (`@bench(name="probe")`)
|
||||||
|
|
||||||
|
기각. probe 는 bench 가 아니라 verification 도구로 의도된다 — sweep / 분석
|
||||||
|
용 multi-engine 실행과 invariant PASS/FAIL 출력이 본질이며, ADR-0045 의
|
||||||
|
"단일 디바이스 + 단일 RuntimeContext" bench 모델과 맞지 않는다.
|
||||||
|
|
||||||
|
### A2. monotonicity 위반 시 exit code 1
|
||||||
|
|
||||||
|
기각 (현재). 인간 검사 도구 위주로 의도되어 있어 PASS/FAIL 줄을 출력하고
|
||||||
|
exit 0 로 종료. CI 가 violation 으로 fail 하길 원하면 별도 wrapper 가
|
||||||
|
`grep "\[x\]"` 결과로 판단하면 됨. 후속으로 strict-mode flag (`--strict`)
|
||||||
|
도입 가능.
|
||||||
|
|
||||||
|
### A3. probe 의 case 정의를 외부 YAML 로
|
||||||
|
|
||||||
|
기각 (현재). 8개 case (4 H2D + 4 D2H + 5 PE DMA — 합 13개) 는 코드에
|
||||||
|
하드코딩되어 있고 의미가 토폴로지 mesh 구조에 단단히 묶여 있다. 외부
|
||||||
|
YAML 로 옮기면 cube 인덱스의 의미 (4, 8, 12 / 1, 4, 15) 를 별도로 문서화
|
||||||
|
해야 하므로 응집도 손실. 케이스 추가가 잦아지면 그때 별도 ADR 로 도입.
|
||||||
|
|
||||||
|
### A4. multi-flow contention 측정 추가
|
||||||
|
|
||||||
|
기각 (probe 범위 밖). D8 에서 명시한 single-flow 격리 모델이 probe 의 핵심
|
||||||
|
의도. multi-flow contention 은 ADR-0033 latency model 의 다른 영역으로,
|
||||||
|
별도 도구 또는 별도 case category 로 처리.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- probe 의 case catalog (D1) 와 측정 단위 (D2/D3) 가 ADR-level 에서 명시
|
||||||
|
되어, 새 traffic 카테고리 추가 시 어떤 표 포맷을 따라야 하는지 분명.
|
||||||
|
- formula vs actual 의 컬럼 의미 (D4) 가 굳어져, probe 결과를 보고 "왜
|
||||||
|
Drain% 가 5% 인가 / 70% 인가" 같은 질문을 빠르게 ADR-0033 가정 점검으로
|
||||||
|
연결 가능.
|
||||||
|
- invariant 자동 체크 (D5) 가 ADR 에 굳어져, 향후 latency 모델 변경 시
|
||||||
|
monotonicity / D2H≥H2D 회귀를 probe 가 즉시 잡아낸다는 안전망 정착.
|
||||||
|
- D8 의 case 간 격리가 명시되어, probe 결과를 single-flow 측정으로 안전
|
||||||
|
하게 해석 가능. multi-flow 측정이 필요해지면 별도 도구 트랙이 필요함이
|
||||||
|
분명.
|
||||||
|
- A2 의 strict-mode flag 가 후속 작업 후보로 기록되어, CI 통합 요구 시
|
||||||
|
최소 추가 작업으로 도입 가능.
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
# ADR-0050: CCL Algorithm Module Contract — `ccl/algorithms/*.py`
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted (2026-05-22).
|
||||||
|
|
||||||
|
`src/kernbench/ccl/algorithms/` 디렉터리 안의 모듈이 AHBM CCL backend
|
||||||
|
(ADR-0047) 에서 collective algorithm 으로 사용되려면 갖춰야 할 인터페이스,
|
||||||
|
kernel 시그너처, 그리고 새 알고리즘 추가 절차를 명시한다. ADR-0047 D3 가
|
||||||
|
"algorithm 모듈은 `kernel`, `kernel_args`, optional `TOPO_NAME_TO_KIND` 를
|
||||||
|
expose 해야 한다" 라고만 한 줄로 언급하나, **algorithm 모듈 작성자가 따라야
|
||||||
|
할 contract** 는 ADR-level 에서 정리된 적이 없다. ADR-0045 가 bench 모듈
|
||||||
|
contract 를 다루는 것과 짝을 이룬다.
|
||||||
|
|
||||||
|
## First action (제일 처음에 하는 일)
|
||||||
|
|
||||||
|
알고리즘 모듈이 import 되는 시점은 두 가지다:
|
||||||
|
|
||||||
|
1. **AHBM backend 진입**: 사용자 코드가 `dist.init_process_group(backend="ahbm")`
|
||||||
|
를 호출하면, `AhbmCCLBackend.__init__` 안에서 `self._algo_module =
|
||||||
|
importlib.import_module(self._merged["module"])` 가 실행된다. 이때 모듈
|
||||||
|
레벨에서 가장 먼저 일어나는 일:
|
||||||
|
- `SIP_TOPO_RING/TORUS/MESH` 같은 정수 상수가 모듈 namespace 에 노출.
|
||||||
|
- `TOPO_NAME_TO_KIND` 사전이 모듈 namespace 에 노출 — backend 가
|
||||||
|
`topo_map = getattr(self._algo_module, "TOPO_NAME_TO_KIND", None)` 로
|
||||||
|
조회.
|
||||||
|
- `kernel_args` 함수 정의 — 호출 시 호출자가 사용.
|
||||||
|
- `allreduce_intercube_multidevice` 같은 알고리즘 함수 정의.
|
||||||
|
- 모듈 마지막 줄에서 `kernel = allreduce_intercube_multidevice` 로
|
||||||
|
alias 가 노출.
|
||||||
|
|
||||||
|
2. **ccl.yaml install 단계**: `kernbench.ccl.install.install_ipcq` 가 호출
|
||||||
|
되어 IPCQ neighbor table 을 푸시할 때 같은 알고리즘 모듈이 import 됨.
|
||||||
|
|
||||||
|
즉, **algorithm 모듈의 첫 일은 "topology-kind 상수, `TOPO_NAME_TO_KIND`
|
||||||
|
사전, `kernel_args` 함수, 그리고 `kernel` alias 를 모듈 namespace 에 노출
|
||||||
|
하는 것"** 이다. 모든 노출은 import-time 부수효과로 충분하며 별도 초기화
|
||||||
|
함수 호출이 필요하지 않다.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
`AhbmCCLBackend` (ADR-0047) 는 process group 초기화 시점에 `ccl.yaml` 의
|
||||||
|
`defaults.algorithm` (또는 사용자가 지정한 알고리즘 이름) 으로부터 모듈
|
||||||
|
경로를 얻어 dynamic import 한다. backend 는 그 모듈로부터 다음 4 가지를
|
||||||
|
기대한다:
|
||||||
|
|
||||||
|
- `kernel`: collective 의 진입 함수.
|
||||||
|
- `kernel_args(world_size, n_elem, cube_w=, cube_h=) -> tuple`: kernel 에
|
||||||
|
넘길 위치 인자 묶음.
|
||||||
|
- `TOPO_NAME_TO_KIND` (optional): `topology.yaml` 의 `sips.topology`
|
||||||
|
문자열 (예: `"ring_1d"`, `"torus_2d"`, `"mesh_2d_no_wrap"`) 을 정수
|
||||||
|
상수로 매핑하는 dict.
|
||||||
|
- (간접) IPCQ neighbor table 설치: `configure_sfr_intercube_multisip` 가
|
||||||
|
알고리즘 모듈의 `TOPO_NAME_TO_KIND` 와 `cube_w/h` 를 보고 SFR 을 결정.
|
||||||
|
|
||||||
|
현재 코퍼스의 유일한 algorithm 모듈은 `lrab_hierarchical_allreduce.py`
|
||||||
|
(248 줄) 이다. 이름은 "**l**eft-**r**ight **a**lternating **b**roadcast
|
||||||
|
**hierarchical allreduce**". 향후 `ring_allreduce`, `tree_allreduce`,
|
||||||
|
`broadcast` 같은 모듈이 추가될 때마다 이 contract 를 따라야 일관된
|
||||||
|
디스패치가 가능하다.
|
||||||
|
|
||||||
|
이 contract 가 ADR-level 에 없으면:
|
||||||
|
|
||||||
|
- 새 algorithm 작성자가 ADR-0047 D3 의 한 줄 만으로 시그너처를 추론해야.
|
||||||
|
- kernel 함수 인자 순서 (특히 `t_ptr, n_elem, cube_w, cube_h, n_sips,
|
||||||
|
sip_rank, sip_topo_kind, sip_topo_w, sip_topo_h, tl`) 의 의미가 코드
|
||||||
|
grep 없이는 명확하지 않다.
|
||||||
|
- `kernel_args` 가 어떤 인자를 받고 어떤 tuple 을 돌려줘야 하는지 관례
|
||||||
|
로만 굳어진다.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### D1. algorithm 모듈은 4 가지 public symbol 을 노출한다
|
||||||
|
|
||||||
|
```python
|
||||||
|
# src/kernbench/ccl/algorithms/<name>.py
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
# (필수) topology-kind 상수 — 알고리즘 내부에서 사용
|
||||||
|
SIP_TOPO_RING = 0
|
||||||
|
SIP_TOPO_TORUS = 1
|
||||||
|
SIP_TOPO_MESH = 2
|
||||||
|
|
||||||
|
# (선택) topology 이름 → kind 매핑. backend 가 ccl.yaml/topology 의
|
||||||
|
# 문자열 SIP topology 를 정수로 변환하는 데 사용.
|
||||||
|
TOPO_NAME_TO_KIND = {
|
||||||
|
"ring_1d": SIP_TOPO_RING,
|
||||||
|
"torus_2d": SIP_TOPO_TORUS,
|
||||||
|
"mesh_2d_no_wrap": SIP_TOPO_MESH,
|
||||||
|
}
|
||||||
|
|
||||||
|
# (필수) kernel 인자 빌더
|
||||||
|
def kernel_args(world_size: int, n_elem: int, *, cube_w: int = 4, cube_h: int = 4) -> tuple:
|
||||||
|
return (n_elem, cube_w, cube_h, world_size)
|
||||||
|
|
||||||
|
# (필수) kernel 함수 (`tl=...` 키워드를 통해 TLContext 가 주입됨)
|
||||||
|
def my_allreduce_kernel(t_ptr, n_elem, cube_w, cube_h, n_sips,
|
||||||
|
sip_rank, sip_topo_kind, sip_topo_w, sip_topo_h, *, tl):
|
||||||
|
...
|
||||||
|
|
||||||
|
# (필수) kernel alias — backend 가 `module.kernel` 로 접근
|
||||||
|
kernel = my_allreduce_kernel
|
||||||
|
```
|
||||||
|
|
||||||
|
- `kernel` alias 는 backend 가 직접 호출하는 entry point 다. 함수 이름이
|
||||||
|
무엇이든 (`allreduce_intercube_multidevice` 처럼) `module.kernel = fn`
|
||||||
|
으로 노출해야 한다.
|
||||||
|
- `kernel_args` 가 없으면 backend 가 알고리즘 인자를 만들 방법이 없다.
|
||||||
|
signature 는 D2 참고.
|
||||||
|
- `TOPO_NAME_TO_KIND` 가 없으면 backend 는 `sip_topo_kind = 0` 으로
|
||||||
|
fallback 한다. 단일 topology 만 지원하는 알고리즘이라면 생략 가능.
|
||||||
|
|
||||||
|
### D2. `kernel_args` 시그너처 — `(world_size, n_elem, *, cube_w, cube_h)`
|
||||||
|
|
||||||
|
```python
|
||||||
|
def kernel_args(world_size: int, n_elem: int, *,
|
||||||
|
cube_w: int = 4, cube_h: int = 4) -> tuple:
|
||||||
|
return (n_elem, cube_w, cube_h, world_size)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **위치 인자**: `world_size` (= rank 수), `n_elem` (= 단일 shard 의
|
||||||
|
element 수, f16 기준).
|
||||||
|
- **키워드 인자**: `cube_w`, `cube_h` (= cube mesh 크기). default 는
|
||||||
|
4×4 — `topology.yaml` 의 `sip.cube_mesh` 기본값과 정합.
|
||||||
|
- **반환**: kernel 의 위치 인자 순서대로 묶은 tuple.
|
||||||
|
|
||||||
|
backend 의 `all_reduce` 가 호출 시:
|
||||||
|
|
||||||
|
```python
|
||||||
|
kernel_args_tuple = self._algo_module.kernel_args(
|
||||||
|
self._world_size, n_elem, cube_w=eff_cube_w, cube_h=eff_cube_h,
|
||||||
|
)
|
||||||
|
extra_args = (sip_rank, sip_topo_kind, sip_topo_w, sip_topo_h)
|
||||||
|
pending = self.ctx.launch(
|
||||||
|
self._merged["algorithm"], kernel_fn, tensor,
|
||||||
|
*kernel_args_tuple, *extra_args, _defer_wait=True,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
즉 kernel 의 최종 위치 인자는: `(tensor_ptr, *kernel_args_tuple,
|
||||||
|
sip_rank, sip_topo_kind, sip_topo_w, sip_topo_h)` 이며, 거기에 `tl=...` 가
|
||||||
|
키워드로 자동 주입된다. `kernel_args` 가 돌려주는 tuple 의 길이/순서는
|
||||||
|
**kernel signature 와 1:1 일치** 해야 한다.
|
||||||
|
|
||||||
|
### D3. `kernel` 함수 시그너처 — 정형화된 9 + tl 인자
|
||||||
|
|
||||||
|
권장 시그너처:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def my_kernel(
|
||||||
|
t_ptr: int, # VA base of the row-wise-sharded tensor on this SIP
|
||||||
|
n_elem: int, # element count per cube tile (or per shard)
|
||||||
|
cube_w: int, # cube mesh width (kernel_args 에서 옴)
|
||||||
|
cube_h: int, # cube mesh height (kernel_args 에서 옴)
|
||||||
|
n_sips: int, # world_size 와 동일 (rank = SIP, ADR-0024)
|
||||||
|
sip_rank: int, # 이 SIP 의 rank
|
||||||
|
sip_topo_kind: int, # TOPO_NAME_TO_KIND lookup 결과
|
||||||
|
sip_topo_w: int, # SIP mesh width (ring_1d 면 0)
|
||||||
|
sip_topo_h: int, # SIP mesh height (ring_1d 면 0)
|
||||||
|
*, tl, # TLContext (auto-injected)
|
||||||
|
) -> None:
|
||||||
|
```
|
||||||
|
|
||||||
|
`kernel_args` 가 다른 위치 인자 순서를 채택하더라도, kernel 의 **마지막
|
||||||
|
4 개 위치 인자는 항상 `(sip_rank, sip_topo_kind, sip_topo_w, sip_topo_h)`**
|
||||||
|
이며 backend 가 `extra_args` 로 append 한다 (ADR-0047 D5). 이 4 개 인자는
|
||||||
|
사용자 정의 algorithm 도 받아야 하지만, 알고리즘이 single-SIP 이라면
|
||||||
|
그냥 무시하면 된다.
|
||||||
|
|
||||||
|
`tl` 은 위치 인자가 아닌 키워드로 주입된다 — `RuntimeContext.launch` 가
|
||||||
|
kernel 호출 직전에 `tl=tl_ctx` 를 추가한다. 따라서 kernel signature 의
|
||||||
|
`tl` 은 keyword-only (`*, tl`) 또는 마지막 키워드 매개변수 형태여야
|
||||||
|
한다.
|
||||||
|
|
||||||
|
### D4. kernel body 의 자유도와 제약
|
||||||
|
|
||||||
|
kernel body 안에서 사용 가능한 표면: ADR-0046 D3 의 모든 `tl.*` primitive.
|
||||||
|
|
||||||
|
특히 자주 쓰이는 패턴:
|
||||||
|
|
||||||
|
- `cube_id = tl.program_id(axis=1)` — 이 PE 가 속한 cube 인덱스.
|
||||||
|
- `pe_addr = t_ptr + cube_id * nbytes` — cube-별 tile 의 VA 계산.
|
||||||
|
- `acc = tl.load(pe_addr, shape=(n_elem,), dtype="f16")` — local 데이터
|
||||||
|
로드.
|
||||||
|
- `tl.send(dir=...)` / `tl.recv(dir=..., shape=, dtype=)` — IPCQ
|
||||||
|
collective.
|
||||||
|
- `acc = acc + recv` — TensorHandle 산술 연산자 (ADR-0046 D4).
|
||||||
|
- `tl.store(pe_addr, acc)` — 결과 저장.
|
||||||
|
|
||||||
|
kernel body 는 일반 Python 함수이며, branching/looping 자유. 단:
|
||||||
|
|
||||||
|
- SimPy `yield` 또는 `async` 금지 (ADR-0046 D1).
|
||||||
|
- TensorHandle 의 `.data` 직접 접근 금지 — phase 1 timing 모델은
|
||||||
|
데이터 의존을 모른다 (ADR-0020 의 2-pass 분리).
|
||||||
|
- kernel 실행은 deterministic 해야 한다 — 같은 입력으로 두 번 실행하면
|
||||||
|
같은 op 시퀀스 발사. random / external IO 금지.
|
||||||
|
|
||||||
|
### D5. SIP topology semantics — `sip_topo_kind` 의 의미
|
||||||
|
|
||||||
|
backend 가 `topology.yaml` 의 `system.sips.topology` 문자열을 algorithm
|
||||||
|
모듈의 `TOPO_NAME_TO_KIND` 로 lookup 하여 `sip_topo_kind` 정수로 변환.
|
||||||
|
algorithm 은 이 정수를 보고 분기:
|
||||||
|
|
||||||
|
```python
|
||||||
|
if sip_topo_kind == SIP_TOPO_RING:
|
||||||
|
acc = _inter_sip_ring(...)
|
||||||
|
elif sip_topo_kind == SIP_TOPO_TORUS:
|
||||||
|
acc = _inter_sip_torus_2d(...)
|
||||||
|
elif sip_topo_kind == SIP_TOPO_MESH:
|
||||||
|
acc = _inter_sip_mesh_2d(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
각 topology branch 는 IPCQ direction 이름 (예: `"global_E"`, `"W"`, `"S"`,
|
||||||
|
`"N"`) 을 통해 peer 와 통신. direction 의 의미는 ADR-0023/0025 가 정의
|
||||||
|
하며, `configure_sfr_intercube_multisip` 가 IPCQ neighbor table 을 그에
|
||||||
|
맞춰 설치한다.
|
||||||
|
|
||||||
|
algorithm 모듈은 자기가 지원하지 않는 topology kind 가 들어오면 silent
|
||||||
|
no-op 으로 두기보다 명시적으로 `raise ValueError(f"unsupported topology
|
||||||
|
kind {sip_topo_kind}")` 하는 것을 권장 — 실수로 backend 에 잘못 dispatch
|
||||||
|
된 경우 빠르게 fail.
|
||||||
|
|
||||||
|
### D6. ccl.yaml 의 algorithm entry 구조
|
||||||
|
|
||||||
|
algorithm 모듈은 `ccl.yaml` 의 entry 와 짝을 이룬다 (ADR-0023 D10 +
|
||||||
|
ADR-0047 D3):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
defaults:
|
||||||
|
algorithm: lrab_hierarchical_allreduce
|
||||||
|
n_elem: 8
|
||||||
|
|
||||||
|
algorithms:
|
||||||
|
lrab_hierarchical_allreduce:
|
||||||
|
module: kernbench.ccl.algorithms.lrab_hierarchical_allreduce
|
||||||
|
# optional: world_size override
|
||||||
|
# optional: per-algorithm parameters consumed by configure_sfr_intercube_multisip
|
||||||
|
```
|
||||||
|
|
||||||
|
- `module`: full Python module path. backend 의 `importlib.import_module`
|
||||||
|
가 이 문자열을 그대로 사용.
|
||||||
|
- `world_size` (optional): 명시되면 topology fallback 을 override
|
||||||
|
(ADR-0047 D2).
|
||||||
|
- algorithm-specific parameters 는 `configure_sfr_intercube_multisip` 가
|
||||||
|
소비.
|
||||||
|
|
||||||
|
새 algorithm 추가 시:
|
||||||
|
|
||||||
|
1. `src/kernbench/ccl/algorithms/<name>.py` 작성 (D1 컨벤션).
|
||||||
|
2. `ccl.yaml` 의 `algorithms` 섹션에 entry 추가.
|
||||||
|
3. (필요 시) `kernbench.ccl.sfr_config` 에 SFR 설치 분기 추가.
|
||||||
|
4. test 추가 (예: `tests/sccl/test_<name>.py`, ADR-0043 의 eval harness
|
||||||
|
확장).
|
||||||
|
|
||||||
|
### D7. legacy "rank = flat PE index" 모드
|
||||||
|
|
||||||
|
ADR-0047 D2 가 명시한 `ccl.yaml` 의 `world_size` override 경로는 legacy
|
||||||
|
"rank = flat PE index" 테스트가 사용한다. algorithm 모듈은 이 모드 에서도
|
||||||
|
`n_sips=world_size` 만큼의 rank 가 들어옴을 가정하면 된다 — backend 가
|
||||||
|
rank↔(SIP, cube, PE) 매핑을 사전에 분리해 두므로 algorithm 본체에서는
|
||||||
|
modal 분기가 필요 없다.
|
||||||
|
|
||||||
|
단, single-cube workload 에서는 `cube_w=cube_h=1` 이 들어와 mesh-기반
|
||||||
|
phase 들이 skip 되도록 작성해야 한다 (`lrab_hierarchical_allreduce.py`
|
||||||
|
의 `single_cube = (cube_w == 1 and cube_h == 1)` 패턴 참고).
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
### A1. algorithm 모듈을 class 로 구조화 (`class Allreduce: kernel(...)` 등)
|
||||||
|
|
||||||
|
기각. Python 모듈 namespace 자체가 algorithm 의 identity 로 사용 중이며
|
||||||
|
(ADR-0047 D3 의 `importlib.import_module`), class 한 겹은 추가 indirection
|
||||||
|
만 늘리고 dispatch 측 코드를 두텁게 만든다. 모듈-레벨 free function
|
||||||
|
+ `kernel` alias 패턴이 충분히 명확.
|
||||||
|
|
||||||
|
### A2. `kernel_args` 를 명시적 dataclass 로 typing
|
||||||
|
|
||||||
|
기각 (현재). algorithm 마다 인자 갯수가 다른 것이 정상이며, dataclass 한
|
||||||
|
종류를 강제하면 다양한 algorithm 간 호환이 어려워진다. tuple 반환은 simple
|
||||||
|
하고 backend 측 `*kernel_args_tuple` unpacking 과 깨끗이 맞물린다.
|
||||||
|
algorithm 별 자체 타입 강도가 필요해지면 그 algorithm 모듈 안에서 NamedTuple
|
||||||
|
사용은 자유.
|
||||||
|
|
||||||
|
### A3. SFR 설치를 algorithm 모듈 안으로
|
||||||
|
|
||||||
|
기각. SFR 설치 (`configure_sfr_intercube_multisip`) 는 topology + algorithm
|
||||||
|
모두를 보고 IPCQ neighbor table 을 설치하는 cross-module 결정이라, algorithm
|
||||||
|
모듈 내부보다 `kernbench.ccl.sfr_config` 같은 전용 위치가 자연스럽다. D6 의
|
||||||
|
"필요 시 sfr_config 분기 추가" 워크플로우가 책임 분리 측면에서 더 명확.
|
||||||
|
|
||||||
|
### A4. algorithm name 을 모듈 namespace 에 자동 등록 (decorator)
|
||||||
|
|
||||||
|
기각. ADR-0045 (bench) 와 달리 algorithm 은 ccl.yaml entry 와 직접 묶여
|
||||||
|
있어 추가 등록 레지스트리가 중복이다. `module` 문자열 매핑 하나면 충분.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- ADR-0047 D3 의 한 줄 contract 가 D1–D7 의 작성자-친화적 가이드로 확장
|
||||||
|
되어, 새 algorithm 추가 시 시그너처를 grep 으로 추론할 필요 없음.
|
||||||
|
- D3 의 9 + tl 인자 시그너처가 표준화되어, backend 의 `extra_args` append
|
||||||
|
(ADR-0047 D5) 와 자연스럽게 맞물림. 향후 single-SIP-only algorithm 도
|
||||||
|
4 개의 sip_* 인자를 받아야 함이 명시.
|
||||||
|
- D5 의 fail-loud 권장으로, ccl.yaml 의 topology 가 algorithm 미지원
|
||||||
|
topology 로 잘못 설정되면 backend 가 silent wrong-result 가 아닌
|
||||||
|
ValueError 로 fail.
|
||||||
|
- D6 의 단계별 추가 절차가 명시되어, 새 algorithm 추가가 sfr_config /
|
||||||
|
test / ccl.yaml 어디까지 손대야 하는지 분명.
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
# ADR-0051: Routing Helper API — `AddressResolver` + `PathRouter`
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted (2026-05-22).
|
||||||
|
|
||||||
|
`policy/routing/router.py` 가 노출하는 두 helper 클래스
|
||||||
|
(`AddressResolver`, `PathRouter`) 의 모든 public API, 인자, 반환 값,
|
||||||
|
그리고 네 가지 다른 adjacency graph 의 사용처를 명시한다. ADR-0002 가
|
||||||
|
routing distance 와 ordering, bypass 규칙을 정의하나, **helper API 표면
|
||||||
|
자체** 는 ADR-level 에 정리된 적이 없다.
|
||||||
|
|
||||||
|
## First action (제일 처음에 하는 일)
|
||||||
|
|
||||||
|
### `AddressResolver(graph)`
|
||||||
|
|
||||||
|
생성 즉시 다음 두 가지를 캐시한다:
|
||||||
|
|
||||||
|
1. `self._node_ids = set(graph.nodes)` — 모든 node id 의 set (lookup 용).
|
||||||
|
2. `self._hbm_slice_bytes = hbm_total_gb * (1 << 30) // slices_per_cube` —
|
||||||
|
`graph.spec.cube.memory_map` 으로부터 산출 (기본 `48 GB / 8 slices = 6
|
||||||
|
GB`). 이 값이 `resolve()` 가 HBM PA 의 `hbm_offset` 에서 `pe_id` 를
|
||||||
|
복원하는 데 쓰인다.
|
||||||
|
|
||||||
|
즉, **AddressResolver 의 첫 일은 "전체 node id 집합과 HBM slice 크기를
|
||||||
|
미리 계산해 두는 것"** 이다. graph 자체는 보유하지 않는다.
|
||||||
|
|
||||||
|
### `PathRouter(graph)`
|
||||||
|
|
||||||
|
생성 즉시 **네 개의 별도 adjacency graph 를 동시 구축**한다:
|
||||||
|
|
||||||
|
1. `self._adj_all`: 모든 edge 포함 (component-to-component routing 용).
|
||||||
|
2. `self._adj`: `kind != "command"` 인 edge 만 (PE DMA / 일반 data path).
|
||||||
|
3. `self._adj_mcpu_dma`: `_MCPU_DMA_EXCLUDE = {"pe_internal",
|
||||||
|
"pe_to_router"}` 를 제외 (M_CPU DMA 가 PE pipeline 노드로 잘못 라우팅
|
||||||
|
되지 않게).
|
||||||
|
4. `self._adj_local`: `_UCIE_KINDS` 8 종을 제외 (cube-local routing 용 —
|
||||||
|
UCIe 가 zero-distance bus 처럼 보여 Dijkstra 가 mesh 보다 선호하는
|
||||||
|
것을 막음).
|
||||||
|
|
||||||
|
각 그래프는 `defaultdict(list)` of `(neighbor, weight)` 형태이며,
|
||||||
|
`edge.routing_weight_mm or edge.distance_mm` 이 weight 로 쓰인다.
|
||||||
|
|
||||||
|
즉, **PathRouter 의 첫 일은 "topology edge 들을 4개의 다른 정책으로 동시
|
||||||
|
분류하여 4 개의 인접 리스트로 구축하는 것"**. 매 `find_*()` 호출 시 적절
|
||||||
|
한 그래프를 골라 Dijkstra 를 돌린다.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
`policy/routing/router.py` 는 다음 두 책임을 함께 수행한다:
|
||||||
|
|
||||||
|
- **이름 매핑**: 토폴로지 명명 규칙 (`sip{S}.cube{C}.<comp>`,
|
||||||
|
`sip{S}.io{I}.pcie_ep` 등) 의 단일 소유자. 컴포넌트 / probe / IPCQ
|
||||||
|
install / runtime API 가 이름 문자열을 직접 만들지 않고 helper 를 호출.
|
||||||
|
- **경로 결정**: edge 의 `kind` 에 따른 정책 분리. 같은 src→dst 라도
|
||||||
|
routing 의도 (PE DMA vs M_CPU DMA vs general component routing) 에 따라
|
||||||
|
다른 adjacency 를 사용해야 결과가 달라진다.
|
||||||
|
|
||||||
|
이 helper API 가 코드 전반에서 광범위하게 소비되는데도 (probe.py /
|
||||||
|
distributed.py / install.py / 각종 component / tests), ADR-level 에서
|
||||||
|
**정확한 시그너처 / 반환 의미 / 어떤 adjacency 를 쓰는지** 가 한 곳에
|
||||||
|
정리되어 있지 않다. 본 ADR 이 그 빈자리를 채운다.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### D1. `AddressResolver` 의 5 개 public API
|
||||||
|
|
||||||
|
#### D1.1. `resolve(addr: PhysAddr) -> str`
|
||||||
|
|
||||||
|
`PhysAddr` 인스턴스를 토폴로지의 destination node id 로 변환.
|
||||||
|
|
||||||
|
```
|
||||||
|
addr.kind == "hbm" → f"sip{s}.cube{d}.hbm_ctrl.pe{pe_id}"
|
||||||
|
where pe_id = addr.hbm_offset // self._hbm_slice_bytes (ADR-0017 D4/D9)
|
||||||
|
|
||||||
|
addr.kind == "pe_resource":
|
||||||
|
addr.unit_type == PE → f"sip{s}.cube{d}.pe{addr.pe_id}.pe_tcm"
|
||||||
|
addr.unit_type == SRAM → f"sip{s}.cube{d}.sram"
|
||||||
|
addr.unit_type == MCPU → f"sip{s}.cube{d}.m_cpu"
|
||||||
|
그 외 → RoutingError("unsupported unit_type")
|
||||||
|
|
||||||
|
다른 kind → RoutingError("unsupported address kind")
|
||||||
|
```
|
||||||
|
|
||||||
|
산출된 node id 가 `self._node_ids` 에 없으면 `RoutingError(f"node {node_id}
|
||||||
|
not found in topology")`. 즉, address 의 syntax 가 valid 해도 topology 에
|
||||||
|
실제로 매핑되는 노드가 없으면 fail-loud.
|
||||||
|
|
||||||
|
#### D1.2. `find_m_cpu(sip, cube) -> str`
|
||||||
|
|
||||||
|
`f"sip{sip}.cube{cube}.m_cpu"`. 없으면 `RoutingError`.
|
||||||
|
|
||||||
|
#### D1.3. `find_pcie_ep(sip, io_id="io0") -> str`
|
||||||
|
|
||||||
|
`f"sip{sip}.{io_id}.pcie_ep"`. 없으면 `RoutingError`.
|
||||||
|
|
||||||
|
#### D1.4. `find_io_cpu(sip, io_id="io0") -> str`
|
||||||
|
|
||||||
|
`f"sip{sip}.{io_id}.io_cpu"`. 없으면 `RoutingError`.
|
||||||
|
|
||||||
|
#### D1.5. `find_all_pcie_eps() -> list[str]`
|
||||||
|
|
||||||
|
전 SIP 의 PCIE_EP node id 를 정렬된 리스트로 반환. `endswith(".pcie_ep")`
|
||||||
|
필터링. cross-SIP IPCQ 가 모든 PCIE_EP 를 enumerate 할 때 사용.
|
||||||
|
|
||||||
|
명명 규칙 (`sip{S}.cube{C}.<comp>`, `sip{S}.{io_id}.<comp>`) 의 단일
|
||||||
|
소유자가 이 클래스다 (ADR-0015 D4). 토폴로지 빌더가 같은 명명 규칙으로
|
||||||
|
노드를 만들고, 컴포넌트는 이름 문자열을 절대 직접 구성하지 않는다 —
|
||||||
|
모두 helper 를 거친다.
|
||||||
|
|
||||||
|
### D2. `PathRouter` 의 4 개 adjacency graph
|
||||||
|
|
||||||
|
생성자가 한 번에 구축. edge `kind` 가 정책을 결정:
|
||||||
|
|
||||||
|
| graph | 제외 edge kinds | 용도 |
|
||||||
|
|-------------------|-----------------------------------------------|--------------------------------------------|
|
||||||
|
| `_adj_all` | (none) | M_CPU↔NOC command 포함, IO_CPU/M_CPU routing |
|
||||||
|
| `_adj` | `"command"` | PE DMA / 일반 data path |
|
||||||
|
| `_adj_mcpu_dma` | `"pe_internal"`, `"pe_to_router"` | M_CPU DMA (PE pipeline 우회) |
|
||||||
|
| `_adj_local` | `_UCIE_KINDS` (`ucie_internal`, `ucie_conn_to_router`, `router_to_ucie_conn`, `ucie_conn_to_noc`, `noc_to_ucie_conn`, `ucie_mesh`, `io_to_cube`, `cube_to_io`) | same-cube routing (UCIe bus 우회) |
|
||||||
|
|
||||||
|
각 그래프는 `dict[node_id, list[(neighbor, weight)]]` 이며, weight 는
|
||||||
|
`edge.routing_weight_mm or edge.distance_mm`. command edge 의 routing
|
||||||
|
영향력을 명시적으로 가르고, UCIe 의 "0-distance bus" 가 mesh 보다 선호
|
||||||
|
되는 것을 막기 위한 `_adj_local` 분리가 ADR-0017 D7 의 cross-PE-slice
|
||||||
|
mesh-distance 요구와 정합.
|
||||||
|
|
||||||
|
### D3. `PathRouter` 의 6 개 public API (+ 2 backward-compat)
|
||||||
|
|
||||||
|
#### D3.1. `find_path(src_pe: str, dst_node: str) -> list[str]`
|
||||||
|
|
||||||
|
**PE DMA routing**. `src_pe` 는 PE prefix (예: `"sip0.cube0.pe0"`) 이며,
|
||||||
|
함수가 `.pe_dma` 를 자동으로 prepend 하여 실제 시작 노드를
|
||||||
|
`"sip0.cube0.pe0.pe_dma"` 로 설정.
|
||||||
|
|
||||||
|
cube-local 여부 (`_same_cube`) 에 따라 adjacency 선택:
|
||||||
|
|
||||||
|
- **same-cube** (src 와 dst 가 `sip{S}.cube{C}.` prefix 공유):
|
||||||
|
`_adj_local` 사용. UCIe 우회를 막아 cross-PE-slice 가 mesh 거리를 정확
|
||||||
|
히 지불 (ADR-0017 D7).
|
||||||
|
- **cross-cube**: `_adj` 사용. UCIe 가 자연스럽게 cross-cube path 의
|
||||||
|
최적 선택지로 포함됨.
|
||||||
|
|
||||||
|
#### D3.2. `find_path_with_distance(src_pe, dst_node) -> tuple[list[str], float]`
|
||||||
|
|
||||||
|
D3.1 과 동일한 adjacency 정책을 사용하나, 결과로 `(path, total_distance)`
|
||||||
|
를 함께 반환. probe / 분석 도구에서 distance 메트릭이 필요할 때 사용.
|
||||||
|
|
||||||
|
#### D3.3. `find_mcpu_dma_path(m_cpu_id: str, dst_hbm_id: str) -> list[str]`
|
||||||
|
|
||||||
|
**M_CPU DMA path**. cube 가 같으면 `_adj_local` (mesh 안에서 마무리), 다르
|
||||||
|
면 `_adj_all` (UCIe 경유). `_MCPU_DMA_EXCLUDE` 가 PE pipeline 노드를 자동
|
||||||
|
배제하므로, M_CPU 가 PE 의 내부 stage 를 거쳐 routing 되는 잘못된 경로가
|
||||||
|
나오지 않는다.
|
||||||
|
|
||||||
|
#### D3.4. `find_memory_path(src: str, dst: str) -> list[str]`
|
||||||
|
|
||||||
|
`pcie_ep → io_noc → cube → router mesh → hbm_ctrl` 같은 직접 메모리
|
||||||
|
경로. `_adj_mcpu_dma` 를 사용하여 `pe_internal` 및 `pe_to_router` edge
|
||||||
|
를 제외 — host-issued read/write 가 PE pipeline 으로 새지 않게 보장.
|
||||||
|
probe (ADR-0049 D1 의 H2D/D2H case) 에서 직접 호출.
|
||||||
|
|
||||||
|
#### D3.5. `find_node_path(src: str, dst: str) -> list[str]`
|
||||||
|
|
||||||
|
임의의 두 node 사이의 path. **command edge 포함** (`_adj_all` 사용). M_CPU
|
||||||
|
↔ NOC 같은 command-kind link 를 거쳐야 하는 IoCpuComponent /
|
||||||
|
MCpuComponent 등이 호출.
|
||||||
|
|
||||||
|
#### D3.6. backward-compat shims
|
||||||
|
|
||||||
|
- `_dijkstra(start, goal) -> list[str]` — `_run_dijkstra(self._adj, …)`
|
||||||
|
의 thin wrapper.
|
||||||
|
- `_dijkstra_with_dist(start, goal) -> tuple[list[str], float]` — distance
|
||||||
|
포함 버전.
|
||||||
|
|
||||||
|
언더스코어 prefix 에서 보듯이 내부 API 인 척이지만 기존 테스트가 직접
|
||||||
|
호출. 새 코드는 D3.1–D3.5 를 사용하고, 이 두 shim 은 deprecation 후보.
|
||||||
|
|
||||||
|
### D4. Dijkstra 알고리즘 — single-source shortest path
|
||||||
|
|
||||||
|
`_run_dijkstra_with_dist(adj, start, goal)`:
|
||||||
|
|
||||||
|
- `heapq` priority queue.
|
||||||
|
- `best: dict[node, distance]` — 노드별 최단 거리 캐시.
|
||||||
|
- `prev: dict[node, predecessor]` — path reconstruction.
|
||||||
|
- weight 는 `routing_weight_mm or distance_mm`. UCIe 처럼 routing_weight 가
|
||||||
|
명시되어 distance 와 다른 edge 가 있으므로 weight 분리가 의도된 것.
|
||||||
|
|
||||||
|
`start == goal` 은 빠른 path `([start], 0.0)` 반환. 도달 불가는
|
||||||
|
`RoutingError(f"no path from {start} to {goal}")`.
|
||||||
|
|
||||||
|
이 알고리즘은 **deterministic** 하다 — 같은 graph + start/goal 이면 같은
|
||||||
|
경로. 이는 SPEC R1 의 "Routing MUST be deterministic" 요구와 정합. tie-
|
||||||
|
break 는 `heapq` 의 push 순서를 따른다 (Python list 순서가 deterministic).
|
||||||
|
|
||||||
|
### D5. helper API 의 단일 소유자 원칙
|
||||||
|
|
||||||
|
다음 정보는 오직 router.py 안에서만 결정된다:
|
||||||
|
|
||||||
|
- 명명 규칙: `sip{S}.cube{C}.<comp>`, `sip{S}.{io_id}.<comp>`,
|
||||||
|
`sip{S}.cube{C}.hbm_ctrl.pe{pe_id}`.
|
||||||
|
- adjacency 정책: 어떤 edge kind 가 어떤 그래프에 포함되는가.
|
||||||
|
- HBM slice 크기로부터 PE id 복원 방법.
|
||||||
|
- Dijkstra의 weight 결정 (`routing_weight_mm or distance_mm`).
|
||||||
|
|
||||||
|
이 단일 소유자 원칙이 깨지면 (예: 컴포넌트가 자체적으로 `f"sip{s}..."` 를
|
||||||
|
구성하기 시작하면) 명명 규칙 변경 시 영향 범위가 폭발한다. ADR-0015 D4 의
|
||||||
|
정신과 정렬.
|
||||||
|
|
||||||
|
### D6. helper API consumer 의 목록
|
||||||
|
|
||||||
|
본 helper 가 노출하는 메소드를 호출하는 곳을 명시 (현재 코퍼스 기준):
|
||||||
|
|
||||||
|
- `probes/probe.py` (ADR-0049): `find_pcie_ep`, `find_io_cpu`,
|
||||||
|
`find_m_cpu`, `find_node_path`, `find_mcpu_dma_path`,
|
||||||
|
`find_memory_path`, `find_path`, `resolve`.
|
||||||
|
- `runtime_api/distributed.py` (ADR-0047): 간접 (engine 내부 routing).
|
||||||
|
- `ccl/install.py` (ADR-0023): `find_all_pcie_eps`, `resolve`.
|
||||||
|
- `sim_engine/event_log.py`: probe 와 유사하게 `find_pcie_ep`,
|
||||||
|
`find_memory_path`.
|
||||||
|
- `components/builtin/m_cpu.py`, `components/builtin/io_cpu.py`:
|
||||||
|
`find_node_path`, `find_mcpu_dma_path`.
|
||||||
|
- 각종 tests (test_routing.py, test_cross_sip_routing.py 등): D3.1–D3.5
|
||||||
|
대부분.
|
||||||
|
|
||||||
|
새 consumer 가 추가될 때 본 ADR 의 D1/D3 가 그 의도에 맞는 메소드가
|
||||||
|
이미 있는지 / 새 메소드를 추가해야 하는지 1차 판단의 기준이 된다.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
### A1. 단일 adjacency graph + edge-kind filter 동적 적용
|
||||||
|
|
||||||
|
기각. 매 `find_*()` 마다 graph filtering 을 다시 하면 Dijkstra 의 cache
|
||||||
|
locality 와 성능이 떨어진다. 4 개 그래프 동시 구축 (D2) 은 메모리 비용
|
||||||
|
이 작고 (edge ≤ 수만 건 규모), 호출 시점에 정책 선택이 O(1) 로 결정.
|
||||||
|
|
||||||
|
### A2. adjacency 분리를 edge 의 `kind` 가 아닌 별도 metadata 로
|
||||||
|
|
||||||
|
기각. edge `kind` 는 이미 topology builder 가 부여하며 (ADR-0015 D4 +
|
||||||
|
ADR-0017), 별도 metadata 를 도입하면 두 시스템이 동기화되어야 하는
|
||||||
|
중복이 생긴다.
|
||||||
|
|
||||||
|
### A3. Dijkstra 대신 BFS + uniform weight
|
||||||
|
|
||||||
|
기각. routing_weight_mm 이 edge 별로 다른 (mesh link / UCIe / IO-internal)
|
||||||
|
현실에서 BFS 는 hop 수 최소화일 뿐 latency / distance 최단을 보장하지
|
||||||
|
않는다. SPEC R1 + R2 의 결정적·정확한 routing 요구에 어긋남.
|
||||||
|
|
||||||
|
### A4. helper API 를 클래스 메서드가 아닌 모듈 함수로
|
||||||
|
|
||||||
|
기각. 두 클래스 (`AddressResolver`, `PathRouter`) 가 각각 cache 상태
|
||||||
|
(`_node_ids`, `_hbm_slice_bytes`, 4 adjacency graphs) 를 보유해야 하며,
|
||||||
|
같은 graph 인스턴스에 여러 routing 질의가 발생한다. 모듈 함수는 매 호출
|
||||||
|
시 state 를 다시 만들거나 global 로 두어야 해서 안전성/성능 저하.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- 컴포넌트 / probe / IPCQ install / runtime API 가 모두 router.py 의
|
||||||
|
helper 만 호출하면 명명 규칙 변경 (예: `.io0.` → `.iochiplet0.`) 이
|
||||||
|
단 한 파일 수정으로 끝남 (D5).
|
||||||
|
- D2 의 4 그래프 분리가 ADR 에 굳어져, 새 edge kind 가 추가될 때 (예:
|
||||||
|
Inter-die UCIe link 의 새 kind) 어느 그래프에 포함시킬지 결정의 명확
|
||||||
|
한 기준 제공.
|
||||||
|
- D3.1 의 cube-local vs cross-cube 분기 (ADR-0017 D7) 가 명시되어, 향후
|
||||||
|
routing 동작을 변경하려는 사람이 어느 adjacency 를 건드려야 할지 안다.
|
||||||
|
- D6 의 consumer 목록이 명시되어, helper API 변경 시 PR review 범위가
|
||||||
|
분명. backward-compat shim (D3.6) 의 deprecation 후보가 식별됨.
|
||||||
@@ -0,0 +1,352 @@
|
|||||||
|
# ADR-0052: OpLog + MemoryStore Schemas — sim_engine internals
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted (2026-05-22).
|
||||||
|
|
||||||
|
`sim_engine/op_log.py` 의 `OpRecord` 스키마와 `OpLogger` 의 record_start /
|
||||||
|
record_end / record_copy 동작, 그리고 `sim_engine/memory_store.py` 의
|
||||||
|
`MemoryStore` 가 사용하는 (space, addr) 주소공간 namespace 와 read/write
|
||||||
|
의미를 명시한다. ADR-0020 (2-pass data execution) 가 두 인프라의 존재를
|
||||||
|
선언하나, **레코드의 정확한 필드와 의미** 는 ADR-level 에서 정리되지
|
||||||
|
않았고 ADR-0046 D3.2 (`tl.store` visibility), ADR-0023 D9 (IPCQ copy
|
||||||
|
record) 등 여러 ADR 이 이들의 동작에 의존하고 있다.
|
||||||
|
|
||||||
|
## First action (제일 처음에 하는 일)
|
||||||
|
|
||||||
|
### `OpLogger(memory_store=None)`
|
||||||
|
|
||||||
|
생성 즉시 다음 3 가지 필드 초기화:
|
||||||
|
|
||||||
|
1. `self._records: list[OpRecord] = []` — 누적된 op record.
|
||||||
|
2. `self._pending: dict[int, dict] = {}` — `id(msg)` 키로 partial record
|
||||||
|
(record_start 시점에 만들어졌고 record_end 가 아직 안 온 것).
|
||||||
|
3. `self._memory_store = memory_store` — 옵션 MemoryStore reference.
|
||||||
|
math op 의 input 스냅샷 + dma_write 의 HBM source 스냅샷 캡처에 사용.
|
||||||
|
|
||||||
|
생성 시점에는 records / pending 모두 비어 있으며, `record_*` 호출이
|
||||||
|
순차적으로 데이터를 누적한다.
|
||||||
|
|
||||||
|
### `MemoryStore()`
|
||||||
|
|
||||||
|
생성 즉시 `self._storage: dict[str, dict[int, np.ndarray]] = {}` 단 하나
|
||||||
|
의 필드 초기화. 두 단계 dict (`space → addr → ndarray`) 이며 lazy 하게
|
||||||
|
필요한 space 가 생길 때마다 inner dict 가 채워진다.
|
||||||
|
|
||||||
|
즉, **두 인프라의 첫 일은 "비어 있는 누적 buffer + space-별 sparse dict
|
||||||
|
를 만들어 두는 것"** 이다. 첫 record / write 가 실제로 도착하면 그때
|
||||||
|
필드가 채워지기 시작한다.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
ADR-0020 (2-pass data execution) 의 D2/D5/D7 가 다음을 선언:
|
||||||
|
|
||||||
|
- Phase 1 (timing) 동안 `ComponentBase._on_process_start/end` hook 이
|
||||||
|
`OpLogger.record_start/end` 를 호출하여 모든 data op 의 시간 + 메타
|
||||||
|
데이터를 기록.
|
||||||
|
- Phase 2 (data) 가 op_log 를 t_start 순으로 재생하여 실 데이터 결과를
|
||||||
|
계산.
|
||||||
|
- 데이터 페이로드 자체는 `MemoryStore` 에 (space, addr) 키로 보관.
|
||||||
|
|
||||||
|
ADR-0023 D9 (IPCQ atomic write), ADR-0027 (Megatron TP scratch
|
||||||
|
overwrite 회피), ADR-0046 D3.2 (`tl.store` visibility) 등 후속 ADR 들이
|
||||||
|
op_log 와 MemoryStore 의 동작에 의존하지만, **정확한 record 필드 / space
|
||||||
|
이름 / 스냅샷 시점** 은 코드 grep 으로만 확인 가능하다. 본 ADR 이 이를
|
||||||
|
정리한다.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### D1. `OpRecord` 스키마 — 7 개 필드
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass
|
||||||
|
class OpRecord:
|
||||||
|
t_start: float
|
||||||
|
t_end: float
|
||||||
|
component_id: str
|
||||||
|
op_kind: str # "memory" | "gemm" | "math" | "unknown"
|
||||||
|
op_name: str # e.g. "dma_read", "gemm_f16", "exp",
|
||||||
|
# "TileToken/DMA_READ", "composite_gemm",
|
||||||
|
# "ipcq_copy"
|
||||||
|
params: dict[str, Any]
|
||||||
|
dependency_ids: list[int] = field(default_factory=list)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`t_start` / `t_end`**: SimPy 시간 (float ns). `t_start` 는 component
|
||||||
|
가 op 를 시작한 시점, `t_end` 는 완료 시점. duration = `t_end - t_start`.
|
||||||
|
- **`component_id`**: op 가 발생한 node id (예:
|
||||||
|
`"sip0.cube0.pe0.pe_dma"`).
|
||||||
|
- **`op_kind`**: 4 가지 중 하나. Phase 2 DataExecutor 가 이 값으로 분기.
|
||||||
|
- **`op_name`**: 디버깅 / 분석용 사람-친화 이름. TileToken 일 경우
|
||||||
|
`"TileToken/{stage_type}"` (예: `"TileToken/DMA_READ"`) 로 stage 를
|
||||||
|
구분.
|
||||||
|
- **`params`**: op-종속 메타데이터 dict (D3 참고).
|
||||||
|
- **`dependency_ids`**: 현재 사용되지 않음 (default `[]`). 향후 cross-op
|
||||||
|
dependency 추적이 필요해질 때를 위한 자리.
|
||||||
|
|
||||||
|
### D2. `OpLogger.records` — t_start 정렬 보장
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def records(self) -> list[OpRecord]:
|
||||||
|
self._records.sort(key=lambda r: r.t_start)
|
||||||
|
return self._records
|
||||||
|
```
|
||||||
|
|
||||||
|
매 접근 시 `t_start` 로 stable sort. 즉 같은 t_start 인 record 들은 insertion
|
||||||
|
순서를 유지. ADR-0020 D5 의 "t_start stable ordering" 요구와 정합.
|
||||||
|
|
||||||
|
Phase 2 DataExecutor 는 항상 `records` property 를 통해 접근하므로,
|
||||||
|
record_end 호출이 t_start 와 다른 순서로 도착해도 (예: 짧은 op 가 긴
|
||||||
|
op 보다 늦게 시작했으나 먼저 끝남) 재정렬되어 일관된 시퀀스를 받는다.
|
||||||
|
|
||||||
|
### D3. op_name 별 `params` 스키마 (`_extract_op_info` 매핑)
|
||||||
|
|
||||||
|
#### D3.1. `op_kind="memory", op_name="dma_read"` (DmaReadCmd)
|
||||||
|
|
||||||
|
```python
|
||||||
|
{"src_addr": int, "nbytes": int, "handle_id": str}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### D3.2. `op_kind="memory", op_name="dma_write"` (DmaWriteCmd)
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"src_space": str, # handle.space ("tcm"|"hbm"|"sram"), default "tcm"
|
||||||
|
"src_addr": int, # handle.addr
|
||||||
|
"shape": tuple, "dtype": str,
|
||||||
|
"dst_space": "hbm", # DmaWrite 는 항상 HBM 으로
|
||||||
|
"dst_addr": int,
|
||||||
|
"nbytes": int,
|
||||||
|
"handle_id": str,
|
||||||
|
# record_end 시점에 src_space == "hbm" 이면 snapshot 추가 (D4)
|
||||||
|
"snapshot": np.ndarray | None,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### D3.3. `op_kind="gemm", op_name=f"gemm_{dtype_a}"` (GemmCmd)
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"src_a_addr": int, "src_b_addr": int, "dst_addr": int,
|
||||||
|
"shape_a": tuple, "shape_b": tuple, "shape_out": tuple,
|
||||||
|
"dtype_in": str, "dtype_out": str,
|
||||||
|
"m": int, "k": int, "n": int,
|
||||||
|
# ADR-0027: per-operand + output spaces 보존
|
||||||
|
"src_a_space": str, "src_b_space": str, "dst_space": str,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### D3.4. `op_kind="math", op_name=msg.op` (MathCmd; op = "exp", "sum", "add", "where" 등)
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"input_addrs": list[int], # 입력 핸들들의 addr
|
||||||
|
"input_shapes": list[tuple],
|
||||||
|
"input_spaces": list[str],
|
||||||
|
"input_dtypes": list[str],
|
||||||
|
"dst_addr": int, "dst_space": str,
|
||||||
|
"shape_out": tuple, "dtype": str,
|
||||||
|
"axis": int | None, # reduction 인 경우만 의미 있음
|
||||||
|
# record_end 시점에 모든 input 의 스냅샷이 채워짐 (D4)
|
||||||
|
"input_snapshots": list[np.ndarray | None],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### D3.5. `op_kind="gemm" or "math", op_name=f"composite_{op}"` (CompositeCmd)
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"op": str, # "gemm" | "math"
|
||||||
|
"out_addr": int, "out_nbytes": int,
|
||||||
|
# op == "gemm" 인 경우 GemmCmd 와 같은 필드 추가:
|
||||||
|
"src_a_addr": int, "src_b_addr": int,
|
||||||
|
"shape_a": tuple, "shape_b": tuple,
|
||||||
|
"dtype_in": str, "dtype_out": str,
|
||||||
|
"src_a_space": str, "src_b_space": str,
|
||||||
|
"dst_space": "hbm", "dst_addr": int, # = out_addr
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`op == "gemm"` 이면 `op_kind = "gemm"`, 아니면 `"math"`. Phase 2 측에서
|
||||||
|
GemmCmd 와 동일 path 로 재생되도록 alias.
|
||||||
|
|
||||||
|
#### D3.6. `op_kind="memory", op_name="ipcq_copy"` (record_copy 전용 경로)
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"src_space": str, "src_addr": int,
|
||||||
|
"dst_space": str, "dst_addr": int,
|
||||||
|
"shape": tuple, "dtype": str, "nbytes": int,
|
||||||
|
"snapshot": np.ndarray | None, # 호출자가 전달, 없으면 record_copy 가 fresh read
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`PE_DMA._handle_ipcq_inbound` (ADR-0023 D9) 가 이 record 를 발사하여 IPCQ
|
||||||
|
slot 의 inbound copy 를 Phase 2 가 재생 가능하게 한다. 이 record 는
|
||||||
|
`record_start` / `record_end` 를 거치지 않고 직접 `record_copy()` 로 push.
|
||||||
|
|
||||||
|
#### D3.7. `op_kind="unknown", op_name=type(msg).__name__`
|
||||||
|
|
||||||
|
`_extract_op_info` 가 인식 못 한 message 의 fallback. params = `{}`.
|
||||||
|
DataExecutor 가 이 op_kind 를 만나면 skip — Phase 2 replay 에 영향 없음.
|
||||||
|
|
||||||
|
### D4. snapshot 캡처 시점
|
||||||
|
|
||||||
|
`OpLogger._memory_store` 가 set 되어 있을 때 record_end 가 다음을 수행:
|
||||||
|
|
||||||
|
- **math op**: 모든 input addr/shape/space/dtype 으로
|
||||||
|
`self._memory_store.read(...)` 를 호출하여 `params["input_snapshots"]` 에
|
||||||
|
ndarray copy 첨부. read 실패 시 None.
|
||||||
|
- **dma_write op**: `src_space == "hbm"` 인 경우에만 source HBM 의
|
||||||
|
스냅샷을 `params["snapshot"]` 에 첨부. TCM source 는 **명시적으로
|
||||||
|
스킵** — TCM (PE scratch) 은 Phase 2 math/gemm 재생이 다시 채우므로,
|
||||||
|
Phase-1-time snapshot 을 잡으면 이전 kernel 의 stale 데이터를 잡을 위험
|
||||||
|
(ADR-0027 postmortem: TP gemm → all_reduce race).
|
||||||
|
- **ipcq_copy**: `record_copy` 호출자가 `snapshot=token.data` 같이 in-flight
|
||||||
|
스냅샷을 전달. 없으면 record_copy 가 fresh read 로 대체 시도.
|
||||||
|
|
||||||
|
스냅샷은 `.copy()` 가 호출되어 (`ndarray.copy()` 가 fresh allocation) 이후
|
||||||
|
storage mutation 으로부터 안전. ADR-0027 의 "cross-PE Phase 2 ordering"
|
||||||
|
race 회피의 근간.
|
||||||
|
|
||||||
|
`memory_store` 가 None 인 경우 (Phase 1 timing-only 모드) 스냅샷 단계는
|
||||||
|
전부 skip. record 의 timing 정보만 보존되며 데이터 replay 는 불가능.
|
||||||
|
|
||||||
|
### D5. TileToken 처리 — record_start 가 stage 정보를 캡처
|
||||||
|
|
||||||
|
ADR-0014 D6 의 self-routing tile token (pipeline 모드) 은 stage_idx 가
|
||||||
|
record_end 시점에 이미 advance 되어 있을 수 있다 (TileToken 이 다음
|
||||||
|
component 로 이동하면서 next stage 의 params 를 캐시). 따라서:
|
||||||
|
|
||||||
|
`record_start` 가 다음을 `pending[id(msg)]["snap"]` 에 미리 저장:
|
||||||
|
|
||||||
|
```python
|
||||||
|
snap["stage_type"] = stage.stage_type.name # "DMA_READ", "GEMM", 등
|
||||||
|
snap["stage_params"] = dict(stage.params) # 시점의 params 복사본
|
||||||
|
```
|
||||||
|
|
||||||
|
`record_end` 에서 이 snap 을 꺼내 params 에 merge:
|
||||||
|
|
||||||
|
- `params["stage_type"]` 가 final params 에 추가.
|
||||||
|
- `stage_params` 의 key 들이 (이미 있으면 보존) merge.
|
||||||
|
- `op_name == "TileToken"` 이면 `op_name = f"TileToken/{stage_type}"` 로
|
||||||
|
rewrite (예: `"TileToken/DMA_READ"`) — 같은 component 에서 발생한 서로
|
||||||
|
다른 stage 의 record 를 disambiguate.
|
||||||
|
|
||||||
|
이 메커니즘 덕분에 DMA_READ vs DMA_WRITE, FETCH vs STORE 가 같은 component
|
||||||
|
(예: pe_dma) 에서 발생하더라도 reporting 측에서 구분 가능.
|
||||||
|
|
||||||
|
### D6. `MemoryStore` — (space, addr) 두 단계 dict
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MemoryStore:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._storage: dict[str, dict[int, np.ndarray]] = {}
|
||||||
|
|
||||||
|
def write(self, space, addr, data): self._storage[space][addr] = data
|
||||||
|
def read(self, space, addr, shape=None, dtype=None) -> np.ndarray: ...
|
||||||
|
def has(self, space, addr) -> bool: ...
|
||||||
|
def snapshot(self) -> MemoryStore: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### D6.1. space namespace
|
||||||
|
|
||||||
|
문자열 키. 표준 값:
|
||||||
|
|
||||||
|
- `"hbm"`: HBM 데이터 (deploy_tensor + Phase 2 dma_write 결과).
|
||||||
|
- `"tcm"`: PE-로컬 TCM (Phase 2 math/gemm 결과).
|
||||||
|
- `"sram"`: cube-level SRAM (ADR-0023 D9.7 IPCQ slot tier).
|
||||||
|
|
||||||
|
다른 space (예: `"reg"`) 도 자유롭게 허용 — `_storage` 가 lazy dict 라
|
||||||
|
새 space 가 write 호출과 함께 자동 생성.
|
||||||
|
|
||||||
|
#### D6.2. address keying
|
||||||
|
|
||||||
|
`addr` 는 정수. **physical address (PA) 또는 virtual address (VA)** 일 수
|
||||||
|
있다 — MemoryStore 자체는 address space 의 의미를 모르고 그저 키로 쓴다.
|
||||||
|
Phase 1 의 `MemoryWriteMsg` 는 PA + VA 둘 다 write (`_create_tensor` 에서
|
||||||
|
PA 로 zero-init, VA base 로도 zero-init), Phase 2 는 op_log 가 captured
|
||||||
|
한 address 로 read/write.
|
||||||
|
|
||||||
|
`addr` 의 의미는 호출자가 결정한다 — `MemoryStore` 는 lookup 만 제공.
|
||||||
|
|
||||||
|
#### D6.3. read/write 의미 — reference store (no copy)
|
||||||
|
|
||||||
|
`write(space, addr, data)`: `data` ndarray 의 reference 를 저장. **copy
|
||||||
|
하지 않음**. 호출자가 같은 ndarray 를 이후 mutate 하면 stored value 도
|
||||||
|
변경된다.
|
||||||
|
|
||||||
|
`read(space, addr, shape=None, dtype=None)`: 저장된 ndarray 의 reference
|
||||||
|
반환. `shape` 또는 `dtype` 이 제공되면:
|
||||||
|
|
||||||
|
- `dtype != stored.dtype`: `arr.view(np_dtype)` 로 reinterpret cast (no
|
||||||
|
copy).
|
||||||
|
- `shape != stored.shape`: `nbytes` 가 일치하면 `arr.reshape(shape)` (view).
|
||||||
|
- `nbytes` 불일치: `ValueError`.
|
||||||
|
|
||||||
|
데이터를 안전하게 분리하려면 호출자가 `arr.copy()` 호출. ADR-0027 의
|
||||||
|
race 회피가 op_log snapshot 단계에서 명시적 copy 를 강제하는 이유.
|
||||||
|
|
||||||
|
#### D6.4. `has(space, addr) -> bool`
|
||||||
|
|
||||||
|
해당 키의 존재 여부만 확인. 데이터 인스턴스화는 안 함.
|
||||||
|
|
||||||
|
#### D6.5. `snapshot() -> MemoryStore`
|
||||||
|
|
||||||
|
shallow copy. inner dict 의 새 인스턴스를 만들되 ndarray reference 는
|
||||||
|
공유. Phase 2 초기화 시점에 Phase 1 의 store 를 fork 하여 Phase 2 의
|
||||||
|
mutation 이 Phase 1 의 다른 사용처에 영향을 주지 않게 분리하는 데 사용.
|
||||||
|
|
||||||
|
### D7. op_log 가 SimPy 단일-스레드를 가정한다
|
||||||
|
|
||||||
|
`OpLogger` 의 `_records`, `_pending` 은 lock 없이 사용. SimPy 가 single-
|
||||||
|
threaded 라 `record_start` → `record_end` 사이에 다른 thread 가 끼어들
|
||||||
|
수 없다는 가정.
|
||||||
|
|
||||||
|
향후 multi-process kernbench (ADR-0047 D6) 가 도입되면 OpLogger 도 process
|
||||||
|
별로 분리되어야 함이 명시. 단일 OpLogger 인스턴스가 multiple process 의
|
||||||
|
record 를 받지 못한다.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
### A1. op_log 를 SQLite / parquet 같은 외부 store 로
|
||||||
|
|
||||||
|
기각 (현재). in-memory list 가 Phase 1 → Phase 2 의 핸드오프 latency 를
|
||||||
|
최소화한다. 외부화는 long-running batch run 에서 의미가 있겠으나, 현재
|
||||||
|
single-run 워크로드 에서는 overhead 만 추가.
|
||||||
|
|
||||||
|
### A2. snapshot 을 record_start 시점에 캡처
|
||||||
|
|
||||||
|
기각. record_start 시점은 input 이 아직 채워지지 않은 상황 (예: math
|
||||||
|
op 의 input 이 직전 op 의 output 일 때) 이 흔하다. record_end 가 정확한
|
||||||
|
시점.
|
||||||
|
|
||||||
|
### A3. MemoryStore 를 component-별 store 로 분리
|
||||||
|
|
||||||
|
기각. (space, addr) 키가 이미 충분히 disambiguation 을 제공하며, component
|
||||||
|
별 분리는 cross-PE IPCQ copy (ADR-0023 D9) 가 source/destination 양쪽
|
||||||
|
store 를 접근해야 하는 케이스를 복잡하게 만든다.
|
||||||
|
|
||||||
|
### A4. op_log 에 cross-op dependency edge 명시
|
||||||
|
|
||||||
|
부분 채택. `dependency_ids` 필드가 OpRecord 에 자리 잡고 있지만 현재
|
||||||
|
사용되지 않음 (D1). Phase 2 DataExecutor 가 t_start 정렬 + secondary sort
|
||||||
|
(memory ops before math at same t_start) 로 ordering 을 결정하며, 명시적
|
||||||
|
dependency graph 가 필요해지면 이 필드가 채워질 자리. 현재는 ordering rule
|
||||||
|
이 충분하므로 미사용.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- ADR-0020 의 op_log / MemoryStore 선언이 D1–D6 의 구체 schema 로 확장
|
||||||
|
되어, Phase 2 DataExecutor 작성/수정 시 정확한 필드 의미를 grep 없이
|
||||||
|
ADR 에서 확인 가능.
|
||||||
|
- D3 의 op_name 별 params 스키마가 명시되어, 새 op (예: 새 reduction
|
||||||
|
type) 추가 시 `_extract_op_info` 분기 어디에 끼울지 명확.
|
||||||
|
- D4 의 snapshot 시점 차이 (math = input snapshot, dma_write = HBM-only
|
||||||
|
snapshot) 가 ADR 에 굳어져, ADR-0027 의 cross-PE race 회피 결정이 향후
|
||||||
|
refactor 에서 silently 깨지지 않음.
|
||||||
|
- D6.3 의 reference-store 의미가 명시되어, 호출자가 mutation safety 책임
|
||||||
|
을 인지. ADR-0027 의 explicit `.copy()` 패턴이 정당화됨.
|
||||||
|
- D7 의 single-thread 가정이 명시되어, multi-process kernbench (ADR-0047
|
||||||
|
D6 supersession 후보) 도입 시 OpLogger 분리가 필요함이 분명.
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
# ADR-0053: Topology Builder + Visualizer Algorithms
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted (2026-05-22).
|
||||||
|
|
||||||
|
`topology/builder.py`, `topology/mesh_gen.py`, `topology/visualizer.py` 가
|
||||||
|
함께 수행하는 토폴로지 컴파일·시각화 파이프라인의 핵심 알고리즘 선택
|
||||||
|
(placement-driven router attachment, mesh auto-layout, source_hash 캐시,
|
||||||
|
view projection, SVG rendering) 을 명시한다. ADR-0006 가 topology
|
||||||
|
compilation 의 high-level intent (compiled topology, distance extraction,
|
||||||
|
automatic diagram generation) 를 정의하나, **builder 가 실제로 어떤
|
||||||
|
알고리즘을 사용하는지** 는 코드 grep 으로만 확인 가능했다.
|
||||||
|
|
||||||
|
## First action (제일 처음에 하는 일)
|
||||||
|
|
||||||
|
`resolve_topology(path_str)` 가 호출되면 다음 4 단계가 순서대로 일어난다:
|
||||||
|
|
||||||
|
1. **경로 검증** (`builder.py::resolve_topology`):
|
||||||
|
`Path(path_str).expanduser().resolve()`, 존재 확인, file 여부 확인.
|
||||||
|
실패 시 `FileNotFoundError` 또는 `ValueError`.
|
||||||
|
2. **YAML 파싱** (`_read_spec`): `yaml.safe_load`. parse error 면 line/
|
||||||
|
column 정보 포함한 `ValueError`. dict 가 아니면 reject.
|
||||||
|
3. **mesh 자동 생성** (`mesh_gen.ensure_mesh_file`): topology yaml 과
|
||||||
|
같은 디렉터리에 `cube_mesh.yaml` 을 만들거나 (캐시 invalid 시) 재사용
|
||||||
|
(캐시 hit 시). 이 단계가 cube NoC 의 라우터 grid 와 부착 정보를 결정.
|
||||||
|
4. **graph 컴파일** (`_compile_graph`): system → IO chiplets → cubes →
|
||||||
|
inter-cube edges → IO↔cube edges → system↔IO edges 순으로 nodes/edges
|
||||||
|
를 누적, 그 다음 4 개의 view projection (system, sip, cube, pe) 을
|
||||||
|
생성하여 `TopologyGraph` 로 묶음.
|
||||||
|
|
||||||
|
즉, **topology compile 의 첫 일은 "topology.yaml 을 dict 로 읽고, 동일
|
||||||
|
디렉터리에 cube_mesh.yaml 을 생성/검증한 뒤, system→sip→cube→pe 순으로
|
||||||
|
flat graph + 4-view projection 을 만드는 것"** 이다.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
`topology/` 패키지의 책임:
|
||||||
|
|
||||||
|
- **builder.py** (1207 줄): topology.yaml 을 받아 `TopologyGraph` (nodes
|
||||||
|
+ edges + 4 view projections) 를 컴파일.
|
||||||
|
- **mesh_gen.py** (305 줄): cube NoC 의 라우터 grid 와 PE/UCIe/M_CPU/SRAM
|
||||||
|
부착 위치를 자동 결정하여 `cube_mesh.yaml` 로 캐시.
|
||||||
|
- **visualizer.py** (887 줄): `TopologyGraph` 로부터 SVG 다이어그램 4종
|
||||||
|
(system / sip / cube / pe) 을 생성.
|
||||||
|
|
||||||
|
ADR-0006 가 "topology compilation 의 결과는 distance metadata 와 diagram
|
||||||
|
generation 의 single source" 라는 high-level 결정을 정의하나, 구체 알고리즘
|
||||||
|
(예: placement-driven nearest-router attachment, HBM 제외 zone 산출,
|
||||||
|
source_hash 의 어떤 필드가 invalidation 을 트리거하는가) 은 ADR 에 없다.
|
||||||
|
|
||||||
|
특히 다음 결정들이 ADR-level 에 부재:
|
||||||
|
|
||||||
|
- 왜 mesh_gen 이 별도 파일 (`cube_mesh.yaml`) 로 캐시되는가?
|
||||||
|
- source_hash 가 어떤 필드를 포함하며, 어떤 변경이 재생성을 강제하는가?
|
||||||
|
- placement coordinate 가 cube 좌표가 아닌 mm 단위인 이유?
|
||||||
|
- HBM zone 제외와 UCIe N/S/E/W 분배가 mesh 안에서 어떻게 결정되는가?
|
||||||
|
- view projection 4 개 (system/sip/cube/pe) 의 추상화 레벨 차이?
|
||||||
|
|
||||||
|
이 ADR 이 이 결정들을 한 곳에 정리한다.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### D1. compile 파이프라인 — 6 단계
|
||||||
|
|
||||||
|
`_compile_graph(spec)`:
|
||||||
|
|
||||||
|
1. **시스템 노드 생성** (`_instantiate_system`): `fabric.switch0`, host CPU
|
||||||
|
등 system-level 노드 추가.
|
||||||
|
2. **per-SIP loop** (`for sip_id in range(system.sips.count)`):
|
||||||
|
- **IO chiplets** (`_instantiate_io_chiplets`): pcie_ep / io_cpu /
|
||||||
|
io_noc / io_ucie PHY / conn 노드 + 내부 양방향 edge 생성.
|
||||||
|
- **cube instantiation** (`_instantiate_cube`): cube_mesh.yaml 의 router
|
||||||
|
grid 를 토대로 cube-별 라우터, PE sub-components (pe_cpu, pe_dma,
|
||||||
|
pe_fetch_store, pe_gemm, pe_math, pe_mmu, pe_tcm, pe_scheduler,
|
||||||
|
pe_ipcq), m_cpu, sram, hbm_ctrl 인스턴스화 + 내부 edge 깔기.
|
||||||
|
- **inter-cube edges** (`_add_inter_cube_edges`): UCIe N/S/E/W mesh
|
||||||
|
edge.
|
||||||
|
- **IO ↔ cube edges** (`_add_io_to_cube_edges`): io_noc 와 cube 의
|
||||||
|
edge UCIe phy 사이 연결.
|
||||||
|
3. **switch ↔ IO edges** (`_add_system_to_io_edges`): `fabric.switch0`
|
||||||
|
와 각 SIP 의 `pcie_ep` 사이 양방향 edge (ADR-0038 D3 + ADR-0010 의
|
||||||
|
cross-SIP IPCQ 경로).
|
||||||
|
4. **view projections** 4 종 build:
|
||||||
|
- `_build_system_view(spec)` — Tray 레벨, SIP 들과 system switch.
|
||||||
|
- `_build_sip_view(spec)` — SIP 안의 cube mesh + IO chiplet.
|
||||||
|
- `_build_cube_view(spec)` — 단일 cube 안의 router grid + PE/M_CPU/SRAM/
|
||||||
|
HBM_CTRL 부착.
|
||||||
|
- `_build_pe_view(spec)` — 단일 PE 안의 9 sub-components + 내부 edge.
|
||||||
|
5. **TopologyGraph 리턴**: `TopologyGraph(spec, nodes, edges, system_view,
|
||||||
|
sip_view, cube_view, pe_view)`.
|
||||||
|
|
||||||
|
이 6 단계는 **순서가 의미를 가진다**: cubes 가 만들어진 후에야 inter-cube
|
||||||
|
edges 가 valid 한 src/dst 를 갖고, IO chiplet 이 먼저 만들어져야 IO ↔ cube
|
||||||
|
edge 가 그를 참조할 수 있다. 새 노드 종류를 끼울 때는 의존 관계를 보고
|
||||||
|
적절한 위치에 삽입해야 한다.
|
||||||
|
|
||||||
|
### D2. `cube_mesh.yaml` — 별도 파일 + source_hash 캐시
|
||||||
|
|
||||||
|
`mesh_gen.ensure_mesh_file(cube_spec, mesh_path)`:
|
||||||
|
|
||||||
|
1. `source_hash = _compute_source_hash(cube_spec)` 산출. 입력 필드:
|
||||||
|
- `geometry` (cube_mm.w/h 등).
|
||||||
|
- `pe_layout` (corners, pe_per_corner).
|
||||||
|
- `ucie.n_connections`.
|
||||||
|
- `memory_map.hbm_mapping_mode`.
|
||||||
|
- `placement` (m_cpu/sram pos_mm).
|
||||||
|
2. `mesh_path` (= `topology.yaml` 와 같은 디렉터리의 `cube_mesh.yaml`) 이
|
||||||
|
존재하고 `existing.source_hash == source_hash` 면 재사용 (캐시 hit).
|
||||||
|
3. 아니면 `_generate_mesh(cube_spec, source_hash)` 로 새 mesh 생성 후
|
||||||
|
yaml 로 저장.
|
||||||
|
|
||||||
|
별도 파일로 캐시하는 이유:
|
||||||
|
|
||||||
|
- mesh 생성은 PE/UCIe/router 부착 계산이 들어가 매번 다시 하기 무거움.
|
||||||
|
- 같은 cube spec 으로 여러 번 실행 시 동일 mesh 가 보장되어야 함.
|
||||||
|
- 사람이 직접 mesh 를 inspect / debug 할 수 있는 artifact 가 됨.
|
||||||
|
|
||||||
|
`source_hash` 가 list 한 5 개 필드가 mesh 형상을 결정하는 핵심이며, 그
|
||||||
|
외 (예: bandwidth, overhead_ns) 변경은 mesh 재생성을 트리거하지 않는다.
|
||||||
|
|
||||||
|
### D3. cube NoC mesh auto-layout 알고리즘
|
||||||
|
|
||||||
|
`_generate_mesh(cube_spec)`:
|
||||||
|
|
||||||
|
#### D3.1. 행/열 결정
|
||||||
|
|
||||||
|
- `pe_positions = _corner_pe_positions(cube_w, cube_h)`: 4 corner (NW/NE/
|
||||||
|
SW/SE) 마다 PE center 좌표 (mm). hardcoded `(1.5, 1.5)` / `(cube_w-1.5,
|
||||||
|
cube_h-1.5)` 패턴 + `pe_per_corner=2` 면 각 corner 에 2 PE 위치.
|
||||||
|
- `col_xs = _compute_col_positions(...)`: PE 들의 x 좌표 union + `max_spacing
|
||||||
|
= 3.0 mm` 보다 큰 gap 에 relay 컬럼 삽입.
|
||||||
|
- `row_ys, rows_per_half = _compute_row_positions(cube_h, n_connections,
|
||||||
|
pe_positions)`:
|
||||||
|
- `n_conn = max(n_connections, 2)` (hot path minimum).
|
||||||
|
- `rows_per_half = ceil(n_conn / 2)`.
|
||||||
|
- top 절반 + HBM 두 row + bottom 절반. HBM 은 `(cube_h/2 - 1.5, cube_h/2
|
||||||
|
+ 1.5)` 에 위치. PE rows 와 HBM rows 사이 `hbm_gap = 1.5 mm`.
|
||||||
|
|
||||||
|
#### D3.2. HBM 제외 zone
|
||||||
|
|
||||||
|
`hbm_row_start = rows_per_half`, `hbm_row_end = rows_per_half + 1`.
|
||||||
|
`hbm_col_start = n_cols // 2 - 1`, `hbm_col_end = n_cols // 2`.
|
||||||
|
|
||||||
|
이 (row, col) 사각형 안의 router 슬롯은 `None` 으로 마킹 (라우터 없음).
|
||||||
|
실제 HBM 컨트롤러는 별도 `hbm_ctrl.pe{X}` 노드로 ADR-0017 D9 의 per-PE
|
||||||
|
파티션 패턴을 따라 부착.
|
||||||
|
|
||||||
|
#### D3.3. PE 부착
|
||||||
|
|
||||||
|
각 corner 의 PE 들은 다음 row 에 매핑:
|
||||||
|
|
||||||
|
- Top half: NW → row 0, NE → row 1 (top_corners 안의 index).
|
||||||
|
- Bottom half: SW → row `hbm_row_end + 1`, SE → row `hbm_row_end + 2`.
|
||||||
|
|
||||||
|
각 PE 의 x 좌표가 가장 가까운 col 의 router 에 부착 (`min(range(n_cols),
|
||||||
|
key=lambda c: abs(col_xs[c] - pe_x))`). 부착 항목은 `pe{pe_idx}.dma`,
|
||||||
|
`pe{pe_idx}.cpu`, `pe{pe_idx}.hbm` 세 가지 (router 별 attach list 에 push).
|
||||||
|
|
||||||
|
#### D3.4. M_CPU / SRAM 부착 — nearest router by Euclidean distance
|
||||||
|
|
||||||
|
`placement.m_cpu.pos_mm` (default `[1.5, 5.5]`) 와 `placement.sram.pos_mm`
|
||||||
|
(default `[1.5, 8.5]`) 의 좌표에서 가장 가까운 router 를 Euclidean
|
||||||
|
distance 로 찾아 attach list 에 `"m_cpu"` / `"sram"` 추가.
|
||||||
|
|
||||||
|
#### D3.5. UCIe N/S/E/W 분배
|
||||||
|
|
||||||
|
`ucie_pe_rows = top_pe_rows + bot_pe_rows` (총 `2 * rows_per_half` 개).
|
||||||
|
|
||||||
|
- UCIe-E: 매 PE row 마다 rightmost col 의 router 에 `ucie_e.c{i}`.
|
||||||
|
- UCIe-W: leftmost col 의 router 에 `ucie_w.c{i}` (E 의 mirror).
|
||||||
|
- UCIe-N/S: PE column 들 중 절반을 좌측, 절반을 우측으로 나눠 top row /
|
||||||
|
bottom row 의 해당 col 에 부착.
|
||||||
|
|
||||||
|
각 UCIe connection 은 `c{i}` index 가 붙어 ucie_n_connections 만큼의 PHY
|
||||||
|
가 분산된다 (ADR-0017 D5+).
|
||||||
|
|
||||||
|
### D4. node 명명 규칙 — 단일 소유자
|
||||||
|
|
||||||
|
builder.py 는 다음 명명 규칙으로 노드를 만든다 (ADR-0051 D5 의 단일
|
||||||
|
소유자 원칙):
|
||||||
|
|
||||||
|
- `fabric.switch0` — system-level switch.
|
||||||
|
- `sip{S}.{io_id}.{pcie_ep|io_cpu|io_noc|io_ucie.{dir}|conn.{id}}` — IO
|
||||||
|
chiplet.
|
||||||
|
- `sip{S}.cube{C}.{m_cpu|sram|hbm_ctrl.pe{X}|noc.r{R}c{C}|...}` — cube 내부.
|
||||||
|
- `sip{S}.cube{C}.pe{P}.{pe_cpu|pe_dma|pe_fetch_store|pe_gemm|pe_math|pe_mmu|pe_tcm|pe_scheduler|pe_ipcq}` — PE sub-components.
|
||||||
|
|
||||||
|
이 명명 규칙을 변경하려면 builder.py 와 router.py (ADR-0051) 의 helper
|
||||||
|
양쪽이 함께 갱신되어야 한다. 컴포넌트는 명명 규칙을 직접 알지 못하고
|
||||||
|
helper 만 호출한다.
|
||||||
|
|
||||||
|
### D5. edge `kind` 분류
|
||||||
|
|
||||||
|
각 edge 가 부여받는 `kind` 가 라우팅 정책 (ADR-0051 D2) 의 입력. 주요
|
||||||
|
kind 값:
|
||||||
|
|
||||||
|
- `"pe_internal"` — PE 내부 sub-component 간.
|
||||||
|
- `"pe_to_router"` — PE_DMA ↔ cube NoC router.
|
||||||
|
- `"router_mesh"` — cube NoC router 간.
|
||||||
|
- `"router_to_hbm"`, `"router_to_mcpu"`, `"router_to_sram"`,
|
||||||
|
`"sram_to_router"` 등 — cube-attached component 간.
|
||||||
|
- `"ucie_internal"`, `"ucie_conn_to_router"`, `"router_to_ucie_conn"`,
|
||||||
|
`"ucie_conn_to_noc"`, `"noc_to_ucie_conn"`, `"ucie_mesh"` — UCIe 관련.
|
||||||
|
- `"io_internal"` — IO chiplet 내부.
|
||||||
|
- `"io_to_cube"`, `"cube_to_io"` — IO ↔ cube 경계.
|
||||||
|
- `"pcie"` — switch ↔ pcie_ep.
|
||||||
|
- `"command"` — control-plane only edges (M_CPU ↔ NOC 등; PE DMA path 에서
|
||||||
|
제외).
|
||||||
|
|
||||||
|
새 edge kind 를 추가하면 router.py 의 4 adjacency graph (ADR-0051 D2) 의
|
||||||
|
어느 카테고리에 속할지 결정해야 한다 — 그렇지 않으면 default 로 `_adj_all`
|
||||||
|
에만 포함되어 의도와 다른 routing 발생 가능.
|
||||||
|
|
||||||
|
### D6. view projection — 4 추상화 레벨
|
||||||
|
|
||||||
|
`TopologyGraph` 는 flat (nodes + edges) 외에 4 개의 view projection 을
|
||||||
|
보유:
|
||||||
|
|
||||||
|
- **system_view** (`_build_system_view`): Tray 레벨. SIP 박스들 + `fabric.
|
||||||
|
switch0`. PCIE 링크 표시. 외부 발표용 high-level overview.
|
||||||
|
- **sip_view** (`_build_sip_view`): 한 SIP 안. cube mesh + IO chiplet
|
||||||
|
(pcie_ep + io_cpu + io_noc). UCIe N/S/E/W 가 cube 간 연결로 보임.
|
||||||
|
- **cube_view** (`_build_cube_view`): 한 cube 안. router grid + PE/M_CPU/
|
||||||
|
SRAM/HBM_CTRL 부착 + UCIe PHY edge 부분. cube 내부 라우팅 / placement
|
||||||
|
진단용.
|
||||||
|
- **pe_view** (`_build_pe_view`): 한 PE 안. 9 sub-components + 내부 edge
|
||||||
|
(pe_internal kind). 자세한 PE 내부 dataflow 검토용.
|
||||||
|
|
||||||
|
view 는 spec 에서 `visualization.emit_views: [system, sip, cube]` 같이
|
||||||
|
선택적으로 출력 (ADR-0006). pe view 는 기본 출력에서 빠져 있으나 코드는
|
||||||
|
유지 (자세한 디버그용).
|
||||||
|
|
||||||
|
### D7. visualizer.py — SVG 다이어그램 출력
|
||||||
|
|
||||||
|
`emit_diagrams(graph, out_dir)` 가 모든 view 를 SVG 로 렌더. 핵심 함수:
|
||||||
|
|
||||||
|
- `_render_view_svg(view)` — 일반적인 view 렌더 (router grid 가 없는
|
||||||
|
경우).
|
||||||
|
- `_render_cube_view_svg(view, spec)` — cube view 전용 (HBM block 그리기,
|
||||||
|
router grid layout, PE/M_CPU/SRAM/HBM positioning).
|
||||||
|
- `_draw_node`, `_draw_edge` — 노드 / edge 의 시각적 표현.
|
||||||
|
- `_pick_scale`, `_compute_node_sizes` — 자동 스케일링.
|
||||||
|
|
||||||
|
visualizer 는 **derived artifact** (ADR-0006) 로 분류되며, 코드 변경 시
|
||||||
|
production check 대상이 아니다. CLAUDE.md 의 "Derived Artifacts" 항목과
|
||||||
|
정합.
|
||||||
|
|
||||||
|
### D8. spec 변경의 영향 범위
|
||||||
|
|
||||||
|
| spec 필드 | 영향 | mesh 재생성 |
|
||||||
|
|---------------------------------------|-------------------|-------------|
|
||||||
|
| `system.sips.count` | SIP 갯수, node 수 | No |
|
||||||
|
| `sip.cube_mesh.w/h` | cube mesh 형상 | No |
|
||||||
|
| `cube.geometry.cube_mm.w/h` | cube 크기 (mm) | **Yes** |
|
||||||
|
| `cube.pe_layout.corners/pe_per_corner`| PE 부착 위치 | **Yes** |
|
||||||
|
| `cube.ucie.n_connections` | UCIe PHY 분배 | **Yes** |
|
||||||
|
| `cube.memory_map.hbm_mapping_mode` | HBM 분배 모드 | **Yes** |
|
||||||
|
| `cube.placement` | M_CPU/SRAM 위치 | **Yes** |
|
||||||
|
| `cube.memory_map.*` (위 제외) | HBM 용량 / BW | No |
|
||||||
|
| `*.links.*.bw_gbs` | edge bandwidth | No |
|
||||||
|
| `*.attrs.overhead_ns` | 컴포넌트 latency | No |
|
||||||
|
|
||||||
|
위 표가 D2 의 `_compute_source_hash` 입력과 일치. mesh 재생성이 필요한
|
||||||
|
변경은 `cube_mesh.yaml` 의 source_hash 가 자동 invalidate.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
### A1. mesh 를 별도 캐시 파일 없이 매 compile 시 재생성
|
||||||
|
|
||||||
|
기각. 같은 spec 으로 여러 번 호출되는 케이스 (CLI run, probe, test) 마다
|
||||||
|
mesh 생성 비용을 다시 지불. 또한 사람이 mesh 를 inspect 할 수 있는 artifact
|
||||||
|
가 사라짐.
|
||||||
|
|
||||||
|
### A2. mesh 생성을 builder.py 에 합치기
|
||||||
|
|
||||||
|
기각 (현재). 305 줄 짜리 자체 알고리즘이며, mesh layout 의 결정 (placement-
|
||||||
|
driven router attachment, HBM exclusion zone) 이 builder 의 일반적인
|
||||||
|
node/edge 생성 책임과 다르다. 분리 유지가 단일 책임 원칙에 더 부합.
|
||||||
|
|
||||||
|
### A3. placement coordinate 를 cube 좌표 (col/row) 로 표현
|
||||||
|
|
||||||
|
기각. mm 단위 좌표가 시각화 측 (visualizer) 과 mesh layout 측 (nearest-
|
||||||
|
router 산출) 양쪽에서 일관되게 쓰인다. cube 좌표는 router grid 가 결정
|
||||||
|
되기 전까지는 정의되지 않으므로 placement 입력에 부적절.
|
||||||
|
|
||||||
|
### A4. view projection 을 lazy 하게 생성
|
||||||
|
|
||||||
|
기각 (현재). 4 개 view 의 생성 비용이 작고 (보통 < 100 ms), eager 생성이
|
||||||
|
`TopologyGraph` 를 통한 single source of truth 를 보장.
|
||||||
|
|
||||||
|
### A5. visualizer 출력 형식을 SVG 외 (PNG/PDF) 도
|
||||||
|
|
||||||
|
기각. SVG 가 vector + 텍스트 검색 가능 + 브라우저 직접 렌더가 가능한 가장
|
||||||
|
유연한 형식. PNG 변환이 필요하면 별도 도구 (rsvg-convert 등) 로 후처리.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- ADR-0006 의 high-level intent 가 D1–D7 로 구체화되어, topology 변경
|
||||||
|
영향을 D8 표로 빠르게 가늠 가능.
|
||||||
|
- D3 의 mesh auto-layout 알고리즘이 ADR-level 에서 굳어져, 추후 새 PE
|
||||||
|
부착 패턴 (예: HBM 의 6-zone 분할) 도입 시 어느 단계가 영향받는지 명확.
|
||||||
|
- D5 의 edge kind 목록과 D7 의 view 구조가 명시되어, 새 component 종류
|
||||||
|
추가 시 (builder + router + visualizer) 어디까지 손대야 하는지 PR
|
||||||
|
reviewer 가 한눈에 파악 가능.
|
||||||
|
- D2 의 source_hash invalidation 규칙이 명시되어, cube_mesh.yaml 이 stale
|
||||||
|
하게 남는 경우 (예: bw 값만 바꿨을 때) 가 정상 동작임이 분명.
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
# ADR-0054: 마일스톤 평가 bench — 자기완결적 sweep + figure bench
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted (2026-05-22).
|
||||||
|
|
||||||
|
ADR-0044(D1/D2)와 ADR-0045(D5)를 개정하고, ADR-0043/0044의 "로직이
|
||||||
|
`scripts/` + `tests/`에 산다" 배치를 대체한다: GEMM/allreduce 평가
|
||||||
|
하니스가 이제 사용자가 실행하여 모든 결과 + figure를 재생성하는
|
||||||
|
자기완결적 **bench**가 된다.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
ADR-0043(allreduce 평가)과 ADR-0044(GEMM 평가)는 각 하니스를 **sweep**
|
||||||
|
(수동 `scripts/` 드라이버, 또는 allreduce의 경우 parametrized 테스트
|
||||||
|
자체) + committed 데이터를 렌더링하는 **figure 테스트**로 분리했다.
|
||||||
|
따라서 sweep/render 로직은 `scripts/gemm_sweep.py`,
|
||||||
|
`tests/gemm/_gemm_plot_helpers.py`, `tests/sccl/_allreduce_helpers.py`에
|
||||||
|
존재했다.
|
||||||
|
|
||||||
|
마일스톤 요구사항("사용자가 *하나의 bench*를 실행해 모든 결과와 플롯을
|
||||||
|
생성하도록 allreduce + GEMM 평가를 리팩터")은 그 배치로는 충족 불가다:
|
||||||
|
bench는 production 코드이며 **`tests/`를 import할 수 없다**(ADR-0007 레이어
|
||||||
|
방향). 평가 로직은 bench에서 닿을 수 있도록 production으로 이동해야 했다.
|
||||||
|
|
||||||
|
선택한 home은 별도 `kernbench.eval` 패키지가 아니라 bench 모듈 자체다.
|
||||||
|
bench 파일은 임의의 모듈 레벨 코드를 가질 수 있으며, 하니스를 bench로
|
||||||
|
합치면 도메인당 파일 하나가 유지되고 패키지 레이어가 하나 줄어든다.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### D1. 두 마일스톤 bench가 평가 로직을 보유
|
||||||
|
|
||||||
|
- `src/kernbench/benches/milestone_1h_gemm.py` — GEMM shape×variant sweep
|
||||||
|
+ 세 figure renderer(`scripts/gemm_sweep.py` +
|
||||||
|
`tests/gemm/_gemm_plot_helpers.py`에서 이동).
|
||||||
|
- `src/kernbench/benches/milestone_1h_ccl.py` — distributed allreduce
|
||||||
|
드라이버, latency + buffer-kind sweep, topology diagram, FSIM 비교, 그리고
|
||||||
|
direct-launch 패리티 레퍼런스(`tests/sccl/_allreduce_helpers.py`에서 이동).
|
||||||
|
|
||||||
|
각 파일은 해당 도메인 평가 로직의 **단일 home**이다.
|
||||||
|
|
||||||
|
### D2. "평가 bench" 패턴 (ADR-0045 D5 확장)
|
||||||
|
|
||||||
|
ADR-0045 D5는 bench를 단일 구성(single-SIP, 또는 ADR-0024 multi-SIP CCL
|
||||||
|
예외)으로 고정했다. 본 ADR은 세 번째 패턴을 추가한다:
|
||||||
|
|
||||||
|
- **평가 bench**는 *여러* 구성을 구동하고 figure를 렌더링할 수 있다. 외부
|
||||||
|
`run_bench` 엔진 대신 sweep 지점마다 자체 `GraphEngine` /
|
||||||
|
`RuntimeContext`를 빌드한다.
|
||||||
|
- 그러면 외부 ctx에 제출된 handle이 없으므로, bench는 마지막에
|
||||||
|
**sentinel 텐서**(`torch.zeros((1, 1), …)`)를 제출하여 `run_bench`의
|
||||||
|
"최소 한 번 제출" 계약(ADR-0045 D4)을 만족시키고 CLI가 0으로 종료되게
|
||||||
|
한다.
|
||||||
|
|
||||||
|
### D3. 출력 위치
|
||||||
|
|
||||||
|
두 bench 모두 `src/kernbench/benches/1H_milestone_output/{gemm,ccl}/`에
|
||||||
|
쓴다(사용자 요청 — bench 옆 아티팩트). 디렉터리는 생성된 PNG/CSV/JSON만
|
||||||
|
보유하며(`.py`/`__init__.py` 없음), 따라서 eager-import audit(ADR-0045
|
||||||
|
첫 동작)이 무시한다 — `pkgutil.iter_modules`는 비-패키지 하위 디렉터리를
|
||||||
|
yield하지 않는다. `docs/diagrams/` 아티팩트처럼 **커밋된다**(원격에서
|
||||||
|
figure를 볼 수 있도록); bench 재실행 시 제자리에서 재생성된다.
|
||||||
|
|
||||||
|
### D4. GEMM 무거운 sweep — 기본은 fresh, `MILESTONE_FAST`로 재사용
|
||||||
|
|
||||||
|
`milestone-1h-gemm`은 기본적으로 전체 24-sim sweep을 실행한다(분 단위;
|
||||||
|
한 shape는 2048 tile). `MILESTONE_FAST=1`은 committed
|
||||||
|
`docs/diagrams/gemm_sweep.json`을 재사용하고 렌더링만 한다(초 단위). 이는
|
||||||
|
ADR-0044 D1/D2의 "무거운 sweep은 수동/`slow` 단계로 유지"를 뒤집는다:
|
||||||
|
bench 실행이 곧 재생성이다. slow 경로는 `@pytest.mark.slow` bench
|
||||||
|
테스트로 행사되고, fast 경로는 기본 실행된다.
|
||||||
|
|
||||||
|
### D5. 테스트 + 스크립트는 thin re-export shim으로 재사용 (단일 home 유지)
|
||||||
|
|
||||||
|
기존 figure 테스트와 `scripts/gemm_sweep.py` 진입점은 유지되며 이제 bench
|
||||||
|
모듈을 재사용한다:
|
||||||
|
|
||||||
|
- `tests/gemm/_gemm_plot_helpers.py` → renderer +
|
||||||
|
`GEMM_SWEEP_JSON`/`GEMM_PLOTS_DIR`/`ROOT`를
|
||||||
|
`kernbench.benches.milestone_1h_gemm`에서 re-export.
|
||||||
|
- `tests/sccl/_allreduce_helpers.py` → 드라이버 코어, config writer, sweep
|
||||||
|
상수, renderer, disk aggregator를 `kernbench.benches.milestone_1h_ccl`에서
|
||||||
|
re-export하고, **pytest 전용** 조각은 로컬 유지: `pytest.param` 행렬
|
||||||
|
(`CONFIGS` / `_sweep_params` / `_bk_params`)과 fixture 결합
|
||||||
|
`_run_distributed`(`monkeypatch.chdir` + `_drive_distributed`) wrapper.
|
||||||
|
- `scripts/gemm_sweep.py` → bench의 `run_sweep` 위 thin wrapper.
|
||||||
|
|
||||||
|
테스트가 bench 모듈을 import하는 것은 허용된다(테스트는 production 위에
|
||||||
|
위치, ADR-0007); 이는 전체 패키지 eager audit을 유발하며, 그것은 이미 매
|
||||||
|
`kernbench` 실행 시 동작한다. matplotlib는 renderer 내부에서 lazy import로
|
||||||
|
유지되어 audit의 startup 비용은 불변이다.
|
||||||
|
|
||||||
|
### D6. 평면 모듈 네이밍 (`benches/` 하위 폴더 없음)
|
||||||
|
|
||||||
|
`1H_milestone…`로 명명된 `benches/` 하위 패키지는 불가능하다 — Python
|
||||||
|
패키지 이름은 숫자로 시작할 수 없다. 따라서 bench는 평면 모듈
|
||||||
|
`milestone_1h_gemm.py` / `milestone_1h_ccl.py`이며 bench 이름은
|
||||||
|
`milestone-1h-gemm` / `milestone-1h-ccl`(kebab-case, ADR-0045 D1에 따라
|
||||||
|
글자로 시작)이다.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
|
||||||
|
- `kernbench run --bench milestone-1h-gemm`(또는 `…-ccl`)이 도메인의 모든
|
||||||
|
결과 + figure를 한 명령으로 재생성한다 — 마일스톤 요구사항.
|
||||||
|
- 평가 로직의 단일 소스(bench), shim을 통해 테스트와 스크립트가 재사용;
|
||||||
|
중복 없음.
|
||||||
|
- figure 테스트와 `scripts/gemm_sweep.py`는 변경 없이 계속 동작.
|
||||||
|
|
||||||
|
### Negative / limitations
|
||||||
|
|
||||||
|
- 두 bench 파일이 크다(CCL 쪽은 distributed 드라이버, sweep, matplotlib
|
||||||
|
드로잉을 섞는다). 대부분 평가 하니스인 "bench"는 이례적이며, 본 ADR이
|
||||||
|
이를 정당화한다.
|
||||||
|
- 생성 아티팩트가 명시적 요청에 의해 source tree(`src/kernbench/benches/`)
|
||||||
|
안에 살며 커밋된다(원격에서 figure를 볼 수 있도록); bench 재실행 시
|
||||||
|
재생성된다.
|
||||||
|
- `milestone-1h-ccl`(및 기본 `milestone-1h-gemm`)은 분 단위 소요 —
|
||||||
|
on-demand 마일스톤 아티팩트에는 수용 가능, 일상 실행에는 아님.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **ADR-0007**: 레이어 방향(테스트는 production을 import할 수 있으나 bench는
|
||||||
|
테스트를 import할 수 없는 이유).
|
||||||
|
- **ADR-0043 / ADR-0044**: 본 ADR이 bench로 이전하는 allreduce / GEMM 평가
|
||||||
|
하니스.
|
||||||
|
- **ADR-0045**: bench 모듈 계약; 여기 D2가 그 D5(single-device 규칙)를
|
||||||
|
평가-bench 패턴으로 확장하고, sentinel을 위해 D4(NO_REQUESTS)에 의존.
|
||||||
|
- **ADR-0024**: allreduce sweep이 구동하는 rank = SIP launcher.
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
- GEMM theoretical 모델 상수(ADR-0044 D5)를 복사 대신 ADR-0033/0014에서
|
||||||
|
소싱해야 하는가? 본 ADR로는 불변.
|
||||||
|
- `build_overview_slides.py`가 GEMM 막대를 네이티브로 그리는 대신 마일스톤
|
||||||
|
출력 PNG를 소비해야 하는가? 여전히 open(ADR-0044 D6 / Negative).
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
# ADR-0062: Lazy `tl.load` — 첫 사용 시점 auto-wait를 갖는 non-blocking HBM load
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
> **ADR-0060**(AHBM GQA Fused Attention)의 보조 ADR. Decode와 long-context
|
||||||
|
> 어텐션은 **KV-load-bound**이며, load/compute 오버랩이 지배적 레버다. 본
|
||||||
|
> ADR은 별도의 `load_async` op을 추가하는 대신 `tl.load` 자체를 **lazy**
|
||||||
|
> (non-blocking, wait를 첫 사용 시점에 자동 삽입)로 만든다. 오늘날 유일한
|
||||||
|
> async primitive는 IPCQ 통신용이지 HBM load용이 아니다.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
### 오버랩이 요구하는 것
|
||||||
|
|
||||||
|
FlashAttention은 operand를 스트리밍한다: GEMM/MATH 엔진이 현재 타일을 처리하는
|
||||||
|
동안 DMA 엔진은 이미 다음 operand를 당기고 있어야 한다. 그 오버랩이 있으면
|
||||||
|
대역폭-bound 커널은 타일당 `compute + dma` 대신 대략 `max(compute, dma)`로
|
||||||
|
돈다.
|
||||||
|
|
||||||
|
ADR-0060의 하이브리드 설계에서 두 GEMM은 `tl.composite(op="gemm")`로 발행되고,
|
||||||
|
그 K/V operand는 `tl.ref`(HBM 상주, PE_SCHEDULER가 타일당 스트리밍)이므로 **타일당
|
||||||
|
K/V prefetch는 composite scheduler가 처리**한다. lazy `tl.load`는 나머지 명시적
|
||||||
|
load(Q group, 그리고 비-composite 커널)를 담당하여 그것들도 greenlet을 멈추는
|
||||||
|
대신 뒤따르는 compute와 오버랩되게 한다.
|
||||||
|
|
||||||
|
### 현재 존재하는 것
|
||||||
|
|
||||||
|
- `tl.load(ptr, shape, dtype)`은 **blocking**이다: `DmaReadCmd`를 emit하고
|
||||||
|
greenlet 커널은 PE_DMA가 완료를 알릴 때까지 suspend된다
|
||||||
|
(`tl_context.py:177-203`; greenlet 구동 `kernel_runner.py:146-153`). 한
|
||||||
|
커널에서 두 `tl.load`가 in-flight일 수 없다.
|
||||||
|
- async 패턴이 **존재**하긴 하나 IPCQ 전용이다:
|
||||||
|
`tl.recv_async(dir, ...) -> RecvFuture` + 지연 wait
|
||||||
|
(`kernel_runner.py:248-285`). 이는 메커니즘을 입증한다 — future를 반환하고
|
||||||
|
나중에 wait 체크(`if not future.event.triggered: yield future.event`)로
|
||||||
|
resolve되는 non-blocking 커맨드가 greenlet 모델에서 동작한다.
|
||||||
|
- DMA 엔진은 read 채널을 write 채널과 분리된 SimPy resource(capacity 1,
|
||||||
|
`pe_dma.py:45`)로 모델한다. 따라서 in-flight read는 표현 가능하며, 단일 read
|
||||||
|
채널에서 직렬화되면서 PE_GEMM/PE_MATH의 compute와 오버랩된다. 오직
|
||||||
|
*커널-대면 API*만이 현재 load-vs-compute를 직렬화한다.
|
||||||
|
|
||||||
|
`tl.load`는 오늘날 뒤따르는 compute와 오버랩될 수 없다.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**`tl.load`를 lazy로 만든다: `DmaReadCmd`를 발행하고 핸들을 즉시 반환
|
||||||
|
(non-blocking); 런타임은 load된 데이터가 실제로 처음 소비되는 지점에 wait를
|
||||||
|
자동 삽입한다.** 커널-대면 API는 불변이다 — 작성자는 계속 `tl.load`를 쓴다 —
|
||||||
|
따라서 이는 새 op이 아니라 *시맨틱* 변경이다. 기존 `recv_async`/wait 기계장치를
|
||||||
|
(1) HBM-load 경로로, (2) 명시적 `tl.wait` 호출에서 암묵적·의존성 기반 first-use
|
||||||
|
wait로 일반화한다.
|
||||||
|
|
||||||
|
### D1. `tl` 표면 — 불변
|
||||||
|
|
||||||
|
`tl.load(ptr, shape, dtype)`은 시그니처와 `TensorHandle` 반환을 유지한다.
|
||||||
|
바뀌는 것은 *언제* blocking하느냐다: 발행 시점에는 결코 아니고, 결과가 소비 op에
|
||||||
|
의해 처음 읽힐 때만 암묵적으로.
|
||||||
|
|
||||||
|
### D2. 메커니즘 — non-blocking 발행 + 사용 시점 auto-wait
|
||||||
|
|
||||||
|
- `tl.load`는 완료 event를 yield하지 않고 `DmaReadCmd`를 PE_DMA에 post하며,
|
||||||
|
반환 핸들에 pending event를 기록한다(`recv_async` 패턴을 load에 적용).
|
||||||
|
- 소비 op(`tl.dot`, MATH op, `tl.store`, `tl.composite` operand, …)이
|
||||||
|
dispatch될 때, 런타임은 각 입력 핸들의 pending load event를 확인하고 아직
|
||||||
|
triggered가 아니면 먼저 yield한다 — 즉 wait가 가장 늦은 올바른 지점(첫 사용)에
|
||||||
|
자동 삽입된다.
|
||||||
|
- op_log 엔트리는 불변(`memory/dma_read`): asynchrony는 스케줄링 속성이지 새 op
|
||||||
|
종류가 아니므로 `dma_read_count` 및 기존 op_log 소비자가 계속 동작한다.
|
||||||
|
|
||||||
|
### D3. 범위 — 전역
|
||||||
|
|
||||||
|
`tl.load`는 opt-in 플래그 뒤가 아니라 **모든 곳에서** lazy다. 이것이 충실한
|
||||||
|
모델이다: 매 load마다 blocking하는 것은 *naive* 커널의 속성이고, 효율 커널(그리고
|
||||||
|
실제 컴파일러)은 load를 hoist하고 사용 시점에만 wait한다. 결과: `tl.load`와 첫
|
||||||
|
사용 사이에 독립 작업이 있는 기존 커널은 **더 낮은(빠른) latency**를 본다 —
|
||||||
|
동작의 회귀가 아니라 모델의 정확도 개선. 바뀌는 골든 latency는 **재생성**해야
|
||||||
|
하며; load 직후 곧바로 사용하는 커널은 변화가 없다(auto-wait이 즉시 발동하여
|
||||||
|
blocking과 동일).
|
||||||
|
|
||||||
|
### D4. Latency / 오버랩 시맨틱
|
||||||
|
|
||||||
|
- `tl.load`는 **발행**(descriptor push)만 청구하고 커널은 진행한다. (per-op
|
||||||
|
발행 비용은 `dispatch_cycles`, 현재 0; op 종류별 차등 발행 비용은 별도로
|
||||||
|
추적 — ADR-0060 §1, §9, ADR-0064.)
|
||||||
|
- DMA 전송은 read 채널(capacity 1)을 모델 지속시간만큼 점유하며 커널이 다음에
|
||||||
|
내는 compute와 병렬; 다중 in-flight load는 채널에서 직렬화되나 compute와
|
||||||
|
오버랩.
|
||||||
|
- 자동 삽입된 wait는 전송이 끝나지 않았을 때만 blocking.
|
||||||
|
- 결정성 보존: wait 지점은 프로그램 순서(첫 사용)로 고정되고, 완료는 modeled DMA
|
||||||
|
채널의 scheduled event다(SPEC §0.1, R8). latency 빼기 없음 — 오버랩은 실제
|
||||||
|
모델 동시성.
|
||||||
|
|
||||||
|
> **모델링 가정.** auto-wait-at-first-use는 *잘 스케줄된* 커널(컴파일러가
|
||||||
|
> wait를 가장 늦은 올바른 지점에 둠)을 모델한다. 실제 컴파일러는 이를 근사하며,
|
||||||
|
> 일부 load는 hoist 불가(register pressure, aliasing). 성능 시뮬레이터(SPEC
|
||||||
|
> §0)에서 잘 스케줄된 경우를 모델하는 것이 의도된 동작이다.
|
||||||
|
|
||||||
|
## Alternatives
|
||||||
|
|
||||||
|
### A1. 별도 `tl.load_async` op (이전 제안)
|
||||||
|
|
||||||
|
커널이 손으로 호출하는 명시적 `load_async`/`wait` 쌍 추가(더블버퍼 댄스). 기각:
|
||||||
|
커널-대면 API를 키우고 버퍼 수명 부기를 모든 커널 작성자에게 떠넘긴다. lazy
|
||||||
|
`tl.load`는 **API 표면 변경 없이** 동일 오버랩과 컴파일러식 auto-wait를 준다.
|
||||||
|
|
||||||
|
### A2. 커널별 opt-in lazy load
|
||||||
|
|
||||||
|
`tl.load`를 기본 blocking으로 두고 새 커널만 opt-in. 기각: `tl.load` 시맨틱을 두
|
||||||
|
변종으로 쪼개고 기존 bench로부터 모델 개선을 숨긴다; 전역 lazy가 더 깔끔하고
|
||||||
|
골든 재생성 비용은 1회성(D3).
|
||||||
|
|
||||||
|
### A3. composite 스트리밍에만 의존 (lazy load 없이)
|
||||||
|
|
||||||
|
ADR-0060의 composite는 `tl.ref` K/V operand를 스트리밍하므로 GEMM operand DMA는
|
||||||
|
이미 오버랩된다. 그러나 명시적 load(Q group, 비-composite 커널)는 lazy `tl.load`
|
||||||
|
없이 여전히 stall. 단독으로는 불충분.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
- 커널-대면 API 변경 **제로**로 load/compute 오버랩; 작성자는 계속 `tl.load`.
|
||||||
|
- `recv_async`/wait와 대칭 — 개념적 표면적 작음.
|
||||||
|
- 일반적: 어떤 대역폭-bound 커널도 자동 prefetch.
|
||||||
|
|
||||||
|
### Negative
|
||||||
|
- load와 사용 사이에 독립 작업이 있는 커널의 기존 골든 latency가 이동(빨라짐)
|
||||||
|
→ 1회성 재생성(D3).
|
||||||
|
- auto-wait는 런타임이 핸들별 pending event를 추적하고 소비-op dispatch에서
|
||||||
|
확인하도록 요구(데이터 의존성 추적).
|
||||||
|
- scratch 수명(ADR-0063)과 상호작용: in-flight load의 대상 버퍼는 auto-wait이
|
||||||
|
발동하기 전에 recycle되면 안 된다. recycling scope는 살아있는(미-wait된) load
|
||||||
|
버퍼를 제외해야 한다.
|
||||||
|
|
||||||
|
## Test Requirements
|
||||||
|
|
||||||
|
1. **오버랩이 실재**: `tl.load` 후 비슷한 지속시간의 독립 GEMM을 내는 커널이
|
||||||
|
`load+gemm`이 아니라 ≈`max(load, gemm)`으로 완료(end-to-end latency가 직렬
|
||||||
|
합보다 엄격히 작음).
|
||||||
|
2. **auto-wait 정확성**: load된 `TensorHandle`이 처음 소비될 때, 같은 주소에
|
||||||
|
대해 오늘날 blocking `tl.load`와 동일한 바이트를 보유(Phase 2).
|
||||||
|
3. **둘 in-flight**: 서로 다른 주소로의 두 `tl.load`가 나중에 소비되어 둘 다
|
||||||
|
올바른 독립 텐서로 resolve; 그 DMA는 read 채널에서 직렬화.
|
||||||
|
4. **op_log 호환성**: 각 `tl.load`은 여전히 정확히 하나의 `memory/dma_read`를
|
||||||
|
로그; `dma_read_count`는 blocking 버전 대비 불변.
|
||||||
|
5. **무-오버랩 무-변화**: load 직후 곧바로 사용하는 커널은 blocking 모델과 동일한
|
||||||
|
latency(auto-wait 즉시 발동).
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
# ADR-0063: `tl.scratch_scope` — long-context를 위한 tile 단위 scratch 재활용
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
> **ADR-0060**(AHBM GQA Fused Attention)의 보조 ADR. Long-context attention
|
||||||
|
> 은 수많은 K/V tile을 sweep하며, 매 tile의 중간 결과
|
||||||
|
> (`scores`, `P`, `exp`, partial `O`, …) 는 현재 kernel 호출 동안 회수되지
|
||||||
|
> 않는 fresh scratch를 새로 할당한다. PE당 1 MiB scratch 예산은 실제로
|
||||||
|
> 의미 있는 context length에 도달하기 훨씬 전에 소진된다.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
### Bump allocator
|
||||||
|
|
||||||
|
`TLContext` 는 모든 math/compute output handle을 PE-local scratch pool
|
||||||
|
에서 **linear bump cursor** 로 할당한다
|
||||||
|
(`src/kernbench/triton_emu/tl_context.py`; `_scratch_alloc`):
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _scratch_alloc(self, nbytes):
|
||||||
|
aligned = (nbytes + 15) & ~15
|
||||||
|
addr = self._scratch_base + self._scratch_cursor
|
||||||
|
self._scratch_cursor += aligned
|
||||||
|
if self._scratch_cursor > self._scratch_size: # default 1 << 20 = 1 MiB
|
||||||
|
raise RuntimeError("TLContext scratch overflow: ...")
|
||||||
|
return addr
|
||||||
|
```
|
||||||
|
|
||||||
|
Docstring은 cursor가 **"매 kernel 호출마다 reset된다"** 고 명시한다 —
|
||||||
|
즉 kernel entry에서만 reset, kernel body 내에서는 절대 reset되지 않는다.
|
||||||
|
모든 `tl.dot`, `tl.softmax`, `tl.exp`, `tl.sum`, `a - b`, `a * b`, … 는
|
||||||
|
fresh slice를 가져가고 kernel이 return될 때까지 아무것도 회수되지 않는다.
|
||||||
|
|
||||||
|
### 왜 GQA kernel에서 문제가 되는가
|
||||||
|
|
||||||
|
현재 milestone bench는 이 한계 **때문에** `S_q = S_kv_per_rank = 16` 으로
|
||||||
|
명시적으로 고정해 두었다
|
||||||
|
(`tests/attention/test_milestone_gqa_llama70b.py:123-148`):
|
||||||
|
|
||||||
|
> "S_q_prefill and S_kv_per_rank are deliberately small (16 each) so the
|
||||||
|
> simulator's 1 MB per-PE TCM kernel scratch is not exhausted by the
|
||||||
|
> bump-allocated handle outputs of softmax/exp/dot/sum chains over
|
||||||
|
> n_ranks ring steps."
|
||||||
|
|
||||||
|
`n_tiles`개의 tile sweep에 대해 FlashAttention은 O(`n_tiles` ×
|
||||||
|
tile당 중간값) 만큼 할당한다. 의미 있는 context에서는 overflow된다.
|
||||||
|
Flash attention의 *수학* 자체는 **O(1)** 의 live scratch만 필요로 한다 —
|
||||||
|
running `(m, l, O)` + 현재 tile의 working set — 매 tile의 임시값은
|
||||||
|
running state에 fold된 직후 dead가 되기 때문이다. 다만 allocator가 그
|
||||||
|
사실을 모를 뿐이다.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Kernel이 할당 영역을 **재활용 가능(reclaimable)** 으로 표시할 수 있는
|
||||||
|
**scratch scope** 을 추가한다. 이를 통해 tile당 임시값은 재활용되고,
|
||||||
|
live running state(및 in-flight prefetch buffer)는 보존된다.
|
||||||
|
|
||||||
|
### D1. `tl` 표면 — context manager
|
||||||
|
|
||||||
|
```python
|
||||||
|
with tl.scratch_scope():
|
||||||
|
s = tl.dot(q, k_t) # `with` 내부의 모든 handle은
|
||||||
|
p = tl.softmax(s) # 종료 시 rewind되는 영역을 공유
|
||||||
|
o_j = tl.dot(p, v)
|
||||||
|
# ... o_j를 외부 영역에 사는 running (m,l,O) 에 fold ...
|
||||||
|
# __exit__: cursor가 __enter__ 시점 값으로 rewind
|
||||||
|
```
|
||||||
|
|
||||||
|
Semantics:
|
||||||
|
- `__enter__` 는 현재 `_scratch_cursor` 를 save-point로 기록.
|
||||||
|
- `__exit__` 는 cursor를 save-point로 복원, 내부에서 할당된 모든 것을
|
||||||
|
free.
|
||||||
|
- Scope **외부**에서 할당된 handle (running `m,l,O`, ADR-0062의
|
||||||
|
`LoadFuture` 가 보유한 prefetch buffer) 은 주소를 유지 — save-point
|
||||||
|
이전에 또는 enclosing scope에서 할당되었기 때문.
|
||||||
|
|
||||||
|
### D2. 안전 계약
|
||||||
|
|
||||||
|
Scope 내부에서 할당된 handle은 scope exit 이후 **읽으면 안 된다** —
|
||||||
|
그 바이트는 다음 scope의 할당으로 덮어쓰여질 수 있다. Flash loop은
|
||||||
|
이를 자연스럽게 지킨다: tile iteration 사이에 살아남는 값은 running
|
||||||
|
accumulator 뿐이고, 이들은 per-tile scope **외부**에서 할당되며
|
||||||
|
*내부*의 op들이 외부 주소로 쓰면서 갱신된다(merge가 새 running state
|
||||||
|
를 쓰는 방식 — D3 참조).
|
||||||
|
|
||||||
|
### D3. Running accumulator와의 상호작용
|
||||||
|
|
||||||
|
Online-softmax merge는 이전 running `(m, l, O)` 와 현재 tile의
|
||||||
|
`(m_j, l_j, O_j)` 를 읽어 새 running 값을 만든다. 새 running 값을
|
||||||
|
재활용 영역 바깥에 두기 위해, merge는 결과를 loop 시작 전 1회 할당된
|
||||||
|
**stable scratch** (per-tile scope과는 별개의 작은 고정 "running-state"
|
||||||
|
arena) 로 쓴다. 구체적으로 kernel은 두 개의 arena를 둔다:
|
||||||
|
|
||||||
|
- **persistent arena** (1회 할당): `m, l, O` (merge용 double-buffer가
|
||||||
|
필요한 경우 그것까지).
|
||||||
|
- **scoped arena** (매 tile rewind): `scores, P, exp, O_j, scale_*`.
|
||||||
|
|
||||||
|
이는 실제 flash-attention SRAM budgeting의 모습과 같다: 작은 persistent
|
||||||
|
accumulator 영역 + 재활용되는 tile working set.
|
||||||
|
|
||||||
|
#### D3.1 Persistent-arena 쓰기 mechanism: `tl.copy_to(dst, src)`
|
||||||
|
|
||||||
|
Merge op (`tl.maximum`, `tl.exp`, binary `*` / `+`) 모두 D1의 bump
|
||||||
|
cursor로부터 할당하는 `_make_compute_out(...)` 을 호출한다. 따라서
|
||||||
|
`scratch_scope` 내부에서 그 결과 handle들은 scope **내부**에 살고
|
||||||
|
`__exit__` 시점에 사라진다. D3의 two-arena 분리를 실현하려면 kernel은
|
||||||
|
**scoped 결과의 바이트를 persistent 주소로 쓰는** 방법이 필요하다.
|
||||||
|
|
||||||
|
이 gap을 메우는 primitive:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def copy_to(self, dst: TensorHandle, src: TensorHandle) -> None:
|
||||||
|
"""``src`` 의 바이트를 ``dst`` 의 주소로 복사한다 (양쪽 모두 TCM).
|
||||||
|
|
||||||
|
Shape, dtype 일치 필요. ``dst`` 는 통상 어떤 활성 ``scratch_scope`` 도
|
||||||
|
바깥에서 할당된 handle (persistent arena); ``src`` 는 scope ``__exit__``
|
||||||
|
이후에도 바이트가 살아 있어야 하는 scoped handle.
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
`tl.store` (HBM 측 바이트 복사) 와 대칭이지만, running-state writeback이
|
||||||
|
op_log를 불필요한 DMA로 오염시키지 않도록 TCM 전용으로 유지한다.
|
||||||
|
|
||||||
|
**Mechanics:**
|
||||||
|
- 신규 `CopyCmd(src, dst, nbytes, data_op=True)` command.
|
||||||
|
- op_log: `op_kind="math"`, `op_name="copy"` — vector engine에서 실행.
|
||||||
|
- Latency: `pe_math._compute_ns(prod(shape))` — HBM 전송이 아니라
|
||||||
|
on-chip register writeback을 모델링.
|
||||||
|
- Emit-time 검증: `dst.shape == src.shape`, `dst.dtype == src.dtype`,
|
||||||
|
`dst.space == "tcm"`, `src.space == "tcm"`. 작성자 오류는 Phase 2
|
||||||
|
data execution 깊숙한 곳이 아니라 Phase 1에서 노출.
|
||||||
|
|
||||||
|
**호출 패턴:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
m, l, O = init_running(...) # persistent (scope 외부)
|
||||||
|
for j in range(n_tiles):
|
||||||
|
with tl.scratch_scope():
|
||||||
|
... # tile 작업 (재활용)
|
||||||
|
m_new = tl.maximum(m, mj) # scoped scratch
|
||||||
|
l_new = l * scale_old + l_step * scale_step
|
||||||
|
O_new = O * scale_old + O_step * scale_step
|
||||||
|
|
||||||
|
tl.copy_to(m, m_new) # ← 새 running state를 persist
|
||||||
|
tl.copy_to(l, l_new)
|
||||||
|
tl.copy_to(O, O_new)
|
||||||
|
# exit: scoped m_new/l_new/O_new 는 사라지고, 그 바이트는
|
||||||
|
# persistent m/l/O 에 살아있다
|
||||||
|
```
|
||||||
|
|
||||||
|
`copy_to` 는 `__exit__` **이전** 에 실행되므로 `src` (scoped) 의 read는
|
||||||
|
유효하며, exit 이후에는 `dst` (persistent) 만 read되어 D2의
|
||||||
|
"exit 이후에는 읽지 말 것" 안전 계약을 만족한다.
|
||||||
|
|
||||||
|
**왜 모든 math op에 `dst=` kwarg를 추가하지 않고 전용 primitive 인가**
|
||||||
|
(검토 후 기각): `tl.maximum`, `tl.exp`, `_binary_math`, `_unary_math`,
|
||||||
|
`_reduction` 에 `dst=` 를 추가하는 것은 5개 op-family 에 걸쳐 ~25 LOC
|
||||||
|
이며, "호출은 fresh handle을 return한다" 라는 일관된 패턴을 깬다.
|
||||||
|
`tl.copy_to` 는 단일 primitive, 단일 command, 단일 executor handler —
|
||||||
|
같은 효과에 비해 최소한의 surface area.
|
||||||
|
|
||||||
|
### D4. Nesting
|
||||||
|
|
||||||
|
Scope는 nest 가능 (save-point의 stack). Inner scope exit은 inner
|
||||||
|
save-point로 rewind, outer exit은 더 멀리 rewind한다. Prefill의
|
||||||
|
"per-query-block" 외부 scope을 둘러싼 "per-KV-tile" 내부 scope을
|
||||||
|
지원한다.
|
||||||
|
|
||||||
|
## Alternatives
|
||||||
|
|
||||||
|
### A1. Temporary를 HBM에 round-trip
|
||||||
|
|
||||||
|
중간값을 HBM에 store하고 reload하여 TCM scratch를 "free" 한다. 기각:
|
||||||
|
TCM-resident streaming kernel을 HBM-bandwidth-bound로 만들어 목적에
|
||||||
|
정반대이며, op_log를 불필요한 DMA로 오염시킨다.
|
||||||
|
|
||||||
|
### A2. Tiled `tl.composite` (scheduler-관리 scratch)
|
||||||
|
|
||||||
|
`tl.composite` 는 PE_SCHEDULER 내부에서 tile당 scratch를 자동 재활용
|
||||||
|
한다. ADR-0062 A1과 마찬가지로 매력적이지만 flash-capable composite
|
||||||
|
kind (두 GEMM + carry되는 `(m,l,O)`) 가 필요하므로 훨씬 큰 변경에
|
||||||
|
종속된다. `scratch_scope` 는 작고 일반적인 primitive로 greenlet kernel
|
||||||
|
에 동일한 memory 거동을 제공한다. 두 방식은 공존 가능.
|
||||||
|
|
||||||
|
### A3. Scratch 예산 증가
|
||||||
|
|
||||||
|
`pe_tcm.kernel_scratch_mb` 를 증가. 해결책으로는 기각: O(`n_tiles`)
|
||||||
|
scratch leak이 여전히 존재하면서 context-length ceiling만 선형적으로
|
||||||
|
밀어내며, hardware 실제 모습 (실 TCM은 작고, flash attention의 핵심은
|
||||||
|
O(1) working set) 을 잘못 표현한다. 거친 조절 knob으로만 유용, 재활용의
|
||||||
|
대체재는 아니다.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
- 인위적인 `S = 16` validation-scale ceiling 제거; 시간/데이터 mode
|
||||||
|
양쪽에서 의미 있는 context length 가능.
|
||||||
|
- Flash attention의 O(1) working-set 특성을 충실히 모델링.
|
||||||
|
- 작고 일반적인 primitive (모든 tiled kernel이 이점).
|
||||||
|
|
||||||
|
### Negative
|
||||||
|
- Use-after-scope 버그는 stale 바이트를 조용히 읽는다. D2 계약, attention
|
||||||
|
helper 안에서 scope discipline 공유, (선택적으로) rewound 영역을
|
||||||
|
poison시키는 debug build로 완화.
|
||||||
|
- ADR-0062와 조정 필요: prefetch buffer는 tile iteration 사이에 살아
|
||||||
|
있어야 하므로 scoped arena가 아니라 persistent arena에 속한다.
|
||||||
|
|
||||||
|
## Test Requirements
|
||||||
|
|
||||||
|
1. **Recycling**: `tl.scratch_scope()` 내부의 `N` tile loop에서 peak
|
||||||
|
`_scratch_cursor` 가 `N`과 무관하게 한 tile의 footprint로 bound됨
|
||||||
|
(오늘날은 선형 증가 후 overflow).
|
||||||
|
2. **Correctness**: scope이 있는 flash sweep이 scope이 없는 동일 sweep
|
||||||
|
과 (재활용 없이도 수렴하는 작은 `N` 에서) 동일한 `O` 를 산출
|
||||||
|
(Phase 2).
|
||||||
|
3. **Long context**: scope 없이 1 MiB를 초과하는 `N` 의 sweep이 완료
|
||||||
|
(오늘날 `S=16` cap이 회피하는 그 실패).
|
||||||
|
4. **Persistent-vs-scoped isolation**: scope 외부에서 할당된 running
|
||||||
|
`(m,l,O)` 가 `__exit__` 이후에도 올바른 값을 유지.
|
||||||
|
5. **Nesting**: 중첩된 scope이 올바른 save-point로 rewind.
|
||||||
@@ -0,0 +1,434 @@
|
|||||||
|
# ADR-0064: 구조적 CPU dispatch cost 모델 (`logical_bytes` + FIXED + R)
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted (Revision 2)
|
||||||
|
|
||||||
|
> **ADR-0060** (AHBM GQA Fused Attention) 와 **ADR-0065** (flat-ops
|
||||||
|
> composite + 첫 stateful recipe) 의 보조 ADR. 그 hybrid 의 핵심 — "GEMM 은
|
||||||
|
> `tl.composite` 로, softmax merge 는 커널에서" — 은 **PE_SCHEDULER 로
|
||||||
|
> tiling 을 offload 해서 CPU 가 굵은 descriptor 만 issue 하고 앞서가며
|
||||||
|
> 엔진을 포화시킴** 으로 이깁니다. 그 win 이 현재 시뮬레이터에선 보이지
|
||||||
|
> 않습니다 (per-op CPU issue cost = 0).
|
||||||
|
>
|
||||||
|
> Revision 2 는 원안의 **op-type calibration 표** 를 **logical_bytes 기반
|
||||||
|
> 구조 공식** 으로 대체합니다 — op-type 별 calibration 불필요, 새 op_kind
|
||||||
|
> 도 자동 적용.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
### 현재 코드
|
||||||
|
|
||||||
|
- 모든 `tl.*` op 가 cmd emit 전에 `_emit_dispatch_overhead()` 호출
|
||||||
|
(`tl_context.py:196-212`) → `dispatch_cycles > 0` 일 때만
|
||||||
|
`PeCpuOverheadCmd(cycles=dispatch_cycles)` 발행. knob 은 op kind 무관
|
||||||
|
**uniform** 이며 두 실행 경로 모두 **0** 하드코딩
|
||||||
|
(`pe_cpu.py:101` greenlet, `:195` replay).
|
||||||
|
- ⇒ 명령 issue (descriptor 구성 + scheduler 큐 push) 가 PE_CPU 에서
|
||||||
|
**0 ns** 비용.
|
||||||
|
- `PeCpuOverheadCmd` 는 PE_CPU 에서 `yield env.timeout(cmd.cycles)` 로
|
||||||
|
소비 (`kernel_runner.py:131-132`).
|
||||||
|
|
||||||
|
### uniform-zero 가 hybrid 에 부적합한 이유
|
||||||
|
|
||||||
|
ADR-0060 §1 의 핵심: composite 1 개가 `N_tiles` 분량의 GEMM tiling 을
|
||||||
|
offload → CPU 는 `O(1)` 개의 굵은 cmd 만 issue, primitive 경로는
|
||||||
|
`O(N_tiles × ops/tile)`. issue cost = 0 이면 모델은:
|
||||||
|
- primitive 경로의 *CPU 가 push 를 못 따라가 엔진 idle* 을 못 보임
|
||||||
|
- composite 의 *구성 비용 ≫ primitive 1 개이지만 ≪ 대체된 primitive 들의 합* 을 못 보임
|
||||||
|
|
||||||
|
### Revision 1 의 op-type calibration 이 과한 이유
|
||||||
|
|
||||||
|
원안은 `cost_table[kind]` (kind = `composite`, `load`, `dot`, `math`, …)
|
||||||
|
형태였습니다. 비용:
|
||||||
|
- kind 마다 값 필요 (calibration ≥ |kinds|)
|
||||||
|
- 새 kind 추가 시마다 entry 추가
|
||||||
|
- 그런데 잡으려던 *ratio* — "composite ≫ primitive, ≪ 대체된 primitive 들의 합" —
|
||||||
|
는 *op kind* 가 아니라 *cmd 가 들고 있는 필드 수* 의 함수.
|
||||||
|
|
||||||
|
cmd 의 *byte 발자국* 이 자연 proxy: N OpSpec composite 는 1 OpSpec primitive 의 ~N 배 bytes.
|
||||||
|
*고정* 부분 (큐 tail 업데이트, completion 등록, MMIO-급 latency) 은 cmd 마다.
|
||||||
|
둘 합치면: `FIXED + bytes × R`.
|
||||||
|
|
||||||
|
### 이 모델이 실제로 노출하는 것
|
||||||
|
|
||||||
|
**1차** 신호는 per-cmd FIXED 비용에 의한 **command-count 감소**.
|
||||||
|
**byte** 항은 *비정상적으로 큰* composite 가 공짜로 보이지 않도록 막는
|
||||||
|
2차 보정. 현실적 on-die 큐 대역폭 (16 B/cycle, D3) 에서 FIXED 가
|
||||||
|
decode opt2 의 opt3 (≈10 cmds/tile) vs opt2 (≈2 cmds/tile) 의 dispatch
|
||||||
|
cost 차이 중 ≥85% 차지.
|
||||||
|
|
||||||
|
이 framing 이 모델의 한계도 정의: 단일 composite 가 무한히 커지면 byte
|
||||||
|
항만으로는 HW 가 받지 못할 정도의 fused command 를 모델이 보상하는 것을
|
||||||
|
막지 못함 — 그래서 **D7** 의 descriptor 크기 cap 이 필요.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### D1. 구조적 dispatch cost 공식
|
||||||
|
|
||||||
|
PE_SCHEDULER 로 가는 모든 PE command 가 PE_CPU 에서 dispatch cycles 소모:
|
||||||
|
|
||||||
|
```
|
||||||
|
dispatch_cycles(cmd) = FIXED_PER_CMD + cmd.logical_bytes × R
|
||||||
|
```
|
||||||
|
|
||||||
|
- `FIXED_PER_CMD` (cycles/cmd): 큐 tail 업데이트, MMIO-급 RTT,
|
||||||
|
completion-event 등록 — cmd 크기와 무관 고정 비용.
|
||||||
|
- `R` (cycles/byte): scheduler 큐에 cmd 를 직렬화하는 큐-write 대역폭.
|
||||||
|
- `cmd.logical_bytes` (int): cmd 의 *HW-논리* byte 수 — D2 룰로 계산,
|
||||||
|
Python `sys.getsizeof` 가 아님.
|
||||||
|
|
||||||
|
PE_CPU 는 기존 hook 그대로 `PeCpuOverheadCmd(cycles=dispatch_cycles(cmd))`
|
||||||
|
를 dispatch 전에 발행 — 사이클 값만 변경.
|
||||||
|
|
||||||
|
### D2. `logical_bytes` 룰
|
||||||
|
|
||||||
|
각 PE command dataclass 가 `logical_bytes: int` (property) 노출. 룰
|
||||||
|
(HW 친화적, Python overhead 무시):
|
||||||
|
|
||||||
|
| 필드 종류 | bytes |
|
||||||
|
|---|---|
|
||||||
|
| cmd framing (cmd 종류 discriminator + completion id 참조) | 4 |
|
||||||
|
| Opcode (op kind enum) | 1 |
|
||||||
|
| Enum (scope 등) | 1 |
|
||||||
|
| `TensorHandle` 참조 (address only — shape/dtype 은 descriptor table 가정) | 8 |
|
||||||
|
| Scalar (int/float) | 4 |
|
||||||
|
| Tuple length marker | 1 |
|
||||||
|
|
||||||
|
`CompositeCmd` 는 `ops` 와 `rw_handles` 재귀 합산:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def logical_bytes(self) -> int:
|
||||||
|
return (
|
||||||
|
4 # framing
|
||||||
|
+ 1 + sum(op.logical_bytes for op in self.ops)
|
||||||
|
+ 1 + 8 * len(self.rw_handles)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
`OpSpec`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def logical_bytes(self) -> int:
|
||||||
|
return (
|
||||||
|
1 + 1 # opcode + scope
|
||||||
|
+ 1 + 8 * len(self.operands) # named operand handles
|
||||||
|
+ (8 if self.out is not None else 0) # out handle
|
||||||
|
+ 1 + sum(_extra_bytes(v) for v in self.extra.values())
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _extra_bytes(v) -> int:
|
||||||
|
"""OpSpec.extra 값의 타입-aware byte 카운트."""
|
||||||
|
if isinstance(v, bool): return 1
|
||||||
|
if isinstance(v, (int, float)): return 4
|
||||||
|
if isinstance(v, (tuple, list)): return 1 + 4 * len(v) # shape, axes, …
|
||||||
|
if isinstance(v, str): return 1 # opcode-like tag
|
||||||
|
return 4 # default scalar
|
||||||
|
|
||||||
|
|
||||||
|
# extra 의 예시 타입:
|
||||||
|
# m, k, n int → 4 each
|
||||||
|
# reduce_axis int → 4
|
||||||
|
# shape=(64, 64) tuple → 1 + 8 = 9
|
||||||
|
# factor=1.0 float → 4
|
||||||
|
```
|
||||||
|
|
||||||
|
(`DmaReadCmd`, `MathCmd` 등도 같은 룰 — dataclass 당 property 1 개,
|
||||||
|
약 3 줄.)
|
||||||
|
|
||||||
|
**카운팅 룰 — per-op 합산, dedup 없음.** `OpSpec` 안의 각 `TensorHandle`
|
||||||
|
참조는 독립적으로 카운트 — 같은 핸들이 여러 OpSpec 에 등장하거나
|
||||||
|
`rw_handles` 에도 있어도 마찬가지. `rw_handles` block 은 cross-composite
|
||||||
|
해저드 추적 (ADR-0065 D6.3) 의 메타데이터로 `ops` 의 operand 참조와
|
||||||
|
**별도** 카운트. 중복 제거 없음. HW 와 일치: descriptor 가 각 operand
|
||||||
|
slot 을 독립 address field 로 인코딩하고 dispatcher 가 `rw_handles` 를
|
||||||
|
별도 metadata block 으로 추적. 예: `OpSpec(kind="mul_bcast", operands=
|
||||||
|
{"src_a": O, "src_b": corr}, out=O)` 는 핸들 `O` 를 *두 번* 카운트
|
||||||
|
(`src_a` 한 번, `out` 한 번); 만약 `O` 가 둘러싼 CompositeCmd 의
|
||||||
|
`rw_handles` 에도 있으면 세 번째 카운트.
|
||||||
|
|
||||||
|
### D3. Default — 일반 composite ≈ 43 ns 기준
|
||||||
|
|
||||||
|
Anchor: **DMA-staged GEMM 경로용 단일-OpSpec composite** —
|
||||||
|
`OpSpec(kind="gemm", ...)` 1 개; DMA stage 는 PE_SCHEDULER 가 operand
|
||||||
|
`space` 에서 자동 삽입 (ADR-0065 D4), **`logical_bytes` 에 등장하지 않음**
|
||||||
|
(커널이 별도 cmd 로 emit 안 함). 분해:
|
||||||
|
|
||||||
|
```
|
||||||
|
framing 4
|
||||||
|
ops tuple length 1
|
||||||
|
GEMM OpSpec 40 (opcode 1 + scope 1 + 1 + 2 handles 16
|
||||||
|
+ out 8 + 1 + extra m/k/n 12)
|
||||||
|
rw_handles tuple length 1
|
||||||
|
rw_handles content 8 (출력 RW 핸들 1 개)
|
||||||
|
─────────────────────────────
|
||||||
|
total ~54 bytes
|
||||||
|
```
|
||||||
|
|
||||||
|
Dispatch 목표 ≈43 ns. on-die producer→consumer 큐 16 bytes/cycle
|
||||||
|
(일반 on-die descriptor queue 폭) 가정.
|
||||||
|
|
||||||
|
```
|
||||||
|
FIXED_PER_CMD = 40 cycles
|
||||||
|
R = 0.0625 cycles/byte (= 16 bytes/cycle)
|
||||||
|
```
|
||||||
|
|
||||||
|
검증: `40 + 54 × 0.0625 = 43.375 cycles ≈ 43 ns` ✓
|
||||||
|
|
||||||
|
Cycle→ns 변환은 PE 노드의 기존 `clock_freq_ghz` attr (PE_MATH `_compute_ns`
|
||||||
|
가 쓰는 같은 것) 사용. cost-model knob 은 **cycle-domain 만** — clock
|
||||||
|
설정 중복 안 함.
|
||||||
|
|
||||||
|
### D4. topology config override
|
||||||
|
|
||||||
|
default 는 `pe_cpu.py` 에 내장. topology yaml 의 PE 노드 attrs 아래
|
||||||
|
`pe_cost_model:` 절로 override (cycle-domain knob 만; clock 은 PE 의
|
||||||
|
기존 `clock_freq_ghz` 사용):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
pe:
|
||||||
|
attrs:
|
||||||
|
clock_freq_ghz: 1.0 # 기존, cycle→ns 변환용
|
||||||
|
pe_cost_model:
|
||||||
|
fixed_per_cmd_cycles: 40
|
||||||
|
byte_cycles_recip: 0.0625 # = 16 bytes/cycle
|
||||||
|
max_composite_logical_bytes: 1024 # D7 — descriptor 크기 cap
|
||||||
|
```
|
||||||
|
|
||||||
|
누락된 키는 default. dispatch 공식은 PE_CPU init 시
|
||||||
|
`node.attrs["pe_cost_model"]` 에서 읽음.
|
||||||
|
|
||||||
|
### D5. Scope — 무엇이 cost 를 내고 무엇이 안 내나
|
||||||
|
|
||||||
|
| 경로 | dispatch cost? |
|
||||||
|
|---|---|
|
||||||
|
| PE_CPU → PE_SCHEDULER 의 모든 `PeCommand` | **예** |
|
||||||
|
| `PeCpuOverheadCmd` 자체 (이미 cycles 명시) | **아니오** (공식 우회) |
|
||||||
|
| PE_SCHEDULER 가 자동 생성한 Stage (DMA_READ/WRITE/FETCH/STORE) | **아니오** (PE_SCHEDULER 내부) |
|
||||||
|
| 엔진 compute latency (PE_DMA `drain_ns`, GEMM/MATH `_compute_ns`) | **변화 없음** — 엔진에 그대로 (SPEC §0.1) |
|
||||||
|
|
||||||
|
"latency 는 모델링된 컴포넌트에서" invariant 유지 — dispatch cost 는
|
||||||
|
*추가* CPU-side 시간, 엔진 시간에 fold 안 함.
|
||||||
|
|
||||||
|
### D6. 설정 가능 값; goldens 재생성
|
||||||
|
|
||||||
|
issue cost 를 0 → non-zero 로 바꾸면 **모든** bench 의 latency 변화.
|
||||||
|
이 ADR land 시 골든 latency **한 번** 재생성 — ADR-0062 D3 lazy-load 와
|
||||||
|
같은 패턴. 재생성 후 동일 calibration 이 ADR-0065 opt2 측정에 적용.
|
||||||
|
|
||||||
|
### D7. Composite 크기 cap (하드 제한 — validation error)
|
||||||
|
|
||||||
|
각 `CompositeCmd` 의 `logical_bytes` 가 **`max_composite_logical_bytes`**
|
||||||
|
(default **1024 bytes**, D4 로 override) 로 제한. `logical_bytes` 가 cap
|
||||||
|
을 초과하는 composite 는 host-side TLContext 가 emit 시점에 `ValueError`
|
||||||
|
로 **거부** — **자동 segmentation 없음**. 커널 작성자가 각 `CompositeCmd`
|
||||||
|
가 descriptor capacity 에 맞도록 recipe 를 재구성(예: 여러 개의 더 작은
|
||||||
|
composite 로 명시적으로 분할)해야 함.
|
||||||
|
|
||||||
|
**cap 이 필요한 이유.** 실제 하드웨어는 descriptor queue entry 크기,
|
||||||
|
scheduler parser buffer, command SRAM, firmware 입력의 하드 제한 보유.
|
||||||
|
cap 없으면 `FIXED + bytes × R` 모델이 하드웨어가 받지 못할 정도로 큰
|
||||||
|
fused composite 를 보상 (예: primitive 100 개를 composite 1 개로 fuse,
|
||||||
|
FIXED 1 회만 지불).
|
||||||
|
|
||||||
|
**1024 bytes 라는 *특정* 숫자의 근거.** 이는 측정된 HW 숫자가 아니라
|
||||||
|
**safe engineering limit** — kernbench 코드베이스의 현재 모든 알려진
|
||||||
|
composite (가장 큰 것이 decode opt2 의 `#2` ~322 bytes) 보다 훨씬 위이면서
|
||||||
|
미래 recipe 도 지켜야 할 *유한* descriptor capacity 를 의도적으로 표현.
|
||||||
|
값은 topology 별 override (D4); 실제 HW reference 등장 시 재캘리브레이션.
|
||||||
|
이 default 의 역할은 cap 을 *원칙으로 존재시키는 것*, 특정 HW 에 맞추는
|
||||||
|
것이 아님.
|
||||||
|
|
||||||
|
**자동 segmentation 대신 하드 에러를 택한 근거.** 초과 composite 를 자동
|
||||||
|
분할(원래 Revision 2 제안)하는 것은 inter-segment 순서, `rw_handles`
|
||||||
|
공유, completion 체이닝 등 emitter 복잡도를 추가하면서, 실질적으로는
|
||||||
|
하드웨어가 허용하는 것보다 큰 descriptor 를 요청한 커널을 덮어주는
|
||||||
|
것일 뿐. 명시적 에러로 표면화하면 emitter 가 단순해지고, HW 제약이
|
||||||
|
작업을 어떻게 분할할지 가장 잘 아는 커널 작성자에게 보임.
|
||||||
|
|
||||||
|
Decode opt2 의 `#2` composite (10 ops, ~322 bytes) 는 1024 cap 안에
|
||||||
|
편안히 — GQA workload 에 에러 없음.
|
||||||
|
|
||||||
|
### D8. Single-op-cmd fast-path (cmd 타입별 FIXED)
|
||||||
|
|
||||||
|
모든 **single-op** cmd — DMA descriptor (`DmaReadCmd`, `DmaWriteCmd`)
|
||||||
|
*그리고* single-op compute dispatch (`GemmCmd`, `MathCmd`, `CopyCmd`)
|
||||||
|
— 는 일반 control-path 비용과 분리된, 가벼운 FIXED 를 지불한다:
|
||||||
|
**`fixed_per_single_op_cmd_cycles`** (기본 **8 cycles**) 가 이들 전부에서
|
||||||
|
`fixed_per_cmd_cycles` 를 대체한다. **`CompositeCmd` 만이 일반
|
||||||
|
control-path 비용** `fixed_per_cmd_cycles` (= 40 cycles) 를 유지하는
|
||||||
|
유일한 cmd 다.
|
||||||
|
|
||||||
|
구체적으로 D1 의 공식이 다음과 같이 확장된다:
|
||||||
|
```
|
||||||
|
dispatch_cycles(cmd) = FIXED(type(cmd)) + cmd.logical_bytes × R
|
||||||
|
FIXED(CompositeCmd) = fixed_per_cmd_cycles (= 40)
|
||||||
|
FIXED(나머지 전부, 즉 모든 single-op cmd:
|
||||||
|
DmaReadCmd / DmaWriteCmd / GemmCmd / MathCmd / CopyCmd)
|
||||||
|
= fixed_per_single_op_cmd_cycles (= 8)
|
||||||
|
```
|
||||||
|
|
||||||
|
**왜 single-op cmd 에 가벼운 FIXED 를 따로 두나.** D1 은 모든 발행 cmd 에
|
||||||
|
동일한 FIXED 를 매겼다 — 가장 단순한 모델이라서. 그러나 실제로 single-op
|
||||||
|
cmd — DMA descriptor 든, 엔진에 단일 GEMM / elementwise / copy 를
|
||||||
|
dispatch 하는 것이든 — 는 40-cycle FIXED 가 모델링하던 일반 control-path
|
||||||
|
비용보다 훨씬 가볍다. scheduler 측 plan 을 만들 필요 없이, descriptor 나
|
||||||
|
instruction 하나를 엔진 queue 에 push 하는 것뿐이다:
|
||||||
|
|
||||||
|
- NVIDIA Hopper TMA: 단일 PTX 명령 (`cp.async.bulk`) 으로 bulk async DMA
|
||||||
|
발사 — SM 측에선 ~1 ISA cycle, TMA 엔진이 fan-out / chunking 자체 처리.
|
||||||
|
- NVIDIA Ampere `cp.async`: warp-level async copy ~1–2 cycles.
|
||||||
|
- 단일 MMA / tensor-core 발행 (`wgmma`, `mma.sync`): math pipe 에 명령
|
||||||
|
하나, control-path round trip 아님.
|
||||||
|
- AMD AQL packet 을 HSA queue 에 write: ~5–15 cycles.
|
||||||
|
- 일반 descriptor-ring-push 디자인 (Synopsys/Xilinx-style DMA IP): MMIO
|
||||||
|
write 들 합쳐 ~5–20 cycles.
|
||||||
|
- Tenstorrent tile descriptor 발행, Habana Gaudi TPC descriptor RAM
|
||||||
|
+ start register: descriptor 당 한 자릿수 cycles.
|
||||||
|
|
||||||
|
40-cycle FIXED 는 `CompositeCmd` 에 한해 정당하다. composite 는 단일
|
||||||
|
dispatch 가 아니기 때문이다: scheduler 가 tile-feeder plan 을 만들고,
|
||||||
|
내부 `DMA_READ → FETCH → GEMM → STORE → DMA_WRITE` stage 들에 걸쳐
|
||||||
|
per-tile read/write hazard 를 추적하고, completion handle 을 배선해야
|
||||||
|
한다. single-op `GemmCmd` 는 그런 기구 없이 엔진 발행 하나일 뿐이다 —
|
||||||
|
이를 composite 와 같은 40 cycles 로 매기면 단일 명령을 스케줄된 plan
|
||||||
|
전체와 혼동하는 것이다.
|
||||||
|
|
||||||
|
**왜 이게 커널 평가에 중요한가.** 일정한 40-cycle FIXED 는 user 가 직접
|
||||||
|
orchestrate 하는 single-op-primitive 커널의 dispatch overhead 를 부풀린다.
|
||||||
|
`K = N_K · TILE_K` 의 chunked-prefetching async GEMM 은 K-tile 마다
|
||||||
|
`DmaReadCmd` (B-chunk load) **와** `GemmCmd` (per-chunk `tl.dot`) **와**
|
||||||
|
`MathCmd` (누적 `tl.add`) 를 발행하므로, per-cmd FIXED 가 dispatch
|
||||||
|
예산을 지배한다. single-op fast-path 는 D1 이 잡으려던 *구조적 cmd 수
|
||||||
|
신호* (`N` 개의 single-op cmd 를 발행하는 recipe 가 `N` tile 을 내부로
|
||||||
|
묶는 composite 하나보다 더 비싸야 한다는 점) 를 유지하면서, 40-cycle
|
||||||
|
일반 control-path 비용을 덧씌우는 모델링 과잉 청구는 분리한다.
|
||||||
|
|
||||||
|
**왜 굳이 8 cycles 인가.** 위 조사 범위 (TMA ~1, descriptor-ring ~15) 의
|
||||||
|
중간값. 의도적인 *모델링 디폴트* — 측정된 HW 수치가 아님. 이 디폴트의 역할
|
||||||
|
은 정성적 동작에서 single-op dispatch path 를 composite control path 와
|
||||||
|
구별하는 것. Topology override 가능 (D4).
|
||||||
|
|
||||||
|
**Composite 수치에 미치는 영향.** Composite tile 내부 stage (HW tile 당
|
||||||
|
`DMA_READ`, `FETCH`, `GEMM`, `STORE`, `DMA_WRITE`) 는 scheduler 의
|
||||||
|
tile-feeder loop 가 발행하므로 host-side TLContext 의 `_charge_dispatch`
|
||||||
|
를 안 거친다. 단일 `CompositeCmd` 자체는 여전히 40-cycle control-path
|
||||||
|
FIXED 를 지불한다. D8 은 user-side single-op primitive (`tl.load` /
|
||||||
|
`tl.store` / `tl.dot` / `tl.add` / …) 에 매기는 FIXED 만 바꾼다.
|
||||||
|
Composite 측정치는 안정 유지.
|
||||||
|
|
||||||
|
## Alternatives
|
||||||
|
|
||||||
|
### A1. Revision 1 의 op-type calibration 표 유지
|
||||||
|
|
||||||
|
기각: calibration 비용이 |kinds| 에 비례, 그리고 표가 잡으려던 *ratio* 는
|
||||||
|
구조적으로 cmd 크기의 함수. 구조 공식은 같은 정성적 동작을 N 개가 아닌
|
||||||
|
2 개의 calibratable 숫자로 달성.
|
||||||
|
|
||||||
|
### A2. byte-only 공식 (FIXED 항 없음)
|
||||||
|
|
||||||
|
기각. FIXED = 0 이면 opt2 (ADR-0065 Option Y) 가 opt3 를 **이기지 못함**
|
||||||
|
— per-tile dispatch *총 bytes* 가 비슷 (opt3 ≈ 232, opt2 ≈ 380); win 은
|
||||||
|
전적으로 *per-cmd fixed cost 횟수 감소* 에서 옴. byte-only 는 모델이
|
||||||
|
보여야 할 신호를 지움.
|
||||||
|
|
||||||
|
### A3. PE_SCHEDULER 에 dispatch 비용 부과
|
||||||
|
|
||||||
|
기각: saturation 질문은 *"CPU 가 descriptor 를 push 하는 속도가 엔진을
|
||||||
|
계속 바쁘게 할 수 있는가?"* — **PE_CPU** 의 issue-bandwidth 특성.
|
||||||
|
scheduler 에 부과하면 CPU back-pressure 모델링 안 됨.
|
||||||
|
|
||||||
|
### A4. DMA program/setup 시간을 별도 fixed per-descriptor 비용으로 모델
|
||||||
|
|
||||||
|
연기: 처음엔 descriptor-program 비용을 **issuing op 의** dispatch cost 에
|
||||||
|
fold. calibration 이 분리 필요성을 보이면 PE_DMA fixed setup 으로 분리.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
- hybrid 의 CPU-offload / saturation win (ADR-0060 §1) 이 **측정 가능**,
|
||||||
|
구조적으로 정직한 모델 (calibration 표 없음).
|
||||||
|
- 새 op_kind 추가 (예: ADR-0065 의 `softmax_merge` 8-step recipe) 가
|
||||||
|
*zero cost* — 같은 공식 자동 적용.
|
||||||
|
- 하드웨어에 더 충실 (queue-head MMIO RTT + queue-write 대역폭).
|
||||||
|
|
||||||
|
### Negative
|
||||||
|
- **모든** bench goldens 변화 → 일회성 재생성 (D6); CI 골든 fixtures 업데이트.
|
||||||
|
- 두 calibration knob (FIXED, R) 필요; default 는 문서화된 가정에 기반 —
|
||||||
|
HW reference 없는 동안엔 절대 latency 를 잠정 취급, **ratio** 만 방어.
|
||||||
|
- 각 PE command dataclass 에 작은 `logical_bytes` property 추가.
|
||||||
|
|
||||||
|
## Open review items
|
||||||
|
|
||||||
|
1. **FIXED 와 R 의 calibration 출처.** default 는 "일반 composite ≈ 43 ns
|
||||||
|
+ on-die 큐 16 bytes/cycle"; on-die descriptor 큐로 합리.
|
||||||
|
HW reference 등장 시 재방문.
|
||||||
|
2. **대형 composite 에서 Scheduler plan-gen 비용.** 0 유지 — D5 가
|
||||||
|
PE_SCHEDULER 의 plan 생성을 dispatch 공식 밖에 둠. D7 cap (1024 bytes
|
||||||
|
≈ 30–35 OpSpec) 이 최악을 묶지만, cap 근처 composite 도 현재
|
||||||
|
zero-cost 모델에서 1-op composite 와 같은 scheduler 비용. stress test
|
||||||
|
(대형 composite microbench) 가 scheduler-bound 행동을 보이면
|
||||||
|
per-op-count `overhead_ns` 노출.
|
||||||
|
3. **Override 위치.** topology yaml 의 PE 노드 attrs 아래 `pe_cost_model:`
|
||||||
|
block — 모든 knob 을 한 곳에, 리뷰 가능. clock 은 PE 의 기존
|
||||||
|
`clock_freq_ghz` 에서 — 여기 중복 안 함.
|
||||||
|
4. **경로 parity.** greenlet (`_execute_legacy`, `kernel_runner`) 와
|
||||||
|
replay 둘 다 같은 cost model 읽어야 함. 검증.
|
||||||
|
5. **R 에 대한 결론의 민감도.** opt2 < opt3 가 합리적 `R` 범위 (큐 대역폭
|
||||||
|
가정) 에서 유지되어야 함. 민감도 sweep 이 Test Requirements (#9) 의
|
||||||
|
일부.
|
||||||
|
|
||||||
|
## Test Requirements
|
||||||
|
|
||||||
|
테스트는 specific 숫자 anchor 가 아닌 **공식 (D1)** 에 대해 작성 —
|
||||||
|
calibration 이 바뀌거나 OpSpec/CompositeCmd 필드가 추가되어도 유효.
|
||||||
|
|
||||||
|
1. **공식 보존.** 임의 `CompositeCmd` `c` 에 대해 PE_CPU 의 기록된
|
||||||
|
dispatch overhead 가 `FIXED_PER_CMD + c.logical_bytes × R`
|
||||||
|
(floor/round-off 의 ±1 cycle 내). 여러 composite parametrize:
|
||||||
|
1-OpSpec GEMM composite, 5-op MATH chain, 10-op recipe composite.
|
||||||
|
Default-calibration 숫자 (1-OpSpec composite 의 anchor ≈43 ns) 는
|
||||||
|
*informative* 참고, 테스트 gate 가 아님 — gate 는 공식 등가.
|
||||||
|
2. **정성적 ratio (robust).** opt3 per-tile PE_CPU dispatch 가 opt2
|
||||||
|
per-tile dispatch 를 최소 2× 차이로 엄격히 초과 — `opt3 > 2 × opt2`.
|
||||||
|
Default-calibration 모델은 ≈4× 예측; gate 는 느슨한 2× 한계라
|
||||||
|
calibration 이동 (예: HW reference 가 default 대체) 시 깨지지 않음.
|
||||||
|
Informative 숫자 — ADR-0065 §verification 와 DDD-0065 §11 의 모델
|
||||||
|
기대치 참조.
|
||||||
|
3. **Override 경로.** topology yaml 의 `pe_cost_model:` block 이 per-PE
|
||||||
|
dispatch cost 변경; block 누락 시 default 복귀. #1 의 공식 등가가
|
||||||
|
override 값에서도 성립.
|
||||||
|
4. **`PeCpuOverheadCmd` 우회.** 수동 `tl.cycles(n)` 는 정확히 `n` cycles,
|
||||||
|
`n + dispatch_cycles(...)` 아님.
|
||||||
|
5. **double-count 없음.** PE_DMA `drain_ns`, PE_GEMM/MATH `_compute_ns` 가
|
||||||
|
pre-ADR 값과 동일.
|
||||||
|
6. **결정성.** 동일 입력 → 동일 op_log + latency (SPEC §0.1).
|
||||||
|
7. **경로 parity.** greenlet 과 replay 가 동일 커널에 대해 동일
|
||||||
|
dispatch-cycle 회계.
|
||||||
|
8. **Composite 크기 cap (D7).** `logical_bytes >
|
||||||
|
max_composite_logical_bytes` 가 될 composite 는 emit 시점에 `ValueError`
|
||||||
|
(segmentation 없음). cap 이내 composite 는 정상 emit.
|
||||||
|
9. **민감도 (정성적).** `R ∈ {0.25, 0.0625, 0.03125}` cycles/byte 에서
|
||||||
|
3 지점 모두 `opt3 > opt2`. 방향 (R 감소 시 ratio 단조 증가) 도 단언,
|
||||||
|
단 절대 ratio 값은 *불필요*.
|
||||||
|
|
||||||
|
## Migration
|
||||||
|
|
||||||
|
ADR-0064 Revision 2 는 단일 PR 로 land:
|
||||||
|
- 각 `PeCommand` dataclass 의 `logical_bytes` property (D2 의 타입-aware
|
||||||
|
extra 카운트)
|
||||||
|
- `pe_cpu.py` dispatch 경로의 공식 적용
|
||||||
|
- PE_CPU init 의 `pe_cost_model:` override read (cycle-domain knob +
|
||||||
|
`max_composite_logical_bytes`)
|
||||||
|
- **composite 크기 cap (D7)** — TLContext-side 하드 cap,
|
||||||
|
`max_composite_logical_bytes` default 1024: cap 초과 composite 는 emit
|
||||||
|
시점에 `ValueError` (자동 segmentation 없음). 기존 bench 는 트리거
|
||||||
|
안 됨 (현재 최대 composite 가 ~322 bytes), 하지만 recipe 가 커질 때
|
||||||
|
descriptor-capacity 제한이 강제되도록 체크 land.
|
||||||
|
- 일회성 골든 재생성
|
||||||
|
|
||||||
|
land 후 ADR-0065 가 위에 쌓임 — 기존 bench 추가 골든 churn 없음
|
||||||
|
(ADR-0065 는 기존 경로의 `CompositeCmd` 의미 보존 refactor; opt2 만 새 bench).
|
||||||
@@ -0,0 +1,440 @@
|
|||||||
|
# ADR-0065: 평평한 ops `CompositeCmd` + 첫 stateful recipe (`softmax_merge`)
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
> **ADR-0060** (AHBM GQA Fused Attention) 의 보조 ADR. §5.6 / §8 item 4 의
|
||||||
|
> carve-out 을 정확히 구현: decode opt2 = (#1 기존 Q·Kᵀ GEMM composite) +
|
||||||
|
> (#2 softmax + P·V + online-softmax merge 를 담은 단일 composite). ADR-0060
|
||||||
|
> §5.6 의 "ex_composite" / "flash_pv_merge" 명칭은 *retire* — 본 ADR 이
|
||||||
|
> 정식 모양을 확정: 기존 `tl.composite` 입구를 그대로 사용하면서 두 가지
|
||||||
|
> 구조적 추가 — (a) 평평한 `ops` 튜플, (b) MATH micro-op 시퀀스로
|
||||||
|
> 펼쳐지는 첫 stateful recipe `softmax_merge`.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
### ADR-0060 가 이 ADR 에 남긴 것
|
||||||
|
|
||||||
|
ADR-0060 §5.6 는 opt3 (software pipelining) 를 *지금 ship* 으로 권고,
|
||||||
|
opt2 는 — per-tile dispatch 적음, K-before-V DMA priority,
|
||||||
|
**ADR-0064 의 cost model 이후에만 측정 가능** — 후속으로 carve out.
|
||||||
|
|
||||||
|
§8 item 4 가 carve-out 의 sizing: "재방문 시 **두 개** composite 로 분할 —
|
||||||
|
`#1` = Q·Kᵀ (기존 composite + `scale`), `#2` = softmax + P·V + 온라인-softmax
|
||||||
|
누산기 merge". 본 ADR 이 `#2` 를 구현.
|
||||||
|
|
||||||
|
### composite 가 구조 변경이 필요한 이유
|
||||||
|
|
||||||
|
현재 `CompositeCmd` 는 `(op, a, b, out_addr, ops=(head, *epi))` 모양 + 암묵 컨벤션:
|
||||||
|
- `op` 가 head 엔진 선택 (gemm | math)
|
||||||
|
- `ops[0]` 는 head OpSpec, `ops[1:]` 는 epilogue OpSpec 으로 head *이후*
|
||||||
|
per-output-tile 또는 per-K-tile 에 실행 (scope 결정)
|
||||||
|
|
||||||
|
decode opt2 는 MATH op 이 head GEMM **이전** 에 실행 (`m, l, O` 의
|
||||||
|
online-softmax 갱신 + GEMM 입력 `P` emit) 이 필요. 현 모양엔 자리 없음.
|
||||||
|
|
||||||
|
이전에 검토 후 기각된 경로:
|
||||||
|
- softmax_merge 를 #1 (Q·Kᵀ) 의 *epilogue* 로 두기 — `Sj` 를 읽고 *다음*
|
||||||
|
GEMM (#2 P·V) 가 소비할 `P` 를 쓰는 구조라 순환.
|
||||||
|
- 단일 mixed-engine recipe `flash_pv_merge` 가 MATH + GEMM + MATH 흡수 —
|
||||||
|
engine boundary 위반, PE_SCHEDULER 복잡화.
|
||||||
|
|
||||||
|
깔끔한 모양: **command 포맷에서 head/epilogue 구분 제거**. `ops` 를 평평한
|
||||||
|
순서 튜플로, 각 op 의 `scope` + position 이 tile-loop 배치 결정. "prologue"
|
||||||
|
개념은 *사용자 API* 의 `tl.composite(...)` 에 ergonomic 으로 남되 —
|
||||||
|
**HW command 는 보지 않음**.
|
||||||
|
|
||||||
|
### recipe 가 (generic op list 가 아닌) 이유
|
||||||
|
|
||||||
|
decode opt2 의 `#2` MATH chain 은 8 단계 고정 구조 (online softmax merge).
|
||||||
|
커널 작성자에게 8 단계 직접 와이어링은 verbose + correctness hazard
|
||||||
|
(단계 순서 의존). *Recipe* — TLContext 가 8 단계로 (주소 미리 채워)
|
||||||
|
펼치는 단일 명명 op (`softmax_merge`) — 가 커널을 간결히 + 구조를 단일
|
||||||
|
source 에 보존. recipe 표는 TLContext (compiler analog) 에 위치;
|
||||||
|
PE_SCHEDULER 는 안 읽음. D5 boundary 참조.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### D1. `CompositeCmd` 는 평평한 ordered op 리스트
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class CompositeCmd:
|
||||||
|
completion: CompletionHandle
|
||||||
|
ops: tuple[OpSpec, ...] # ordered MATH/GEMM ops
|
||||||
|
rw_handles: tuple[TensorHandle, ...] = () # cross-composite hazard
|
||||||
|
data_op: bool = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def logical_bytes(self) -> int: ... # ADR-0064 D2
|
||||||
|
```
|
||||||
|
|
||||||
|
이전의 `(op, a, b, out_addr, out_nbytes, math_op)` 필드는 `ops` 에 흡수.
|
||||||
|
`ops` 의 첫 OpSpec 이 `kind == "gemm"` 이면 head GEMM; 앞의 OpSpec 들이
|
||||||
|
pre-loop, 뒤의 OpSpec 들이 `scope` 에 따라 post-loop / per-tile epilogue.
|
||||||
|
|
||||||
|
### D2. `OpSpec` 이 named operand 보유
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class OpSpec:
|
||||||
|
kind: str # "gemm" | "rmax" | ...
|
||||||
|
scope: Scope # KERNEL | K_TILE | OUTPUT_TILE
|
||||||
|
operands: dict[str, TensorHandle] # named — D5 참조
|
||||||
|
extra: dict[str, Any] # scalars, axes, m/k/n 등
|
||||||
|
out: TensorHandle | None = None # 명시적 write-back handle
|
||||||
|
|
||||||
|
@property
|
||||||
|
def logical_bytes(self) -> int: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
named operand 로 PE_SCHEDULER 가 (positional 컨벤션 없이) 엔진 port 에
|
||||||
|
입력 라우팅 (예: GEMM 은 `operands["a"]`, `operands["b"]`).
|
||||||
|
|
||||||
|
### D3. Position + scope 가 tile-loop 배치 결정
|
||||||
|
|
||||||
|
**Semantics 분리.** *Position* 이 **phase** (tile-loop 수명주기 어디서
|
||||||
|
실행되는지) 결정; *scope* 가 그 phase 안에서의 **repetition** 결정.
|
||||||
|
`Scope.KERNEL` 의미는 "**`CompositeCmd` invocation 당 1 회**" — kernel
|
||||||
|
launch 당 1 회 아님. 그 1 회 실행이 op 의 시퀀스 내 *position* 에서
|
||||||
|
발생. 같은 KERNEL 값이 OpSpec 이 GEMM op 의 앞/위치/뒤에 있느냐에 따라
|
||||||
|
*phase* 가 달라짐.
|
||||||
|
|
||||||
|
PE_SCHEDULER 가 `cmd.ops` 에서 GEMM op 검색 (composite 당 ≤ 1, D6 참조).
|
||||||
|
인덱스 `g`:
|
||||||
|
|
||||||
|
| OpSpec position | scope | Phase | plan 내 배치 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `0 .. g-1` | KERNEL | pre-tile-loop | tile loop 진입 전 1 회 |
|
||||||
|
| `g` | KERNEL | head GEMM | `extra["m"], extra["k"], extra["n"]` 로 tile loop drive |
|
||||||
|
| `g+1 .. ` | K_TILE | in-accumulation | per K-tile epilogue (K 누산 loop 안) |
|
||||||
|
| `g+1 .. ` | OUTPUT_TILE | per-output-tile | per (m, n) tile epilogue (K 누산 후) |
|
||||||
|
| `g+1 .. ` | KERNEL | post-tile-loop | tile loop 종료 후 1 회 |
|
||||||
|
|
||||||
|
GEMM op 가 없는 composite (예: MATH-only) 는 모든 op 를 KERNEL-scope
|
||||||
|
직렬로 처리 — phase 가 "순서대로 single-shot" 으로 collapse.
|
||||||
|
|
||||||
|
### D4. PE_SCHEDULER 가 operand `pinned` 플래그 보고 DMA 자동 삽입
|
||||||
|
|
||||||
|
PE_SCHEDULER 가 GEMM 의 `operands` 검사. 각각:
|
||||||
|
- **not `pinned`** (데이터가 HBM 에 있어 스트리밍 필요) → FETCH/GEMM Stage
|
||||||
|
앞에 DMA_READ Stage 삽입
|
||||||
|
- **`pinned`** (이전 `tl.load` 로 이미 TCM 에 staged, 또는 recipe 의 TCM
|
||||||
|
scratch / primary-out `P`) → DMA Stage 없음, in-place 소비
|
||||||
|
|
||||||
|
커널은 composite operand 의 명시적 DMA cmd 를 절대 emit 안 함;
|
||||||
|
PE_SCHEDULER 가 핸들에서 추론. `tl.ref` operand 는 not pinned (DMA_READ);
|
||||||
|
`tl.load` 결과와 recipe scratch / primary-out 은 pinned (in-place).
|
||||||
|
|
||||||
|
> **as-built 노트 (P3).** D4 의 초안은 DMA 결정을 `space` 태그
|
||||||
|
> (`hbm`→DMA, `tcm`→in-place) 에서 도출하며 현재 `space` 값
|
||||||
|
> (오늘 `tl.load`=`hbm`, `tl.ref`=`tcm`) 을 뒤집어야 했음. 구현은 기존
|
||||||
|
> **`pinned` 플래그**를 DMA 신호로 그대로 유지 — `pinned` 이 이미
|
||||||
|
> "이미 TCM 에 있음, DMA skip" 을 인코딩하고, `space` 뒤집기는 기존 bench
|
||||||
|
> Stage 시퀀스를 바꾸면서 기능적 이득이 없음. `space` 통일 + `pinned`
|
||||||
|
> 폐기는 후순위 cleanup 으로 연기.
|
||||||
|
|
||||||
|
**Prologue recipe op 은 TCM-only (Phase 1 한계, TLContext emit 시 강제).**
|
||||||
|
*prologue recipe* (비 GEMM) OpSpec 의 모든 operand 는 **반드시** TCM 상주;
|
||||||
|
HBM 핸들을 recipe operand 로 전달하면 emit 시 validation error. Phase 1
|
||||||
|
에서 DMA staging 을 head op 의 operand 로 제한. **head op 자체** (gemm
|
||||||
|
또는 math) 는 기존 DMA-staged-from-HBM 동작 유지 — D4 의 TCM-only 룰은
|
||||||
|
head 에 적용 **안 됨**. 이 invariant 는 D6 #7 에 재기술.
|
||||||
|
|
||||||
|
### D5. RECIPE_DESCRIPTORS — TLContext 내부, `pe_commands.py` 가 아님
|
||||||
|
|
||||||
|
Recipe 는 새 TLContext-adjacent 모듈
|
||||||
|
(`src/kernbench/triton_emu/tl_recipes.py`) 에 위치. PE_SCHEDULER 는
|
||||||
|
import **안 함**. 첫 recipe:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class RecipeDescriptor:
|
||||||
|
operands: dict[str, str] # name → "R" | "RW"
|
||||||
|
primary_out: PrimaryOutSpec | None # 암묵 primary output 의 type/shape 룰
|
||||||
|
tile_alignment: Literal["single_shot", "tile_aligned"]
|
||||||
|
internal_scratch_bytes_fn: Callable[..., int]
|
||||||
|
engine_seq: tuple[EngineOp, ...] # TLContext 가 평평한 OpSpec 으로 펼침
|
||||||
|
|
||||||
|
|
||||||
|
RECIPE_DESCRIPTORS["softmax_merge"] = RecipeDescriptor(
|
||||||
|
operands={"s": "R", "m": "RW", "l": "RW", "O": "RW"},
|
||||||
|
primary_out=PrimaryOutSpec(
|
||||||
|
from_shape="s", from_dtype="s", transform="identity",
|
||||||
|
),
|
||||||
|
tile_alignment="single_shot",
|
||||||
|
internal_scratch_bytes_fn=lambda G, TILE, d, bpe: bpe * (
|
||||||
|
G + G + G + G * TILE + G # m_loc + m_new + corr + P + l_loc
|
||||||
|
),
|
||||||
|
engine_seq=(
|
||||||
|
EngineOp("MATH", "rmax", src="s", dst="m_loc", reduce_axis=-1),
|
||||||
|
EngineOp("MATH", "max_elem", src_a="m", src_b="m_loc", dst="m_new"),
|
||||||
|
EngineOp("MATH", "exp_diff", src_a="m", src_b="m_new", dst="corr"),
|
||||||
|
EngineOp("MATH", "exp_diff", src_a="s", src_b="m_new", dst="P", bcast_axis=0),
|
||||||
|
EngineOp("MATH", "rsum", src="P", dst="l_loc", reduce_axis=-1),
|
||||||
|
EngineOp("MATH", "fma", src_a="l", src_b="corr", src_c="l_loc", dst="l"),
|
||||||
|
EngineOp("MATH", "mul_bcast", src_a="O", src_b="corr", dst="O", bcast_axis=1),
|
||||||
|
EngineOp("MATH", "copy", src="m_new", dst="m"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
TLContext 가 `tl.composite(prologue=[{"op": "softmax_merge", ...}],
|
||||||
|
op="gemm", ...)` 에서:
|
||||||
|
1. operand R/RW 를 `RECIPE_DESCRIPTORS["softmax_merge"]` 와 검증.
|
||||||
|
2. scratch (m_loc, m_new, corr, P, l_loc) 를 scratch_scope helper 로 할당 (ADR-0063).
|
||||||
|
3. primary output P 의 shape 을 `s.shape` 에서 derive (identity).
|
||||||
|
4. `engine_seq` 를 8 개의 평평한 MATH OpSpec 으로 펼침 (모든 주소/크기 채움).
|
||||||
|
각 OpSpec 의 `scope = KERNEL`.
|
||||||
|
5. **Auto-bind (충돌 검사, D6.6).** head GEMM 이 `operands["a"]` 를
|
||||||
|
*명시하지 않은* 경우에만 `operands["a"] = P_handle` auto-bind.
|
||||||
|
커널이 `a` 를 명시적으로 제공했다면 validation error — 모호한
|
||||||
|
primary-output 대체.
|
||||||
|
6. `CompositeCmd(ops=(8 MATH + 1 GEMM + epilogue), rw_handles=(m, l, O))` emit.
|
||||||
|
|
||||||
|
**RECIPE_DESCRIPTORS 는 HW 경로 어디에도 안 나타남**. PE_SCHEDULER 는
|
||||||
|
평평한 ops 리스트만 봄.
|
||||||
|
|
||||||
|
### D6. Invariants
|
||||||
|
|
||||||
|
1. **GEMM 수.** `0 ≤ count(op.kind == "gemm" in cmd.ops) ≤ 1`. GEMM 0 개면
|
||||||
|
MATH-only composite.
|
||||||
|
2. **softmax_merge 가 V operand 없음.** `RECIPE_DESCRIPTORS[
|
||||||
|
"softmax_merge"].operands` 에 HBM-resident handle 없음 → prologue 동안
|
||||||
|
DMA 삽입 없음 → V DMA 는 GEMM head 시작 시에만 발생 → decode opt2 의
|
||||||
|
#1 / #2 간 K-before-V DMA priority 자연 강제.
|
||||||
|
3. **Cross-composite strict FIFO.** PE_SCHEDULER 가 in-flight `rw_handles`
|
||||||
|
추적. 신규 composite 의 `rw_handles` (또는 op 입력) 가 in-flight 와
|
||||||
|
교차하면 모든 이전 composite 완료까지 대기 — out-of-order reorder 없음.
|
||||||
|
4. **legacy caller 후방 호환.** 사용자 API `tl.composite(op="gemm", a, b,
|
||||||
|
epilogue=[...])` 보존. TLContext 가 내부적으로 평평한 ops `CompositeCmd`
|
||||||
|
로 lowering.
|
||||||
|
5. **PE_SCHEDULER recipe-free.** PE_SCHEDULER 는 RECIPE_DESCRIPTORS import
|
||||||
|
안 함, "softmax_merge" 의 존재 모름, recipe 별 코드 분기 없음.
|
||||||
|
6. **암묵 operand 대체 없음 (auto-bind 충돌).** prologue recipe 가
|
||||||
|
`primary_out` 선언하고 head op 의 auto-bind 대상 operand (예: GEMM
|
||||||
|
`a`) 도 *커널이 명시적* 으로 제공하면, TLContext 가 emit 시 validation
|
||||||
|
error. 커널은 recipe 가 바인딩하게 두거나 (operand 생략) **혹은**
|
||||||
|
명시적으로 지정 (prologue 생략, 또는 `primary_out` 없는 recipe 사용)
|
||||||
|
해야 함. "어느 값이 이기느냐" 의 모호함 방지.
|
||||||
|
7. **Prologue MATH operand TCM-only.** D4 의 재기술: *prologue recipe*
|
||||||
|
(비 GEMM) OpSpec 의 모든 operand 는 TCM 상주. head op (gemm 또는 math)
|
||||||
|
은 예외 — HBM 스트리밍 허용. Phase 1 에서 TLContext emit 시 강제.
|
||||||
|
|
||||||
|
### D7. Boundary 요약 (compiler vs scheduler vs engine)
|
||||||
|
|
||||||
|
```
|
||||||
|
HOST DEVICE
|
||||||
|
TLContext (compiler analog) PE_SCHEDULER (scheduler/dispatcher)
|
||||||
|
- RECIPE_DESC 와 검증 - 평평한 ops 리스트 스캔
|
||||||
|
- primary_out shape derive - GEMM 식별 (≤ 1)
|
||||||
|
- scratch 할당 - position-기반 stage 배치
|
||||||
|
- recipe → 평평한 OpSpec 펼침 - space 보고 DMA stage 자동 삽입
|
||||||
|
- rw_handles 계산 - strict-FIFO RW 해저드 추적
|
||||||
|
- CompositeCmd emit - Stage 리스트 emit
|
||||||
|
imports: RECIPE_DESCRIPTORS imports: recipe 관련 없음
|
||||||
|
|
||||||
|
ENGINES (PE_MATH / PE_GEMM / PE_DMA)
|
||||||
|
- Stage.params 읽음 (op_kind, addresses, n_elements)
|
||||||
|
- 기존 latency 모델 변화 없음
|
||||||
|
imports: recipe 관련 없음
|
||||||
|
```
|
||||||
|
|
||||||
|
### D8. Composite 출력 handle + 출력-space DMA
|
||||||
|
|
||||||
|
`tl.composite(...)` 는 **출력 `TensorHandle`** 을 반환(단순 `CompletionHandle`
|
||||||
|
아님) — composite 결과를 `tl.dot` 결과처럼 downstream op 에 먹일 수 있음.
|
||||||
|
handle 의 `pending` 필드가 composite 완료를 참조; downstream consumer 가
|
||||||
|
auto-await(`_await_pending`, ADR-0062 lazy 패턴), `tl.wait(handle)` 동작.
|
||||||
|
|
||||||
|
**`out` 은 항상 `TensorHandle`** (raw 주소 아님) — 위치는 커널의 선택, 추측 안 함:
|
||||||
|
|
||||||
|
- **HBM 출력** → `out=tl.ref(addr, shape)`. `tl.ref` 가 `space="hbm"` handle
|
||||||
|
반환 → composite 가 DMA_WRITE 로 write-back. STORE + DMA_WRITE 는 composite
|
||||||
|
파이프라인 **내부**에 유지 — fused op 의 일부이지 별도 `tl.store` 아님.
|
||||||
|
- **In-place TCM** → `out=<기존 TCM handle>` (예: recipe 누적기 `O`).
|
||||||
|
`space="tcm"` → STORE 만, 결과 TCM 에 남음 (chainable).
|
||||||
|
- **미지정** → TLContext 가 **TCM scratch 자동 할당** (`tl.dot` 과 동일) 후 반환.
|
||||||
|
|
||||||
|
편의상 `out_ptr: int` 는 **HBM 출력 shorthand** 로 허용 —
|
||||||
|
`out=tl.ref(out_ptr, <출력 shape>)` 와 동치(컴파일러가 `space="hbm"` handle 로
|
||||||
|
감쌈). 출력 미지정 시 scratch 주소는 커널이 아니라 컴파일러가 소유.
|
||||||
|
(`tl.ref` 는 `space="hbm"` 반환 — HBM 데이터 참조이므로. operand-**입력** DMA
|
||||||
|
결정은 D4 대로 `pinned` 기반이라 이 라벨이 입력 스트리밍에 영향 없음.)
|
||||||
|
|
||||||
|
출력 handle 의 **`space` 가 타일 루프의 write-back 을 결정** (D4 의 operand
|
||||||
|
`pinned` 규칙의 출력판):
|
||||||
|
|
||||||
|
| `out.space` | 타일 루프 write-back |
|
||||||
|
| --- | --- |
|
||||||
|
| `"tcm"` | STORE 만 — TCM 에 남음 (chainable; **DMA_WRITE 없음**) |
|
||||||
|
| `"hbm"` | STORE + DMA_WRITE — 최종 HBM 출력 |
|
||||||
|
|
||||||
|
이로써 출력이 TCM 에 남는 composite(예: recipe 누적기)가 합법이 되고,
|
||||||
|
TCM-scratch 주소가 DMA 엔진의 PA 디코더(고비트 scratch 주소를 거부)로 가는
|
||||||
|
일을 막음. 기존 벤치는 HBM 출력을 `tl.ref` 로 감싸므로 write-back
|
||||||
|
불변(byte-equal).
|
||||||
|
|
||||||
|
## Alternatives
|
||||||
|
|
||||||
|
### A1. tile 당 3 composite (prologue 개념 없이)
|
||||||
|
|
||||||
|
#2 를 `softmax_merge` + `gemm(P·V)` + `add(O)` 의 3 개로 분할. 장점:
|
||||||
|
flat-ops 재구조화 없음 (`add` 가 기존 epilogue 경로 재사용). 단점: tile
|
||||||
|
당 dispatch 3 회 (대신 2) — ADR-0060 §8 item 4 sizing ("**두 개** 로
|
||||||
|
분할") 과 어긋남; ~32 ns × N_tiles 의 fixed-cost 절감 소실. **기각.**
|
||||||
|
|
||||||
|
### A2. Mixed-engine recipe (`flash_pv_merge`)
|
||||||
|
|
||||||
|
MATH + GEMM + MATH 흡수하는 단일 recipe (ADR-0060 §5.6 원안 표현). 장점:
|
||||||
|
가장 적은 dispatch (1 composite). 단점: PE_SCHEDULER 가 engine-경계
|
||||||
|
recipe 알아야 함, "엔진은 Stage.op_kind 만" boundary 위반, recipe 표가
|
||||||
|
ISA-급이 되어 HW 인터페이스 오염. **기각.**
|
||||||
|
|
||||||
|
### A3. Generic op-list DAG (`tl.ex_composite([...])`)
|
||||||
|
|
||||||
|
사용자 정의 임의 op 시퀀스 + 명명 슬롯 + 데이터플로 분석. 장점: 최대
|
||||||
|
유연성. 단점: 단일 user (softmax_merge) 가 generic DAG 언어를 정당화 못함;
|
||||||
|
PE_SCHEDULER 에 데이터플로 분석 필요; ADR-0060 §8 item 4 의 "inner loop
|
||||||
|
흡수" 트랩 재등장. **기각 (Simplicity First).**
|
||||||
|
|
||||||
|
### A4. RW-aware scheduler reorder (strict FIFO 가 아님)
|
||||||
|
|
||||||
|
PE_SCHEDULER 가 `rw_handles` 교차 없으면 후속 composite 가 in-flight 를
|
||||||
|
추월 허용 (예: 다음 tile 의 #1 가 현재 tile 의 #2 와 겹침 — #1 은
|
||||||
|
m/l/O 안 만짐). 장점: 더 많은 overlap. 단점: scheduler state 증가, 이득
|
||||||
|
한정 (다음 #1 은 어차피 K DMA 대기). **연기** — strict FIFO 먼저;
|
||||||
|
RW-aware reorder 는 후속 ADR 가능.
|
||||||
|
|
||||||
|
### A5. softmax_merge 를 #1 (Q·Kᵀ) 의 epilogue 로 embedding
|
||||||
|
|
||||||
|
오답: softmax_merge 의 출력 `P` 는 #2 의 P·V GEMM 의 입력, #1 의
|
||||||
|
epilogue *이후* 실행. embedding 하면 #2 가 자기 입력을 #1 의 epilogue
|
||||||
|
scratch 에서 읽는 형태 — 순환 tile-loop 의존성. **기각 (incorrect).**
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
- ADR-0060 §5.6 / §8 item 4 carve-out 정확 구현: opt2 = tile 당 2
|
||||||
|
composite, K-before-V DMA priority 자연.
|
||||||
|
- composite contract 균일화: scope-기반 배치의 평평한 ops 리스트. HW cmd
|
||||||
|
에서 "head" 와 "epilogue" 구조 구분 없음.
|
||||||
|
- DMA 가 operand `space` 에서 자동 추론; 커널 표면 단순화.
|
||||||
|
- HW 인터페이스 (`CompositeCmd` 모양) 가 recipe 증가에도 작게 유지 —
|
||||||
|
recipe 펼침이 host-side.
|
||||||
|
- 새 fused 패턴 (linear+gelu+norm, rmsnorm+linear 등) 은 `RECIPE_DESCRIPTORS`
|
||||||
|
entry 추가만으로 가능; PE_SCHEDULER 변경 없음.
|
||||||
|
|
||||||
|
### Negative
|
||||||
|
- CompositeCmd 구조 변경 — 모든 현재 caller 의 refactor. 의미 보존
|
||||||
|
(D6.4); ADR-0064 Revision 2 land 후 기존 골든 불변.
|
||||||
|
- `OpSpec.operands` 가 positional `tuple[Any, ...]` 에서
|
||||||
|
`dict[str, TensorHandle]` 로 변경 — 기존 epilogue lowering 접촉.
|
||||||
|
- 새 TLContext 코드: recipe 펼침 + scratch slot 할당 + primary_out
|
||||||
|
auto-bind. ~80–120 LOC.
|
||||||
|
- PE_SCHEDULER 의 `_generate_plan` 에 position-기반 스캔 + DMA 자동 삽입
|
||||||
|
+ strict-FIFO RW tracker. ~60–100 LOC.
|
||||||
|
- **strict-FIFO RW 해저드 추적이 안전하지만 보수적.** 두 composite 가
|
||||||
|
`rw_handle` 을 공유할 때 신규는 모든 이전 겹치는 composite 가 *완전히
|
||||||
|
완료* 될 때까지 대기 — 원칙적으로 overlap 가능한 경우에도
|
||||||
|
(예: 다음-tile #1 = Q·Kᵀ 가 m/l/O 안 만지므로 현재-tile 의 #2 와
|
||||||
|
overlap 가능하지만, 같은 핸들을 만지는 후속 path 도 FIFO 로 직렬화).
|
||||||
|
더 똑똑한 scheduler 대비 **composite-level overlap 을 과소 노출**.
|
||||||
|
RW-aware reorder (A4) 가 연기된 개선.
|
||||||
|
|
||||||
|
## Open review items
|
||||||
|
|
||||||
|
1. **identity 를 넘는 recipe shape derivation 룰.** 미래 recipe 가
|
||||||
|
non-identity transform (예: 전치된 primary_out) 필요할 수 있음.
|
||||||
|
`PrimaryOutSpec.transform` 필드가 forward-compat; 필요 시 새 transform 추가.
|
||||||
|
2. **Recipe scratch allocator** 의 ADR-0063 `scratch_scope` 통합. 초기
|
||||||
|
배선: TLContext 가 활성 `scratch_scope` 가 있으면 그 안에서 할당,
|
||||||
|
없으면 kernel-scope persistent slot.
|
||||||
|
3. **`tl_recipes.py` 위치.** `triton_emu/` 아래 새 모듈. recipe 지식을
|
||||||
|
compiler analog (TLContext) 와 함께 위치.
|
||||||
|
4. **GEMM `out=TensorHandle`.** 기존 `out_ptr: int` 와 함께 새 형태.
|
||||||
|
둘 다 허용; handle 형태가 신규 코드에 권장 (handle identity 로
|
||||||
|
strict-FIFO RW 추적 가능).
|
||||||
|
|
||||||
|
## Test Requirements
|
||||||
|
|
||||||
|
1. **CompositeCmd 평평화 refactor — 의미 보존.** 기존 모든 bench 의
|
||||||
|
op_log 가 refactor 전후로 byte-equal (legacy API `tl.composite(
|
||||||
|
op="gemm", a, b, epilogue=[...])` 가 같은 Stage 시퀀스로 lowering).
|
||||||
|
2. **softmax_merge recipe lowering.** TLContext 호출 `tl.composite(
|
||||||
|
prologue=[{"op": "softmax_merge", "s": Sj, "m": m, "l": l, "O": O}],
|
||||||
|
op="gemm", b=V_ref, out=O, epilogue=[{"op": "add", "other": O}])` 가
|
||||||
|
정확히 10 ops (8 MATH + 1 GEMM + 1 MATH(add)) + `rw_handles == (m, l, O)`
|
||||||
|
의 `CompositeCmd` 생성.
|
||||||
|
3. **K-before-V DMA priority invariant.** decode opt2 의 op_log 에서
|
||||||
|
#2 의 MATH prologue 동안 V 관련 DMA 없음 (#2 의 V DMA 는 GEMM head
|
||||||
|
시작 시에만).
|
||||||
|
4. **Strict-FIFO RW 직렬화.** `rw_handle` 공유하는 두 연속 composite 가
|
||||||
|
dispatch 순서대로 완료; stage 가 interleave 안 함.
|
||||||
|
5. **`pinned` 에서 DMA 자동 삽입.** not-`pinned` operand (예: `b=tl.ref(...)`)
|
||||||
|
의 GEMM composite 가 DMA_READ Stage emit; `pinned` operand (예: recipe
|
||||||
|
의 TCM scratch / primary-out, 또는 `tl.load` 결과) 는 emit 안 함.
|
||||||
|
(as-built D4 노트: DMA 결정은 `space` 태그가 아니라 기존 `pinned`
|
||||||
|
플래그 사용.)
|
||||||
|
6. **opt2 가 opt3 와 수치 동등.** data mode 에서 opt2 의 최종
|
||||||
|
`(m, l, O)` 가 opt3 와 fp tolerance 안.
|
||||||
|
7. **opt2 dispatch ratio (ADR-0064 Rev2 이후) — robust.** opt3 vs opt2
|
||||||
|
per-tile PE_CPU dispatch cycles 가 `opt3 > 2 × opt2` 만족
|
||||||
|
(정성적 gate, calibration-독립). Default-calibration 모델 기대치는
|
||||||
|
≈ 4.0× (FIXED=40 cycles, R=0.0625 cycles/byte) — DDD-0065 §11 의
|
||||||
|
모델-유도 숫자 참조; 테스트 gate 는 느슨한 `> 2×` 한계라
|
||||||
|
calibration 변경에도 살아남음. Ratio 가 FIXED-dominated —
|
||||||
|
command-count 감소가 1차 신호임을 반영.
|
||||||
|
8. **GEMM-count invariant.** GEMM OpSpec 두 개 가진 composite 가
|
||||||
|
TLContext emit 시 validation error.
|
||||||
|
9. **Composite 크기 cap 안에 (ADR-0064 D7).** Decode opt2 의 `#2`
|
||||||
|
composite (10 ops, ~322 logical bytes — operand 핸들의 per-op 합산)
|
||||||
|
가 default `MAX_COMPOSITE_LOGICAL_BYTES=1024` 안에 편안히 —
|
||||||
|
segmentation 없음. recipe lowering 테스트가 `cmd.logical_bytes
|
||||||
|
< 1024` 단언.
|
||||||
|
10. **Auto-bind 충돌.** `tl.composite(op="gemm", a=A_handle,
|
||||||
|
prologue=[{"op": "softmax_merge", ...}], ...)` 처럼 softmax_merge 가
|
||||||
|
`primary_out` 선언하는데 `a` 도 명시되면 emit 시 validation error
|
||||||
|
(D6.6).
|
||||||
|
11. **MATH operand TCM invariant.** `tl.composite(op="math",
|
||||||
|
a=tl.ref(addr, shape), ...)` (HBM-resident 핸들을 MATH operand 로
|
||||||
|
전달) 가 emit 시 validation error (D6.7).
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **ADR-0060** §5.6, §8 item 4 — 본 ADR 이 구현하는 carve-out.
|
||||||
|
- **ADR-0064 Revision 2** — dispatch cost model; 먼저 land, opt2 의
|
||||||
|
fewer-issues win 을 측정 가능하게 함.
|
||||||
|
- **ADR-0063** `scratch_scope` — recipe intermediate 가 안에 할당;
|
||||||
|
`m, l, O` 는 persistent arena 로 밖에 할당 (DDD-0060 §6.2 참조).
|
||||||
|
- **ADR-0046** `tl_context` contract — `prologue=[...]` kwarg,
|
||||||
|
`out=TensorHandle`, RECIPE_DESCRIPTORS lookup 으로 확장.
|
||||||
|
- **ADR-0042** tile plan generators — (재설계 아닌) 확장 — 평평한 ops
|
||||||
|
리스트 처리 + DMA 자동 삽입.
|
||||||
|
- **ADR-0014** PE pipeline — boundary 보존: scheduler 가 recipe-free,
|
||||||
|
엔진이 op_kind-opaque.
|
||||||
|
|
||||||
|
## Migration
|
||||||
|
|
||||||
|
> **구현 순서 노트.** 아래의 ADR-0064-우선 순서가 원래 계획이었음.
|
||||||
|
> 실제 구현은 **1↔2 단계를 뒤집어** — ADR-0065 Phase 1 (평평화
|
||||||
|
> `CompositeCmd`) 이 ADR-0064 Rev2 *보다 먼저* land. 이유: `logical_bytes`
|
||||||
|
> (ADR-0064 D2) 가 평평화 형태 위에서 자연스럽게 정의되므로, refactor 를
|
||||||
|
> 먼저 하면 버려지는 transitional `logical_bytes` 를 피함. 각 단계는
|
||||||
|
> 독립적으로 안전: P1 은 기존 cost 경로 하에서 의미 보존 refactor
|
||||||
|
> (op_log byte-equal); ADR-0064 Rev2 가 이미 평평화된 형태 위에 구조적
|
||||||
|
> 공식을 교체. 최종 상태는 어느 쪽이든 동일.
|
||||||
|
|
||||||
|
Land 순서 (원래 계획; as-built 1↔2 뒤집힘은 위 노트 참조):
|
||||||
|
1. **ADR-0064 Revision 2** (별도 PR): 구조적 dispatch cost +
|
||||||
|
`logical_bytes` property + topology config override + 일회성 골든 재생성.
|
||||||
|
2. **ADR-0065 Phase 1** (본 ADR, PR a): `CompositeCmd` 평평화 refactor +
|
||||||
|
legacy-API lowering. refactor only; 골든 불변.
|
||||||
|
3. **ADR-0065 Phase 2** (본 ADR, PR b): `tl_recipes.py` + `softmax_merge`
|
||||||
|
recipe + PE_SCHEDULER position-스캔 + DMA 자동 삽입 + strict-FIFO
|
||||||
|
RW tracker.
|
||||||
|
4. **ADR-0065 Phase 3** (본 ADR, PR c): `_gqa_decode_long.py` opt2
|
||||||
|
variant + 수치 동등 테스트 + opt3 대비 dispatch-ratio 측정.
|
||||||
|
|
||||||
|
상세 구현 계획: **DDD-0065** 참조.
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
# ADR Index
|
||||||
|
|
||||||
|
Auto-generated by `tools/generate_adr_index.py`. Total ADRs: **47**.
|
||||||
|
|
||||||
|
Classification mirrors the `/report` skill's section assignment. When adding a new ADR, also add an entry to the `CLASSIFICATION` table in `tools/generate_adr_index.py`.
|
||||||
|
|
||||||
|
## Design Principles
|
||||||
|
|
||||||
|
- [ADR-0013](./ADR-0013-ver-verification-strategy.md) — 검증 전략 및 Phase 1 테스트 계획
|
||||||
|
- [ADR-0033](./ADR-0033-lat-latency-model-assumptions.md) — 레이턴시 모델: 가정 및 알려진 단순화
|
||||||
|
|
||||||
|
## High-level Architecture
|
||||||
|
|
||||||
|
- [ADR-0003](./ADR-0003-dev-target-system-hierarchy.md) — 타겟 시스템 계층 및 모델링 범위 _(System hierarchy (Tray / SIP / CUBE / PE))_
|
||||||
|
- [ADR-0007](./ADR-0007-api-runtime-api-boundaries.md) — 런타임 API 및 시뮬레이션 엔진 경계 _(Runtime API ↔ sim_engine boundaries)_
|
||||||
|
- [ADR-0016](./ADR-0016-dev-iochiplet-noc-and-memory-path.md) — IOChiplet NoC와 메모리 데이터 경로 _(IOChiplet NOC and memory data path)_
|
||||||
|
- [ADR-0017](./ADR-0017-dev-cube-noc-and-hbm-connectivity.md) — 큐브 NoC와 HBM 연결성 _(Cube NOC and HBM connectivity)_
|
||||||
|
|
||||||
|
## Detailed Architecture
|
||||||
|
|
||||||
|
One subsection per component file under `src/kernbench/components/builtin/`.
|
||||||
|
|
||||||
|
### forwarding
|
||||||
|
|
||||||
|
- [ADR-0037](./ADR-0037-dev-forwarding-component.md) — Forwarding 컴포넌트 (forwarding_v1)
|
||||||
|
|
||||||
|
### hbm_ctrl
|
||||||
|
|
||||||
|
- [ADR-0034](./ADR-0034-dev-hbm-controller-internal-design.md) — HBM 컨트롤러 내부 설계
|
||||||
|
|
||||||
|
### io_cpu
|
||||||
|
|
||||||
|
- [ADR-0036](./ADR-0036-dev-io-cpu-component-model.md) — IO_CPU 컴포넌트 모델
|
||||||
|
|
||||||
|
### m_cpu
|
||||||
|
|
||||||
|
- [ADR-0035](./ADR-0035-dev-m-cpu-and-m-cpu-dma-component-model.md) — M_CPU 및 M_CPU.DMA 컴포넌트 모델
|
||||||
|
|
||||||
|
### pcie_ep
|
||||||
|
|
||||||
|
- [ADR-0038](./ADR-0038-dev-pcie-ep-component-model.md) — PCIE_EP Component Model
|
||||||
|
|
||||||
|
### pe_cpu
|
||||||
|
|
||||||
|
- [ADR-0014](./ADR-0014-dev-pe-pipeline-execution-model.md) — PE 파이프라인 실행 모델
|
||||||
|
|
||||||
|
### pe_dma
|
||||||
|
|
||||||
|
- [ADR-0014](./ADR-0014-dev-pe-pipeline-execution-model.md) — PE 파이프라인 실행 모델
|
||||||
|
- [ADR-0023](./ADR-0023-dev-ipcq-pe-collective.md) — PE-level IPCQ — Inter-PE Collective Communication
|
||||||
|
|
||||||
|
### pe_fetch_store
|
||||||
|
|
||||||
|
- [ADR-0014](./ADR-0014-dev-pe-pipeline-execution-model.md) — PE 파이프라인 실행 모델
|
||||||
|
|
||||||
|
### pe_gemm
|
||||||
|
|
||||||
|
- [ADR-0014](./ADR-0014-dev-pe-pipeline-execution-model.md) — PE 파이프라인 실행 모델
|
||||||
|
|
||||||
|
### pe_ipcq
|
||||||
|
|
||||||
|
- [ADR-0023](./ADR-0023-dev-ipcq-pe-collective.md) — PE-level IPCQ — Inter-PE Collective Communication
|
||||||
|
|
||||||
|
### pe_math
|
||||||
|
|
||||||
|
- [ADR-0014](./ADR-0014-dev-pe-pipeline-execution-model.md) — PE 파이프라인 실행 모델
|
||||||
|
|
||||||
|
### pe_mmu
|
||||||
|
|
||||||
|
- [ADR-0039](./ADR-0039-dev-pe-mmu-component-model.md) — PE_MMU Component Model — 컴포넌트 + 유틸리티 이중 역할
|
||||||
|
|
||||||
|
### pe_scheduler
|
||||||
|
|
||||||
|
- [ADR-0014](./ADR-0014-dev-pe-pipeline-execution-model.md) — PE 파이프라인 실행 모델
|
||||||
|
|
||||||
|
### pe_tcm
|
||||||
|
|
||||||
|
- [ADR-0040](./ADR-0040-dev-pe-tcm-component-model.md) — PE_TCM Component Model — 듀얼 채널 BW 직렬화
|
||||||
|
|
||||||
|
### sram
|
||||||
|
|
||||||
|
- [ADR-0041](./ADR-0041-dev-cube-sram-component-model.md) — Cube SRAM Component Model — terminal scratchpad on cube NoC
|
||||||
|
|
||||||
|
### tiling
|
||||||
|
|
||||||
|
- [ADR-0042](./ADR-0042-prog-tile-plan-generators.md) — Tile Plan Generators — GEMM/Math 파이프라인 plan 빌더
|
||||||
|
|
||||||
|
## Implementation Decisions
|
||||||
|
|
||||||
|
### Address Scheme
|
||||||
|
|
||||||
|
- [ADR-0001](./ADR-0001-mem-physaddr-layout.md) — 51비트 물리 주소 레이아웃 및 디코딩 계약
|
||||||
|
- [ADR-0011](./ADR-0011-mem-memory-addressing-simplification.md) — 메모리 주소 지정 — PA / VA / LA 주소 모델
|
||||||
|
|
||||||
|
### Routing & Helper API
|
||||||
|
|
||||||
|
- [ADR-0002](./ADR-0002-lat-routing-distance.md) — 라우팅 거리, 순서 및 우회 규칙
|
||||||
|
- [ADR-0051](./ADR-0051-lat-routing-helper-api.md) — Routing Helper API — `AddressResolver` + `PathRouter`
|
||||||
|
|
||||||
|
### Memory Semantics & Local-HBM Bandwidth
|
||||||
|
|
||||||
|
- [ADR-0004](./ADR-0004-mem-memory-semantics-local-hbm.md) — 메모리 시맨틱 및 로컬 HBM 대역폭 보장
|
||||||
|
|
||||||
|
### Topology Compilation, Diagrams & Builder Algorithms
|
||||||
|
|
||||||
|
- [ADR-0005](./ADR-0005-dev-diagram-views-distance-layout.md) — 다이어그램 뷰 및 거리 기반 레이아웃 규칙
|
||||||
|
- [ADR-0006](./ADR-0006-dev-topology-compilation-distance-diagram.md) — 토폴로지 컴파일, 거리 추출, 그리고 자동 다이어그램 생성
|
||||||
|
- [ADR-0053](./ADR-0053-dev-topology-builder-algorithms.md) — Topology Builder + Visualizer Algorithms
|
||||||
|
|
||||||
|
### Tensor Deployment and Allocation
|
||||||
|
|
||||||
|
- [ADR-0008](./ADR-0008-api-tensor-deploy-and-allocation.md) — 텐서 배포 및 할당 (호스트 할당기, PA 우선)
|
||||||
|
|
||||||
|
### Kernel Execution and Host-Device Messaging
|
||||||
|
|
||||||
|
- [ADR-0009](./ADR-0009-api-kernel-execution-messaging.md) — 커널 실행 메시징 및 완료 시맨틱
|
||||||
|
- [ADR-0012](./ADR-0012-api-host-io-message-schema.md) — Host ↔ IO_CPU 메시지 스키마 (PA-우선, PE-태깅)
|
||||||
|
|
||||||
|
### CLI Surface and Semantics
|
||||||
|
|
||||||
|
- [ADR-0010](./ADR-0010-api-cli-surface-and-semantics.md) — 명령줄 인터페이스 및 실행 시맨틱
|
||||||
|
|
||||||
|
### Component Port/Wire Fabric Model
|
||||||
|
|
||||||
|
- [ADR-0015](./ADR-0015-dev-component-port-wire-model.md) — 컴포넌트 포트/와이어 모델과 패브릭 라우팅
|
||||||
|
|
||||||
|
### Two-Pass Data Execution
|
||||||
|
|
||||||
|
- [ADR-0020](./ADR-0020-prog-data-execution-two-pass.md) — 2-Pass 데이터 실행 모델 (타이밍 / 데이터 분리)
|
||||||
|
|
||||||
|
### 2D Grid Program Identity
|
||||||
|
|
||||||
|
- [ADR-0022](./ADR-0022-prog-program-id-2d-grid.md) — 2D 그리드 program_id 시맨틱
|
||||||
|
|
||||||
|
### Parallelism (Launcher, DP, TP, AHBM backend, CCL algorithm)
|
||||||
|
|
||||||
|
- [ADR-0024](./ADR-0024-par-sip-tp-launcher.md) — SIP-level Launcher — rank = SIP
|
||||||
|
- [ADR-0026](./ADR-0026-par-dppolicy-intra-device.md) — DPPolicy = Intra-Device Only — sip/num_sips 필드 제거
|
||||||
|
- [ADR-0027](./ADR-0027-par-megatron-tp.md) — Megatron-style Tensor Parallelism API
|
||||||
|
- [ADR-0047](./ADR-0047-par-ahbm-ccl-backend.md) — AHBM CCL Backend — `torch.distributed`-compat shim
|
||||||
|
- [ADR-0050](./ADR-0050-par-ccl-algorithm-module-contract.md) — CCL Algorithm Module Contract — `ccl/algorithms/*.py`
|
||||||
|
|
||||||
|
### IPCQ Direction Addressing
|
||||||
|
|
||||||
|
- [ADR-0025](./ADR-0025-algo-ipcq-direction-addressing.md) — IPCQ Direction Addressing — address-based matching
|
||||||
|
|
||||||
|
### Intercube All-Reduce
|
||||||
|
|
||||||
|
- [ADR-0032](./ADR-0032-algo-intercube-allreduce.md) — 큐브 간 All-Reduce — pe0 큐브-메시 리듀스 + 다중-SIP 교환
|
||||||
|
|
||||||
|
### Evaluation Harnesses
|
||||||
|
|
||||||
|
- [ADR-0043](./ADR-0043-eval-allreduce-harness.md) — Allreduce 평가 하니스 — `tests/sccl/`
|
||||||
|
- [ADR-0044](./ADR-0044-eval-gemm-harness.md) — GEMM 평가 하니스 — `scripts/gemm_sweep.py` + `tests/gemm/`
|
||||||
|
- [ADR-0054](./ADR-0054-eval-milestone-benches.md) — 마일스톤 평가 bench — 자기완결적 sweep + figure bench
|
||||||
|
|
||||||
|
### Bench Module Contract
|
||||||
|
|
||||||
|
- [ADR-0045](./ADR-0045-prog-bench-module-contract.md) — Bench Module Contract — registration, dispatch, and authoring
|
||||||
|
|
||||||
|
### Kernel-side tl.* API (TLContext)
|
||||||
|
|
||||||
|
- [ADR-0046](./ADR-0046-prog-tl-context-contract.md) — TLContext — Kernel-side `tl.*` API Contract
|
||||||
|
|
||||||
|
### Memory Allocator Algorithms
|
||||||
|
|
||||||
|
- [ADR-0048](./ADR-0048-mem-allocator-algorithms.md) — Memory Allocator Algorithms — VirtualAllocator + PEMemAllocator
|
||||||
|
|
||||||
|
### Probe Subcommand
|
||||||
|
|
||||||
|
- [ADR-0049](./ADR-0049-ver-probe-subcommand.md) — `kernbench probe` Subcommand — Traffic-Pattern Verification Harness
|
||||||
|
|
||||||
|
### Sim-engine Op Log and Memory Store Schemas
|
||||||
|
|
||||||
|
- [ADR-0052](./ADR-0052-dev-oplog-memory-store-schemas.md) — OpLog + MemoryStore Schemas — sim_engine internals
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
# ADR-0061: `tl.broadcast` / `tl.repeat` — data-faithful GQA head reuse
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Proposed
|
||||||
|
|
||||||
|
> Supporting ADR for **ADR-0060** (AHBM GQA Fused Attention).
|
||||||
|
>
|
||||||
|
> **⚠ Re-evaluated during design iteration — this is now OPTIONAL, not a
|
||||||
|
> GQA blocker.** The original framing (below) held that GQA `h_q > h_kv`
|
||||||
|
> is blocked by the MemoryStore byte-conservation check on a broadcast
|
||||||
|
> view. That is true *only of the baseline's head-packing hack*
|
||||||
|
> (`_view(K, (h_q·d, S_kv))`). The correct GQA design (ADR-0060 §5.2)
|
||||||
|
> processes **one KV head at a time** and folds the `G` group rows into
|
||||||
|
> the matmul **M** dimension — using only byte-conserving reshapes — so it
|
||||||
|
> needs **no broadcast op at all**, and `np.matmul` already broadcasts a
|
||||||
|
> shared operand in Phase-2 data mode. This ADR therefore drops to a
|
||||||
|
> *convenience* primitive: it cleans up additive-mask construction across
|
||||||
|
> the `G·T_q` rows and serves general kernels. **Lowest priority of the
|
||||||
|
> three supporting ADRs.** The rest of this document is retained as the
|
||||||
|
> design for that convenience op.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
### The GQA reuse lever
|
||||||
|
|
||||||
|
Grouped-Query Attention amortises K/V loads: `G = H_q / H_kv` query heads
|
||||||
|
share one KV head. For Llama3-70B `G = 8`. In a kernel this means a KV
|
||||||
|
tile of shape `[S_kv, d]` (for one KV head) must be matmul'd against the
|
||||||
|
`G` query rows of its group. Loading K/V **once** and reusing it across
|
||||||
|
the group is the decode-time efficiency win (ADR-0060 §5.2).
|
||||||
|
|
||||||
|
### Why it does not work today
|
||||||
|
|
||||||
|
Pre-ADR-0060 mesh kernels reshaped tensors with a metadata-only `_view`
|
||||||
|
helper that rewrote `shape` but kept the original `nbytes`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _view(handle, new_shape):
|
||||||
|
return TensorHandle(..., shape=new_shape, nbytes=handle.nbytes, ...)
|
||||||
|
# ^^^ new shape ^^^ OLD nbytes (unchanged)
|
||||||
|
```
|
||||||
|
|
||||||
|
This rewrites `shape` but keeps the original `nbytes`. It is purely
|
||||||
|
symbolic. It survives **Phase 1 (timing)** because Phase 1 never reads
|
||||||
|
tensor bytes. It fails **Phase 2 (`enable_data=True`)** because the
|
||||||
|
data executor materialises every op through `MemoryStore.read`, which
|
||||||
|
enforces byte conservation
|
||||||
|
(`src/kernbench/sim_engine/memory_store.py:67-73`):
|
||||||
|
|
||||||
|
```python
|
||||||
|
if shape is not None and arr.shape != shape:
|
||||||
|
if arr.nbytes != np.prod(shape) * arr.dtype.itemsize:
|
||||||
|
raise ValueError(f"Shape mismatch: stored {arr.shape} ({arr.nbytes}B) "
|
||||||
|
f"vs requested {shape} ({...}B)")
|
||||||
|
arr = arr.reshape(shape)
|
||||||
|
```
|
||||||
|
|
||||||
|
A reshape conserves bytes; a **broadcast does not** (`[S_kv, 1, d]` →
|
||||||
|
`[S_kv, G, d]` multiplies the element count by `G`). So the moment a
|
||||||
|
kernel tries to view a `h_kv`-headed K as if it had `h_q` heads, the
|
||||||
|
stored array has `1/G` of the requested bytes and `read` raises.
|
||||||
|
|
||||||
|
The pre-ADR-0060 baseline documented this as a deliberate limitation:
|
||||||
|
|
||||||
|
> "v1 uses `h_q == h_kv == 1` to avoid … GQA broadcast view (which is
|
||||||
|
> symbolic and does not survive MemoryStore's nbytes check under
|
||||||
|
> simulator data execution). Real GQA (`h_q > h_kv`) is deferred."
|
||||||
|
|
||||||
|
There is **no** broadcast / repeat / expand primitive in the `tl` API
|
||||||
|
today (`src/kernbench/triton_emu/tl_context.py` exposes `trans`,
|
||||||
|
`zeros`, `full`, `arange` as metadata-only ops; none replicate data).
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Add a **data-faithful broadcast op** to the `tl` API and the data
|
||||||
|
executor. Unlike `_view`, it updates `nbytes` and, in Phase 2, actually
|
||||||
|
replicates the underlying ndarray. The latency it charges models the
|
||||||
|
on-chip register/SRAM fan-out, not an HBM re-load (the KV tile is read
|
||||||
|
from HBM once; broadcast is an on-chip replication).
|
||||||
|
|
||||||
|
### D1. `tl` surface
|
||||||
|
|
||||||
|
```python
|
||||||
|
def broadcast(self, x: TensorHandle, new_shape: tuple[int, ...]) -> TensorHandle:
|
||||||
|
"""Replicate x along size-1 axes to new_shape (numpy broadcast rules).
|
||||||
|
|
||||||
|
new_shape must be broadcast-compatible with x.shape: every axis is
|
||||||
|
either equal to x's, or x's is 1 (the axis being expanded). nbytes of
|
||||||
|
the result reflect new_shape (unlike the metadata-only _view).
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
`tl.repeat(x, axis, n)` is offered as a thin convenience wrapper over
|
||||||
|
`broadcast` for the common "insert a group axis and expand it to G"
|
||||||
|
pattern, but `broadcast` is the primitive.
|
||||||
|
|
||||||
|
### D2. Command + op_log
|
||||||
|
|
||||||
|
Emit a new `BroadcastCmd` (sibling of `MathCmd`):
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class BroadcastCmd:
|
||||||
|
src: TensorHandle
|
||||||
|
out: TensorHandle # out.nbytes reflects new_shape
|
||||||
|
new_shape: tuple[int, ...]
|
||||||
|
data_op: bool = True
|
||||||
|
```
|
||||||
|
|
||||||
|
op_log entry: `op_kind="math"`, `op_name="broadcast"` (it runs on the
|
||||||
|
MATH/vector engine, like the other element-wise ops). The output handle
|
||||||
|
is allocated from PE scratch via the existing `_make_compute_out`
|
||||||
|
(`tl_context.py:148-156`), so `out.nbytes` is computed from `new_shape`
|
||||||
|
and the byte-conservation check passes in Phase 2.
|
||||||
|
|
||||||
|
### D3. Phase 2 data execution
|
||||||
|
|
||||||
|
Add `_execute_broadcast` to the data executor
|
||||||
|
(`src/kernbench/sim_engine/data_executor.py`):
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _execute_broadcast(self, op):
|
||||||
|
p = op.params
|
||||||
|
src = self._resolve_read(p["src_space"], p["src_addr"],
|
||||||
|
p["src_shape"], p["dtype"], op.t_start)
|
||||||
|
result = np.broadcast_to(src, p["new_shape"]).copy() # .copy() → writable, owns bytes
|
||||||
|
self.store.write(p["dst_space"], p["dst_addr"], result)
|
||||||
|
```
|
||||||
|
|
||||||
|
`.copy()` is required: `np.broadcast_to` returns a zero-stride view, and
|
||||||
|
MemoryStore stores references — a later consumer's nbytes check must see
|
||||||
|
a real `prod(new_shape)`-element array.
|
||||||
|
|
||||||
|
### D4. Latency model
|
||||||
|
|
||||||
|
Broadcast latency = vector-engine cost of writing `prod(new_shape)`
|
||||||
|
elements (reuse `pe_math._compute_ns(num_elements)`,
|
||||||
|
`src/kernbench/components/builtin/pe_math.py:46-51`). This models the
|
||||||
|
on-chip replication into the register file / SRAM that real GQA hardware
|
||||||
|
performs. It is **not** an HBM transaction — no `dma_read` is logged,
|
||||||
|
consistent with "K/V tile loaded once" (ADR-0060 §5.2).
|
||||||
|
|
||||||
|
## Alternatives
|
||||||
|
|
||||||
|
### A1. Indexed per-head dot (no broadcast op)
|
||||||
|
|
||||||
|
Loop the `G` query heads, each doing its own `tl.dot(Q_g, Kᵀ)` against
|
||||||
|
the *same* (re-`ref`'d, not re-loaded) KV tile:
|
||||||
|
|
||||||
|
```python
|
||||||
|
for g in range(G):
|
||||||
|
s_g = tl.dot(Q[g], K_T) # G separate GEMMs, KV reused via tl.ref
|
||||||
|
```
|
||||||
|
|
||||||
|
- Pro: no new primitive; works in Phase 2 today.
|
||||||
|
- Con: `G×` more GEMM commands (op_log inflation), no batched-GEMM
|
||||||
|
modelling, and the per-call overhead is charged `G` times. It also
|
||||||
|
obscures the "single batched matmul over the group" the hardware does.
|
||||||
|
Rejected as the primary path; acceptable as a fallback if the batched
|
||||||
|
2D dot's shape handling proves awkward.
|
||||||
|
|
||||||
|
### A2. Keep `_view`, relax the nbytes check
|
||||||
|
|
||||||
|
Make `MemoryStore.read` tolerate broadcast (stride-0) shapes. Rejected:
|
||||||
|
it silently breaks byte conservation for *all* readers and would mask
|
||||||
|
genuine shape bugs elsewhere. The check is a correctness invariant
|
||||||
|
(ADR-0052), not an obstacle to route around.
|
||||||
|
|
||||||
|
### A3. Materialise the broadcast at deploy time
|
||||||
|
|
||||||
|
Store K/V already expanded to `h_q` heads in HBM. Rejected: defeats the
|
||||||
|
entire point of GQA (it `G×`'s KV-cache HBM footprint and bandwidth).
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
- Unblocks real GQA (`h_q > h_kv`) in Phase 2 — the headline blocker for
|
||||||
|
ADR-0060 and the deferred milestone work.
|
||||||
|
- A general, reusable primitive (any kernel needing numpy-style
|
||||||
|
broadcast benefits, not just attention).
|
||||||
|
- Byte conservation stays intact; no weakening of MemoryStore.
|
||||||
|
|
||||||
|
### Negative
|
||||||
|
- One new command type + one data-executor handler + one `tl` method.
|
||||||
|
- Phase 2 `.copy()` allocates the expanded array in the store; for very
|
||||||
|
large groups this is real memory. Bounded by tile size in the tiled
|
||||||
|
design (ADR-0060), so acceptable.
|
||||||
|
|
||||||
|
## Test Requirements
|
||||||
|
|
||||||
|
1. **Phase 1 timing**: `tl.broadcast([S,1,d] → [S,G,d])` logs one
|
||||||
|
`math/broadcast` op with `num_elements == S*G*d`; no `dma_read`.
|
||||||
|
2. **Phase 2 data**: broadcasting a known array yields the numpy
|
||||||
|
`broadcast_to(...).copy()` result; a downstream `tl.dot` consuming it
|
||||||
|
passes the MemoryStore nbytes check (the exact case that fails today).
|
||||||
|
3. **GQA end-to-end**: re-run a reduced `milestone-gqa-headline` panel
|
||||||
|
with `h_q=8, h_kv=1` under `enable_data=True`; it must complete (today
|
||||||
|
it raises `ValueError: Shape mismatch`).
|
||||||
|
4. **Broadcast-incompatible shape** (e.g. axis size 3 → 8) raises a
|
||||||
|
clear error at emit time, not deep in Phase 2.
|
||||||
@@ -0,0 +1,355 @@
|
|||||||
|
# Detailed Design Document — AHBM GQA Fused Attention (ADR-0060)
|
||||||
|
|
||||||
|
**Status:** Draft (companion to ADR-0060, Proposed)
|
||||||
|
**Scope:** implementation-ready plan for the **two** GQA FlashAttention
|
||||||
|
kernels (decode=reduce, prefill=ring) on AHBM in kernbench.
|
||||||
|
**Audience:** reviewer resuming with *"GQA 검토 시작하자"*. Read **ADR-0060
|
||||||
|
first** — it is now the authoritative design record (TL;DR has full
|
||||||
|
pseudocode for both kernels). This DDD is the *how to build it*: file plan,
|
||||||
|
phase plan, helper signatures, tests. It does **not** re-derive the design;
|
||||||
|
it points to ADR-0060 sections.
|
||||||
|
|
||||||
|
> **Alignment note (this revision).** ADR-0060 moved to a **composite
|
||||||
|
> hybrid + hierarchical CUBE-Group SP** design with **two kernels**. This
|
||||||
|
> DDD is rewritten to match. The earlier DDD (greenlet-primitive,
|
||||||
|
> `tl.load_async`, single unified kernel) is superseded.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Goal and success criteria
|
||||||
|
|
||||||
|
**Goal:** two *efficient* GQA fused-attention kernels on AHBM in kernbench —
|
||||||
|
**decode+SP** (head-replicated, KV static shard, 2-level reduce) and
|
||||||
|
**prefill+SP** (1 Q head per CUBE, Ring KV) — at realistic scale, in both
|
||||||
|
timing (`enable_data=False`) and data (`enable_data=True`) modes.
|
||||||
|
|
||||||
|
**Success criteria:**
|
||||||
|
1. Correct: kernel output matches a numpy FlashAttention reference within fp
|
||||||
|
tolerance, with **real GQA** (`H_q=64, H_kv=8, G=8`).
|
||||||
|
2. Efficient — levers observable in op_log (ADR-0060 §11):
|
||||||
|
GQA K/V-load amortisation; decode 2-level reduce (`⌈log₂P⌉` + center-mesh
|
||||||
|
over `C`, not `C·P−1`); prefill ring (no reduce, KV rotates); load/compute
|
||||||
|
overlap (lazy `tl.load` + composite streaming); composite GEMM offload.
|
||||||
|
3. Scales: context length not capped by the 1 MiB scratch bump allocator.
|
||||||
|
4. Deterministic and SPEC-compliant (all latency from modelled events).
|
||||||
|
|
||||||
|
**Non-goals:** RoPE / QKV projection / KV-cache writes (upstream
|
||||||
|
`qkv_rope`); output projection (downstream); cross-**SIP** head split
|
||||||
|
(a head stays in one SIP, ADR-0060 §0); cycle-accurate microarchitecture
|
||||||
|
(SPEC §5); a bespoke "flash-composite" command kind (ADR-0060 §8 item 4).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Where the design sits in the current code
|
||||||
|
|
||||||
|
```
|
||||||
|
runtime_api (host)
|
||||||
|
RuntimeContext.zeros/empty/launch context.py (tensor deploy, DPPolicy(+cube_start), launch)
|
||||||
|
│ KernelLaunchMsg
|
||||||
|
▼
|
||||||
|
sim_engine
|
||||||
|
GraphEngine(enable_data=…) engine.py
|
||||||
|
DataExecutor (Phase 2) data_executor.py ← _execute_broadcast (ADR-0061, optional)
|
||||||
|
MemoryStore memory_store.py
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
components / PE pipeline (per PE)
|
||||||
|
PE_CPU → PE_SCHEDULER → PE_{DMA,GEMM,MATH,IPCQ,TCM}
|
||||||
|
greenlet kernel ↔ SimPy kernel_runner.py, pe_cpu.py
|
||||||
|
CompositeCmd → tile plan → engines pe_scheduler.py (ADR-0014 D6)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
tl programming model tl_context.py
|
||||||
|
load (→ lazy, ADR-0062) / store / composite / dot / max/sum/exp/maximum
|
||||||
|
send/recv/recv_async / scratch_scope (ADR-0063) / broadcast (ADR-0061, opt)
|
||||||
|
```
|
||||||
|
|
||||||
|
Both kernels are **bench-side Python** (`src/kernbench/benches/`) run as
|
||||||
|
greenlet kernels emitting `tl.*` ops; the **GEMMs are `tl.composite`**
|
||||||
|
(scheduler-managed tiling + K/V DMA streaming, existing `CompositeCmd` — no
|
||||||
|
new command kind), the softmax merge + reduction/ring stay kernel-level
|
||||||
|
(ADR-0060 §1). The remote work already added building blocks:
|
||||||
|
`DPPolicy.cube_start`, `_attention_mesh_mlo_2d.py` (2D cube reduce —
|
||||||
|
currently AllReduce, to become reduce-to-root), `topologies/llama70b_4sip.yaml`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. File plan
|
||||||
|
|
||||||
|
### 3.1 New / modified production files
|
||||||
|
|
||||||
|
| File | Change | ADR |
|
||||||
|
|---|---|---|
|
||||||
|
| `src/kernbench/triton_emu/tl_context.py` | `tl.load` → **lazy** (non-blocking + auto-wait on first use); `scratch_scope` ctx-mgr; `broadcast` (optional) | 0062/0063/0061 |
|
||||||
|
| `src/kernbench/components/builtin/pe_dma.py` | non-blocking read path: post DMA, resolve on a scheduled event (the `recv_async` pattern for loads) | 0062 |
|
||||||
|
| `src/kernbench/triton_emu/kernel_runner.py` | auto-wait: yield a handle's pending load event at first consume; lazy-load dispatch arm | 0062 |
|
||||||
|
| `src/kernbench/sim_engine/data_executor.py` | `_execute_broadcast` (optional) | 0061 |
|
||||||
|
| `src/kernbench/components/builtin/pe_cpu.py` + cost table | per-op-type CPU issue cost (replaces uniform `dispatch_cycles`) | **0064** (separate) |
|
||||||
|
|
||||||
|
No new GEMM command kind: the two GEMMs use the **existing** `CompositeCmd`.
|
||||||
|
|
||||||
|
### 3.2 Kernel + bench files
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
|---|---|
|
||||||
|
| `src/kernbench/benches/_gqa_decode.py` | decode+SP kernel — evolve `_attention_mesh_mlo`/`_mlo_2d` to composite-hybrid + **2-level reduce-to-root** (Level-2 PE tree + Level-1 center-mesh; currently 2D AllReduce) |
|
||||||
|
| `src/kernbench/benches/_gqa_prefill.py` | prefill+SP kernel — evolve `_attention_mesh_kv` to **head-parallel (1 Q head/CUBE) Ring KV** + composite-hybrid |
|
||||||
|
| `src/kernbench/benches/_gqa_helpers.py` | `head_of_group`, `valid_len_2level`, `init_running`, tree/center-mesh topology, `hierarchical_reduce_and_store`, ring helpers, causal `block_*` predicates + mask builders |
|
||||||
|
| extend `src/kernbench/benches/milestone_gqa_llama70b.py` | headline panels: real `G`, `C=G=8`, contiguous KV, 4-SIP |
|
||||||
|
| `topologies/llama70b_4sip.yaml` | **exists** (remote) — 4 SIP × 16-CUBE 4×4 × 8 PE |
|
||||||
|
|
||||||
|
### 3.3 New tests
|
||||||
|
|
||||||
|
| File | Covers |
|
||||||
|
|---|---|
|
||||||
|
| `tests/attention/test_gqa_decode.py` | decode reduce: GQA reuse (dma_read ⟂ `G`), 2-level reduce step count, root-only output, long context |
|
||||||
|
| `tests/attention/test_gqa_prefill.py` | prefill ring: no `(m,ℓ,O)` reduce, `C` KV rotations, causal step-skip, per-CUBE distributed output |
|
||||||
|
| `tests/test_tl_lazy_load.py` | ADR-0062 unit (overlap, auto-wait correctness, op_log parity) |
|
||||||
|
| `tests/test_tl_scratch_scope.py` | ADR-0063 unit |
|
||||||
|
| `tests/test_tl_broadcast.py` | ADR-0061 unit (optional) |
|
||||||
|
| `tests/test_dppolicy_cube_start.py` | **exists** (remote) — sub-mesh placement |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Data model and launch contract
|
||||||
|
|
||||||
|
### 4.1 Tensor placement — CUBE Group, 2-level SP, contiguous KV
|
||||||
|
|
||||||
|
A `CUBE Group` is `C` CUBEs in one SIP owning one KV head (ADR-0060 §0).
|
||||||
|
Placement via `DPPolicy(..., cube_start=...)`:
|
||||||
|
|
||||||
|
| Tensor | Decode+SP | Prefill+SP (`C=G`) |
|
||||||
|
|---|---|---|
|
||||||
|
| `Q` | `replicate` (all `G` heads, M-folded) over `C×P` ranks | **head-parallel**: CUBE `i` holds Q head `i` |
|
||||||
|
| `K`,`V` | sequence-sharded **two axes** (`cube` row_wise over `C`, `pe` row_wise over `P`) | sequence-sharded over `C` CUBEs (the ring blocks) |
|
||||||
|
| `O` | reduced to CUBE-Group root (all `G` heads) | per-CUBE (one head each), distributed |
|
||||||
|
|
||||||
|
**KV cache is *contiguous* `C×P` position blocks** (rank `r` = `[r·B,(r+1)·B)`),
|
||||||
|
**shared by both kernels** — prefill writes it (via `qkv_rope`), decode reads
|
||||||
|
+ extends it, no reshard. Contiguous is required for prefill causal skip
|
||||||
|
(ADR-0060 §2.1). `cube_start` offsets the group's CUBE sub-mesh within the
|
||||||
|
4×4 SIP mesh.
|
||||||
|
|
||||||
|
**Driver SP-enable threshold (fallback).** Contiguous sharding under-utilizes
|
||||||
|
ranks for short/early decode (only frontier ranks hold data). So the driver
|
||||||
|
**enables SP only past a context-length threshold** where KV-sweep time
|
||||||
|
dominates reduction + placement imbalance; below it, fall back to a smaller
|
||||||
|
`C` (or `C=P=1`, single rank, no reduce). The threshold is a sweep item
|
||||||
|
(P7 / §9), not a fixed constant.
|
||||||
|
|
||||||
|
`--device` enumerates **SIPs**; one SIP-device runs its 2 CUBE Groups
|
||||||
|
(2 KV heads); the head is picked by CUBE coordinate (`head_of_group`), not an
|
||||||
|
in-kernel loop (ADR-0060 §0).
|
||||||
|
|
||||||
|
### 4.2 Launch signatures
|
||||||
|
|
||||||
|
```python
|
||||||
|
# decode+SP
|
||||||
|
ctx.launch(f"{panel}_gqa_decode", gqa_decode_sp,
|
||||||
|
q, k, v, o, counter, start_pe, start_cube, C, P, softmax_scale)
|
||||||
|
# prefill+SP
|
||||||
|
ctx.launch(f"{panel}_gqa_prefill", gqa_prefill_sp,
|
||||||
|
q, k, v, o, T_q, S_kv_local, d, C, softmax_scale, q_block, cube_start)
|
||||||
|
```
|
||||||
|
|
||||||
|
Full per-kernel I/O contract: ADR-0060 §0.5.3/§0.5.4 (note the **output head
|
||||||
|
distribution differs** — decode root-gathered, prefill distributed).
|
||||||
|
|
||||||
|
### 4.3 GQA reuse — fold `G` into the matmul M dim (decode)
|
||||||
|
|
||||||
|
Decode replicates Q and **M-folds** the `G` query heads into the GEMM row
|
||||||
|
dim so one `Q·Kᵀ` serves all `G` heads sharing one `K` (ADR-0060 §0, §5.2):
|
||||||
|
`Q` group `[G, T_q, d]` → `[G·T_q, d]` (byte-conserving `_view`); the
|
||||||
|
composite carries `m = G·T_q` (timing counts all `G` rows). Prefill does
|
||||||
|
**not** M-fold (1 head/CUBE). Caveat: `tl.trans` is reshape-not-transpose →
|
||||||
|
store `K` pre-transposed `[d, S/(C·P)]` (ADR-0060 §3, §B).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Kernel design
|
||||||
|
|
||||||
|
**The full pseudocode for both kernels (and the 3 decode CPU-pipelining
|
||||||
|
variants) lives in ADR-0060 TL;DR + §5.** Implementation notes only here.
|
||||||
|
|
||||||
|
### 5.1 Decode (reduce) — `_gqa_decode.py`
|
||||||
|
- Inner tile = §3 composite-hybrid: `Sj = composite(q_g, Kⱼ)·scale` → softmax
|
||||||
|
MATH → `Oj = composite(P, Vⱼ)` → running merge (ADR-0060 §3).
|
||||||
|
- **Ship opt3** (software pipelining: issue next tile's `Q·Kᵀ` before this
|
||||||
|
tile's softmax; `Sj` in a persistent double buffer) — removes the
|
||||||
|
GEMM-engine bubble, no new command kind (ADR-0060 §5.6). opt1 is the naïve
|
||||||
|
baseline; opt2 (`ex_composite`) waits on ADR-0064.
|
||||||
|
- Combine = **`hierarchical_reduce_and_store`** (ADR-0060 §4): Level-2 PE
|
||||||
|
reduce-to-root tree (intra-CUBE, the cube's KV slice further split across
|
||||||
|
its `P` PEs — decision (a)) → Level-1 center-root CUBE-mesh reduce
|
||||||
|
(intra-CUBE-Group). Data-driven, level-pipelined, root-only.
|
||||||
|
|
||||||
|
### 5.2 Prefill (ring) — `_gqa_prefill.py`
|
||||||
|
- Head-parallel: CUBE `i` owns Q head `i` + KV slice `i`. **No reduce** (each
|
||||||
|
CUBE outputs a distinct head). Ring rotates KV blocks; GQA reuse via the
|
||||||
|
rotation (ADR-0060 §5.5).
|
||||||
|
- **K/V are `tl.load`'d TCM handles** (not `tl.ref`) so the ring can
|
||||||
|
`tl.send`/`recv_async` them; `K` pre-transposed `[d, S/C]`. Receive buffers
|
||||||
|
**ping-pong** (persistent arena) — recv into a *separate* buffer while
|
||||||
|
computing/sending the current one (do not clobber).
|
||||||
|
- Causal step-skip + boundary mask; `recv_async` overlaps next block's
|
||||||
|
receive with current compute.
|
||||||
|
- Within a CUBE: tile the Q head's `T_q` rows across `P` PEs (disjoint output
|
||||||
|
rows → no intra-CUBE reduce); fall back to KV-block split only if `T_q < P`
|
||||||
|
(ADR-0060 §B decode-split item 3).
|
||||||
|
|
||||||
|
### 5.3 Shared helpers — `_gqa_helpers.py`
|
||||||
|
`head_of_group(cube_id) = cube_id // C`; `init_running(...)` → persistent
|
||||||
|
`(m=-inf, ℓ=0, O=0)`; `valid_len_2level(...)` (contiguous block extent);
|
||||||
|
`hierarchical_reduce_and_store(...)`; tree/center-mesh child/parent dirs;
|
||||||
|
`block_all_future`/`block_partial` + `causal_mask`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Supporting primitives — integration points
|
||||||
|
|
||||||
|
### 6.1 ADR-0062 lazy `tl.load` (efficiency — overlap)
|
||||||
|
- `tl.load` issues `DmaReadCmd` non-blocking, returns a handle with a pending
|
||||||
|
event; the runtime **auto-inserts the wait at first consume** (generalises
|
||||||
|
the `recv_async`/wait pattern, `kernel_runner.py:248-285`).
|
||||||
|
- `pe_dma.py`: non-blocking read; DMA occupies the (capacity-1) read channel,
|
||||||
|
overlaps compute on PE_GEMM/PE_MATH.
|
||||||
|
- **Global** semantics change → existing goldens regenerate (ADR-0062 D3).
|
||||||
|
- op_log unchanged (`memory/dma_read`) ⇒ `dma_read_count` stable.
|
||||||
|
|
||||||
|
### 6.2 ADR-0063 `tl.scratch_scope` (scale)
|
||||||
|
- ctx-mgr saving/restoring `_scratch_cursor`. Persistent arena for `(m,ℓ,O)`
|
||||||
|
(+ decode opt3 `Sj` double buffer, + prefill ring ping-pong buffers, +
|
||||||
|
in-flight lazy-load buffers) lives **outside** the scope. Removes `S=16`.
|
||||||
|
|
||||||
|
### 6.3 ADR-0061 `tl.broadcast` (optional)
|
||||||
|
- Not on the GQA critical path (M-fold gives reuse). Convenience for additive
|
||||||
|
mask construction. Lowest priority.
|
||||||
|
|
||||||
|
### 6.4 ADR-0064 per-op-type CPU issue cost (separate ADR)
|
||||||
|
- Replaces uniform `dispatch_cycles=0`. Makes the hybrid's CPU-saturation
|
||||||
|
win (and the decode opt2 `ex_composite` fewer-issues win) **measurable**.
|
||||||
|
Land before claiming hybrid latency wins (ADR-0060 §9).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Phased implementation plan (test-first per CLAUDE.md)
|
||||||
|
|
||||||
|
Each phase is an independent Phase-1→Phase-2 cycle; each keeps the baseline
|
||||||
|
green.
|
||||||
|
|
||||||
|
| Phase | Deliverable | Gate |
|
||||||
|
|---|---|---|
|
||||||
|
| **P1** | Real GQA, decode, composite-hybrid M-fold; one-shot, no tiling. **No new primitive** (existing `CompositeCmd`). | data-mode completes `h_q=G·h_kv`; K/V `dma_read_count` ⟂ `G`; composite tile plan `m=G·T_q` |
|
||||||
|
| **P2** | Decode **2-level reduce-to-root** (Level-2 PE tree + Level-1 center-mesh), replacing the 2D AllReduce | reduce rounds = `⌈log₂P⌉`+center-mesh; **root-only** output; Level-1 on CUBE NOC, Level-2 on PE IPCQ |
|
||||||
|
| **P3** | **ADR-0063** `scratch_scope` + tiled sweep | long-`S` decode completes (today caps S=16); scratch O(1) |
|
||||||
|
| **P4** | **ADR-0062** lazy `tl.load` + composite K/V streaming | tiled-sweep latency < serial `Σ(load+compute)`; goldens regenerated |
|
||||||
|
| **P5** | Decode **opt3** software pipelining | GEMM engine non-idle across tiles (structural / op_log) |
|
||||||
|
| **P6** | Prefill **head-parallel Ring KV** + causal step-skip | `C` KV rotations, no `(m,ℓ,O)` reduce, per-CUBE distributed `O`, causal skip ≈½ |
|
||||||
|
| **P7** | Contiguous shared KV layout + 4-SIP topology + headline milestone panels (real `G`, `C=G`) | shared layout (no reshard); headline sweep.json rows |
|
||||||
|
| **P8** *(opt)* | **ADR-0064** cost model + decode **opt2** `ex_composite`; **ADR-0061** broadcast | cost-model goldens; opt2 fewer-issues measurable |
|
||||||
|
|
||||||
|
P1 delivers "GQA runs" on the composite path. P3 is the key *scale* feature;
|
||||||
|
P4 the overlap lever; P2/P6 the SP algorithms.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Verification plan (concrete)
|
||||||
|
|
||||||
|
Mirrors ADR-0060 §11; grounded in SPEC R2/R5, ADR-0023/0025, ADR-0046, ADR-0054.
|
||||||
|
|
||||||
|
**Runs + numeric (`enable_data=True`):**
|
||||||
|
- decode & prefill complete for `(G∈{1,8}, T_q∈{1,16}, S, C∈{1,8}, P∈{1,8})`
|
||||||
|
without byte-conservation error (the `G=8` cases baseline cannot express).
|
||||||
|
- *Numeric parity (secondary):* `O ≈ numpy FlashAttention` for
|
||||||
|
symmetric/identity inputs (reshape-as-transpose exact); full asymmetric
|
||||||
|
parity gated on a real `tl.transpose` (deferred).
|
||||||
|
|
||||||
|
**Levers (op_log):**
|
||||||
|
- GQA amortisation: K/V `dma_read_count` ⟂ `G`; compute scales with `G`.
|
||||||
|
- Decode reduce: rounds = `⌈log₂P⌉` + center-mesh (not `C·P−1`); result at
|
||||||
|
one rank; Level-1=CUBE NOC, Level-2=PE IPCQ.
|
||||||
|
- Prefill ring: `C` KV rotations, **no** reduce, each CUBE writes a distinct
|
||||||
|
head.
|
||||||
|
- Causal skip: GEMM/step count = lower-triangular.
|
||||||
|
- Overlap: tiled-sweep latency < `Σ(load+compute)`.
|
||||||
|
- Scratch: peak cursor bounded by one tile.
|
||||||
|
|
||||||
|
**Invariants:** determinism (identical op_log + latency); every routed
|
||||||
|
request latency > 0 (SPEC §0.1).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Performance model (expected)
|
||||||
|
|
||||||
|
Per-rank decode (KV-load-bound), composite-streamed + overlapped:
|
||||||
|
```
|
||||||
|
t_decode_rank ≈ max( DMA(K+V over S/(C·P) tiles), compute(QKᵀ+PV, ×G amortised) )
|
||||||
|
+ t_reduce( ⌈log₂P⌉ intra-CUBE + center-mesh over C inter-CUBE )
|
||||||
|
```
|
||||||
|
Prefill (ring): `t ≈ C · max( recv(KV block), compute(QKᵀ+PV over block) )`
|
||||||
|
with causal step-skip removing ≈half the steps; no reduce term.
|
||||||
|
|
||||||
|
Levers: GQA reuse → KV bytes `H_kv·S·d` not `H_q·S·d`; overlap → `max` not
|
||||||
|
sum; decode reduce `⌈log₂P⌉+center-mesh` vs baseline `C·P−1`; prefill ring
|
||||||
|
trades the reduce for KV rotation (good when `O` is big). The CPU-saturation
|
||||||
|
win (composite offload) is *measurable* only with ADR-0064.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Open items (status)
|
||||||
|
|
||||||
|
Most original "Open Decisions" are now **resolved** in ADR-0060; the live
|
||||||
|
review items live in **ADR-0060 §B** (three groups: hybrid pivot,
|
||||||
|
hierarchical CUBE-Group SP, decode/prefill split). Key resolved choices:
|
||||||
|
|
||||||
|
| Was open | Now |
|
||||||
|
|---|---|
|
||||||
|
| greenlet vs composite | **composite hybrid** (GEMMs→composite, merge→kernel) |
|
||||||
|
| one kernel vs two | **two kernels** (decode reduce / prefill ring) |
|
||||||
|
| GQA matmul shape | **M-fold** (decode); head-parallel (prefill) |
|
||||||
|
| reduction topology | decode **2-level reduce-to-root**; prefill **no reduce (ring)** |
|
||||||
|
| `tl.load_async` | **lazy `tl.load`** (ADR-0062, redefined) |
|
||||||
|
| KV placement | **contiguous `C×P`**, shared prefill/decode (ADR-0060 §2.1) |
|
||||||
|
|
||||||
|
Still to confirm (ADR-0060 §B): DDD↔impl reconcile (`_attention_mesh_mlo_2d`
|
||||||
|
AllReduce → reduce-to-root); `head_of_group` sub-mesh partition; `C=G`
|
||||||
|
coupling; short-context KV balance; ADR-0064 calibration. Pre-existing:
|
||||||
|
ghost ADRs 0055–0059 (separate backfill); `bf16→f16` proxy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Risks
|
||||||
|
|
||||||
|
| Risk | Likelihood | Mitigation |
|
||||||
|
|---|---|---|
|
||||||
|
| Numeric parity blocked by trans-as-reshape | med | structural-first verify; pre-transpose K (ADR-0060 §B item 2) |
|
||||||
|
| 2-level reduce pairs not 1-hop on the mesh | med | verify SFR neighbour table; center-root sub-mesh partition (§B) |
|
||||||
|
| scratch_scope use-after-scope (Sj / ring / lazy-load buffers) | med | persistent-arena discipline; ping-pong buffers (§5.2) |
|
||||||
|
| prefill ring buffer clobbered while in flight | med | recv into the *other* ping-pong buffer (§5.2) |
|
||||||
|
| decode opt2 `acc` read before composites drain | med | `tl.wait()` after the loop (ADR-0060 TL;DR opt2) |
|
||||||
|
| impl drift (`_attention_mesh_mlo_2d` is AllReduce, Q-replicated) | med | reconcile to reduce-to-root; add prefill ring (§B) |
|
||||||
|
| CPU-saturation win invisible | exp | ADR-0064 cost model (P8) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Glossary & references
|
||||||
|
|
||||||
|
- **GQA / G** — `G = H_q/H_kv` query heads share one KV head. Llama3-70B: `G=8`.
|
||||||
|
- **CUBE Group** — `C` CUBEs in one SIP owning one KV head (ADR-0060 §0).
|
||||||
|
- **2-level SP** — Level-1 inter-CUBE (over `C`) × Level-2 intra-CUBE PE
|
||||||
|
(over `P`); ranks = `C·P`.
|
||||||
|
- **Composite hybrid** — GEMMs via `tl.composite` (scheduler), softmax merge
|
||||||
|
+ reduction in the kernel (ADR-0060 §1).
|
||||||
|
- **M-fold** — stack the `G` query heads into the GEMM M (row) dim so one
|
||||||
|
`Q·Kᵀ` does all heads sharing one `K`.
|
||||||
|
- **FlashAttention / FlashDecoding / Ring Attention** — ADR-0060 lineage.
|
||||||
|
|
||||||
|
Source anchors: `tl_context.py` (tl API), `pe_scheduler.py:104-143`
|
||||||
|
(composite tile plan), `pe_dma.py:45,89` (read channel, drain), `pe_cpu.py`
|
||||||
|
(dispatch_cycles), `lrab_hierarchical_allreduce.py` (center-root pattern),
|
||||||
|
`_attention_mesh_mlo_2d.py` / `_attention_mesh_kv.py` (impl to evolve),
|
||||||
|
`DPPolicy.cube_start`, `topologies/llama70b_4sip.yaml`.
|
||||||
|
|
||||||
|
ADRs: **0060** (this design), **0062** (lazy load), **0063** (scratch scope),
|
||||||
|
**0061** (broadcast, optional), **0064** (CPU issue cost model); related
|
||||||
|
accepted **0014/0017/0020/0023/0025/0046/0054**.
|
||||||
@@ -184,8 +184,15 @@ The empty-`ops` form is the legacy single-op path.
|
|||||||
- `DMA_READ`: `simpy.Resource(capacity=1)`.
|
- `DMA_READ`: `simpy.Resource(capacity=1)`.
|
||||||
- `DMA_WRITE`: `simpy.Resource(capacity=1)`.
|
- `DMA_WRITE`: `simpy.Resource(capacity=1)`.
|
||||||
- Both channels run concurrently (READ ∥ WRITE allowed).
|
- Both channels run concurrently (READ ∥ WRITE allowed).
|
||||||
- Within a channel, requests serialize (READ ∥ READ disallowed; same
|
- Within a channel, requests serialize **at the issue path** (READ ∥ READ
|
||||||
for WRITE).
|
issued out-of-order is disallowed; same for WRITE). The channel is
|
||||||
|
held only until the request is enqueued onto the next hop (router),
|
||||||
|
then released — it does **not** block until the HBM round-trip
|
||||||
|
completes. Multiple in-flight requests are therefore allowed, and
|
||||||
|
HBM-level serialization is the responsibility of the HBM controller's
|
||||||
|
per-PC `available_at` timestamps (ADR-0033 D1). This is what allows
|
||||||
|
back-to-back small-tile DMAs to amortize the per-request head latency
|
||||||
|
through the fabric.
|
||||||
- `vc_comm` is an orthogonal channel for IPCQ traffic defined in
|
- `vc_comm` is an orthogonal channel for IPCQ traffic defined in
|
||||||
ADR-0023 D8 — out of scope for this ADR.
|
ADR-0023 D8 — out of scope for this ADR.
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Accepted (Revision 5 — Phase 2 landed 2026-04-14, 523 passed + 1 strict xfail)
|
Accepted (Revision 6 — cube_start added 2026-06-04; Revision 5 landed 2026-04-14)
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
@@ -31,9 +31,11 @@ class DPPolicy:
|
|||||||
pe: Literal["replicate", "column_wise", "row_wise"] = "replicate"
|
pe: Literal["replicate", "column_wise", "row_wise"] = "replicate"
|
||||||
num_pes: int | None = None
|
num_pes: int | None = None
|
||||||
num_cubes: int | None = None
|
num_cubes: int | None = None
|
||||||
|
cube_start: int = 0 # Revision 6 — first cube in SIP; see D8
|
||||||
```
|
```
|
||||||
|
|
||||||
Removed fields: `sip`, `num_sips`.
|
Removed fields: `sip`, `num_sips`.
|
||||||
|
Added fields (Revision 6): `cube_start` — see D8.
|
||||||
|
|
||||||
### D2. `ShardSpec` — structural (sip, cube, pe) coordinates, `pe_index` fully removed
|
### D2. `ShardSpec` — structural (sip, cube, pe) coordinates, `pe_index` fully removed
|
||||||
|
|
||||||
@@ -120,7 +122,7 @@ def resolve_dp_policy(
|
|||||||
for ls in local_shards:
|
for ls in local_shards:
|
||||||
all_shards.append(ShardSpec(
|
all_shards.append(ShardSpec(
|
||||||
sip=target_sip, # from caller (current_device)
|
sip=target_sip, # from caller (current_device)
|
||||||
cube=cube_id, # local within SIP
|
cube=policy.cube_start + cube_id, # Rev 6: shifted by D8 cube_start; default 0 = local within SIP
|
||||||
pe=ls.local_pe, # local within cube (explicit name)
|
pe=ls.local_pe, # local within cube (explicit name)
|
||||||
offset_bytes=cube_offset + ls.offset_bytes,
|
offset_bytes=cube_offset + ls.offset_bytes,
|
||||||
nbytes=ls.nbytes,
|
nbytes=ls.nbytes,
|
||||||
@@ -239,6 +241,53 @@ property: code that expected a global flat could otherwise silently
|
|||||||
receive a SIP-local result and index incorrectly — that possibility is
|
receive a SIP-local result and index incorrectly — that possibility is
|
||||||
eliminated.
|
eliminated.
|
||||||
|
|
||||||
|
### D8. Add `cube_start: int = 0` for disjoint cube sub-meshes within a SIP
|
||||||
|
|
||||||
|
**Revision 6 addition (2026-06-04)**.
|
||||||
|
|
||||||
|
**Purpose**: address a disjoint cube sub-mesh within one SIP (e.g.
|
||||||
|
cubes 8..15 alongside cubes 0..7). Required by the GQA Llama-70B
|
||||||
|
8-KV-group headline target — two 2×4 KV-groups per SIP × 4 SIPs = 64
|
||||||
|
cubes — where the second KV-group on each SIP must land on cubes 8..15
|
||||||
|
instead of the default 0..7.
|
||||||
|
|
||||||
|
**Semantics**: `resolve_dp_policy` returns `ShardSpec` with
|
||||||
|
`cube = policy.cube_start + cube_id`, where `cube_id` iterates
|
||||||
|
`0..num_cubes-1` within the launch. Selected cubes lie in
|
||||||
|
`[cube_start, cube_start + num_cubes)` within the target SIP.
|
||||||
|
|
||||||
|
**Default**: `cube_start = 0` preserves every existing call site
|
||||||
|
bit-for-bit (`ShardSpec.cube ∈ [0, num_cubes)` as before). The CCL
|
||||||
|
milestone bench's full-SIP `DPPolicy(num_cubes=16)` continues to
|
||||||
|
produce cubes 0..15.
|
||||||
|
|
||||||
|
**Constraint**: intra-device invariant preserved.
|
||||||
|
`cube_start ∈ [0, cubes_per_sip)` and
|
||||||
|
`cube_start + num_cubes ≤ cubes_per_sip`. SIP boundary crossing remains
|
||||||
|
the job of `ahbm.set_device(rank)` (ADR-0024).
|
||||||
|
|
||||||
|
**Why scalar** (not 2D `cube_mesh_origin` or arbitrary `cube_ids` list):
|
||||||
|
consumer kernels (e.g. `_attention_mesh_mlo_2d`) assume row-major
|
||||||
|
contiguous cubes; scalar `cube_start` pairs naturally with `num_cubes`
|
||||||
|
(range = `[start, start + count)`); zero migration churn at default 0.
|
||||||
|
More general designs can be added on top later if a non-contiguous use
|
||||||
|
case appears.
|
||||||
|
|
||||||
|
**Backward compatibility**: additive change with default value. No
|
||||||
|
existing call site is forced to change. The "breaking change" stance
|
||||||
|
in D7 applies only to the original sip/num_sips removal — `cube_start`
|
||||||
|
does NOT break existing callers.
|
||||||
|
|
||||||
|
**Kernel-side note**: kernbench's `tl.program_id(axis=1)` returns the
|
||||||
|
physical cube id (ADR-0022), not a launch-local rank. Kernels that
|
||||||
|
derive ring positions from `program_id(axis=1)` must subtract
|
||||||
|
`cube_start` to recover launch-local rank when `cube_start > 0` —
|
||||||
|
otherwise they compute out-of-bounds sub-mesh positions and deadlock.
|
||||||
|
See `_attention_mesh_mlo_2d.py` for the reference pattern. A future
|
||||||
|
revision of ADR-0022 could move `program_id(axis=1)` to launch-local
|
||||||
|
rank semantics; that change would let kernels drop the explicit
|
||||||
|
subtraction.
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
- **ADR-0024** (launcher): `set_device(rank)` and current-device scoping
|
- **ADR-0024** (launcher): `set_device(rank)` and current-device scoping
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ Accepted
|
|||||||
Documents the `tests/sccl/` evaluation harness; verified against the
|
Documents the `tests/sccl/` evaluation harness; verified against the
|
||||||
implementation (constants, file set, and sweep dimensions cross-checked).
|
implementation (constants, file set, and sweep dimensions cross-checked).
|
||||||
|
|
||||||
|
**Amended by ADR-0054**: the driver core, sweeps, and renderers moved into
|
||||||
|
the `milestone-1h-ccl` bench (single home); `tests/sccl/_allreduce_helpers.py`
|
||||||
|
now re-exports from it (keeping the pytest-only param builders +
|
||||||
|
`_run_distributed` wrapper local). The figure tests are unchanged.
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
ADR-0032 defines the intercube all-reduce *algorithm*; ADR-0023/0024/0027
|
ADR-0032 defines the intercube all-reduce *algorithm*; ADR-0023/0024/0027
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ implementation (constants, tile sizes, figure set, and the script↔test
|
|||||||
split cross-checked). The D5/D6 caveats are recorded limitations, not
|
split cross-checked). The D5/D6 caveats are recorded limitations, not
|
||||||
inaccuracies.
|
inaccuracies.
|
||||||
|
|
||||||
|
**Amended by ADR-0054**: the sweep + renderers moved into the
|
||||||
|
`milestone-1h-gemm` bench (single home); `scripts/gemm_sweep.py` and
|
||||||
|
`tests/gemm/` now re-export from it. D1/D2's "data generation stays a manual
|
||||||
|
script / heavy work is opt-in" is superseded by the eval-bench pattern (one
|
||||||
|
bench regenerates everything; `MILESTONE_FAST=1` reuses the committed JSON).
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
ADR-0014 (PE pipeline) and ADR-0042 (tile-plan generators) define the GEMM
|
ADR-0014 (PE pipeline) and ADR-0042 (tile-plan generators) define the GEMM
|
||||||
|
|||||||
@@ -0,0 +1,295 @@
|
|||||||
|
# ADR-0045: Bench Module Contract — registration, dispatch, and authoring
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted (2026-05-21).
|
||||||
|
|
||||||
|
Unifies the `src/kernbench/benches/` registration mechanism (@bench), the
|
||||||
|
CLI dispatch path (`kernbench run/list`), and the contract a new bench
|
||||||
|
module must follow. ADR-0010 (CLI surface) specifies the `kernbench
|
||||||
|
list/run` interface, but **how benches are registered and what signature
|
||||||
|
they must follow** had no ADR-level coverage.
|
||||||
|
|
||||||
|
**Extended by ADR-0054**: D5's single-config rule gains a third pattern —
|
||||||
|
the *eval bench* (e.g. `milestone-1h-*`) drives many configs, builds its
|
||||||
|
own per-config engines, and submits a sentinel tensor to satisfy D4.
|
||||||
|
|
||||||
|
## First action
|
||||||
|
|
||||||
|
When `kernbench.benches` is imported, `__init__.py` immediately calls
|
||||||
|
`_eager_import_and_audit(__path__, __name__)`. Its first action is to
|
||||||
|
enumerate every sibling module in the package directory via
|
||||||
|
`pkgutil.iter_modules(__path__)` and **eagerly import** each one via
|
||||||
|
`importlib.import_module(...)` — except modules matching either:
|
||||||
|
|
||||||
|
- name `registry` (the infrastructure module itself), or
|
||||||
|
- name starting with `_` (helper modules).
|
||||||
|
|
||||||
|
At import time, each `@bench(name=..., description=...)` decorator inside
|
||||||
|
the imported module runs, appending `(name, description, fn)` to
|
||||||
|
`_PENDING` and adding `fn.__module__` to `_REGISTERED_MODULES`.
|
||||||
|
|
||||||
|
Once imports finish, `_audit_modules(imported, _REGISTERED_MODULES)`
|
||||||
|
runs; if any imported module did not invoke `@bench` at least once, it
|
||||||
|
raises `RuntimeError("Bench module(s) missing @bench decorator: ...")`
|
||||||
|
immediately. At this point indices are still unassigned — the first call
|
||||||
|
to `list_all()` / `resolve(...)` triggers `_finalize()`, which sorts
|
||||||
|
`_PENDING` alphabetically by name and assigns 1-based indices.
|
||||||
|
|
||||||
|
In short, **the bench infrastructure's first act is "eagerly import
|
||||||
|
every non-helper module in the package and audit that each one
|
||||||
|
registered at least one bench"**.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
`src/kernbench/benches/` currently holds 8 bench modules (`ccl_allreduce`,
|
||||||
|
`gemm_single_pe`, `gpt3_qkv`, `ipcq_allreduce`, `matmul_composite`,
|
||||||
|
`qkv_gemm`, `qkv_gemm_multi_pe`, `va_offset_verify`). Every bench follows
|
||||||
|
the same unified flow:
|
||||||
|
|
||||||
|
```
|
||||||
|
kernbench run --topology <T> --bench <N>
|
||||||
|
↓
|
||||||
|
cli/main.py::cmd_run
|
||||||
|
↓ resolve_topology(T) + resolve(N) + resolve_device(device_arg)
|
||||||
|
↓
|
||||||
|
runtime_api/bench_runner.py::run_bench(topology, bench_fn, device, engine_factory)
|
||||||
|
↓ engine_factory(topology, device) → GraphEngine
|
||||||
|
↓ RuntimeContext(engine, target_device, correlation_id, spec)
|
||||||
|
↓
|
||||||
|
bench_fn(ctx) ← invokes the bench's run(torch)
|
||||||
|
↓ ctx.empty/zeros/from_numpy/launch/distributed.* etc. submit work
|
||||||
|
↓
|
||||||
|
ctx.wait_all() ← drains any outstanding handles
|
||||||
|
↓
|
||||||
|
BenchResult(completion, correlation_id, trace, traces, engine)
|
||||||
|
```
|
||||||
|
|
||||||
|
ADR-0010 covers only the CLI surface (`run/list/probe/web`); ADR-0007
|
||||||
|
covers only the runtime API ↔ sim_engine boundary. The question "what
|
||||||
|
shape must a new bench file take?" had to be answered by grepping the
|
||||||
|
codebase. As a result:
|
||||||
|
|
||||||
|
- The @bench decorator contract (kebab-case name, non-empty description)
|
||||||
|
lived only in the source.
|
||||||
|
- The bench function signature (`def run(torch)`) was a de-facto
|
||||||
|
convention enforced by the CLI dispatcher calling `spec.run`.
|
||||||
|
- New bench authors learned the "helpers must use `_` prefix" rule only
|
||||||
|
after seeing the audit's RuntimeError.
|
||||||
|
- The single-device convention (CLAUDE.md Part 2 CLI Semantics) and its
|
||||||
|
interaction with multi-SIP CCL benches was ambiguous for bench
|
||||||
|
authors.
|
||||||
|
|
||||||
|
This ADR consolidates all of it in one place.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### D1. @bench decorator contract
|
||||||
|
|
||||||
|
```python
|
||||||
|
from kernbench.benches.registry import bench
|
||||||
|
|
||||||
|
@bench(name="my-bench", description="Short, complete-sentence description.")
|
||||||
|
def run(torch):
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
- `name`: kebab-case string matching `^[a-z][a-z0-9]*(-[a-z0-9]+)*$`.
|
||||||
|
Lowercase letters, digits, and dashes only; underscores forbidden;
|
||||||
|
must start with a letter.
|
||||||
|
- `description`: non-empty string (stripped length > 0). Displayed
|
||||||
|
verbatim by `kernbench list`.
|
||||||
|
- The decorator **returns the function unchanged** — direct invocation
|
||||||
|
is fine. Its only side effect is appending to `_PENDING`.
|
||||||
|
|
||||||
|
Violations of the first two rules raise `ValueError` at decoration time.
|
||||||
|
Duplicate names are caught at `_finalize()` with
|
||||||
|
`RuntimeError("duplicate bench name: ...")`.
|
||||||
|
|
||||||
|
### D2. Module-file convention
|
||||||
|
|
||||||
|
Every `src/kernbench/benches/<slug>.py` must be one of:
|
||||||
|
|
||||||
|
- **A bench module**: at top-level import, `@bench(...)` runs at least
|
||||||
|
once to register at least one bench.
|
||||||
|
- **A helper module**: the filename starts with `_` (e.g.,
|
||||||
|
`_shared_helpers.py`). `iter_modules` skips it.
|
||||||
|
|
||||||
|
The audit (`_audit_modules`) rejects any non-helper that fails to call
|
||||||
|
`@bench`. Intended consequence: dropping a new file into `benches/`
|
||||||
|
automatically registers its benches, and helper modules are clearly
|
||||||
|
flagged by their filename prefix alone.
|
||||||
|
|
||||||
|
### D3. The bench function signature is `def run(torch)`
|
||||||
|
|
||||||
|
The decorator does not enforce a function name, but **CLI dispatch calls
|
||||||
|
`spec_entry.run`** (the decorated callable). The convention is therefore:
|
||||||
|
|
||||||
|
- Function name: `run`. Other names work, but always use `run` for
|
||||||
|
readability and grep-ability.
|
||||||
|
- Argument: a single positional `torch`. In practice this is a
|
||||||
|
`RuntimeContext` instance exposing PyTorch-style namespaces
|
||||||
|
(zeros/empty/launch/distributed/...) — see ADR-0024 D3.
|
||||||
|
- Return value: any (`Any`). `run_bench` ignores it and tracks
|
||||||
|
completion via `ctx.handles()` / `engine.get_completion()`.
|
||||||
|
|
||||||
|
The `torch` name imitates a PyTorch-compatible idiom; the actual PyTorch
|
||||||
|
module is not passed in (aligned with ADR-0024's "rank = SIP" launcher
|
||||||
|
convention).
|
||||||
|
|
||||||
|
### D4. A bench must submit at least once
|
||||||
|
|
||||||
|
If `ctx.handles()` is empty after the bench returns, `run_bench` reports
|
||||||
|
`BenchResult.completion = ok=False, error_code="NO_REQUESTS"`. So a
|
||||||
|
meaningful bench must invoke at least one of:
|
||||||
|
|
||||||
|
- Tensor-creation APIs: `torch.zeros(...)`, `torch.empty(...)` — these
|
||||||
|
internally submit `MmuMapMsg` and (for zeros) `MemoryWriteMsg`.
|
||||||
|
- Kernel-launch API: `torch.launch(name, fn, *args)` — submits per-SIP
|
||||||
|
`KernelLaunchMsg`.
|
||||||
|
- (Exception) Empty placeholder benches: e.g.,
|
||||||
|
`ipcq_allreduce.py`'s `print(...)`-only stub will receive a
|
||||||
|
NO_REQUESTS result. CI is expected to recognize and handle placeholder
|
||||||
|
benches specially.
|
||||||
|
|
||||||
|
### D5. Single-device convention + multi-SIP exception (ADR-0024/0027)
|
||||||
|
|
||||||
|
CLAUDE.md Part 2 CLI Semantics' **"benchmarks MUST remain
|
||||||
|
single-device"** rule is interpreted as follows:
|
||||||
|
|
||||||
|
- **Standard bench (single-SIP use)**: define tensor placement with
|
||||||
|
`dp = DPPolicy(...)` and launch with `torch.launch(...)`. The SIP
|
||||||
|
index is chosen by `--device` (CLI's responsibility).
|
||||||
|
- **CCL bench (multi-SIP use)**: as an exception, use
|
||||||
|
`torch.distributed.init_process_group(backend="ahbm")` plus
|
||||||
|
`torch.multiprocessing.spawn(_worker, ..., nprocs=ws)` for the
|
||||||
|
rank = SIP pattern (ADR-0024 D3). `--device` is ignored (or treated
|
||||||
|
as `all`); each spawned worker calls `torch.ahbm.set_device(rank)` to
|
||||||
|
bind to its SIP.
|
||||||
|
|
||||||
|
Multi-device patterns outside these two (e.g., one bench function
|
||||||
|
launching across multiple SIPs in the same process) are forbidden by
|
||||||
|
this ADR. Even with `--device all`, the bench runs once; multi-SIP use
|
||||||
|
inside that single run must follow D5's second pattern.
|
||||||
|
|
||||||
|
### D6. Name/index resolution (`resolve`)
|
||||||
|
|
||||||
|
`resolve(identifier: str)` returns a BenchSpec via:
|
||||||
|
|
||||||
|
1. If `identifier.isdigit()`: convert to int and find the spec where
|
||||||
|
`index ==` that value. If none, `ValueError("No bench with index
|
||||||
|
...")`.
|
||||||
|
2. If `identifier in _REGISTRY`: direct lookup.
|
||||||
|
3. Otherwise: `ValueError("Unknown bench ...")`.
|
||||||
|
|
||||||
|
Empty or whitespace-only identifiers raise `ValueError("bench
|
||||||
|
identifier must be a non-empty string.")`.
|
||||||
|
|
||||||
|
The CLI passes `--bench` directly to `resolve`, so users can use either
|
||||||
|
`kernbench run --bench gemm-single-pe` or `kernbench run --bench 2`.
|
||||||
|
|
||||||
|
### D7. Indices are not a stable API
|
||||||
|
|
||||||
|
`_finalize()` sorts `_PENDING` alphabetically by name and assigns
|
||||||
|
1-based indices. Adding a new bench can shift existing benches'
|
||||||
|
indices. Therefore:
|
||||||
|
|
||||||
|
- Human-interactive use: indices are fine.
|
||||||
|
- Scripts / CI automation: always use the name.
|
||||||
|
|
||||||
|
This caveat is documented in `registry.py`'s module docstring.
|
||||||
|
|
||||||
|
### D8. Surface RuntimeContext exposes to benches
|
||||||
|
|
||||||
|
A bench's `torch` parameter may legitimately use:
|
||||||
|
|
||||||
|
- **Tensor creation**: `torch.empty(shape, dtype=..., dp=DPPolicy(...),
|
||||||
|
name=...)`, `torch.zeros(...)`, `torch.from_numpy(arr)`. All submit
|
||||||
|
host-side metadata plus device deployment (`MmuMapMsg` +
|
||||||
|
`MemoryWriteMsg`).
|
||||||
|
- **Kernel launch**: `torch.launch(kernel_name, kernel_fn, *args)` —
|
||||||
|
converts `(Tensor, int, float)` positional args to `TensorArg` /
|
||||||
|
`ScalarArg`, submits per-SIP `KernelLaunchMsg`, and drains.
|
||||||
|
- **Synchronization**: `torch.wait(handle)`, `torch.wait_all()`
|
||||||
|
(`run_bench` calls the latter automatically).
|
||||||
|
- **Distributed**: `torch.distributed.init_process_group(backend="ahbm")`,
|
||||||
|
`torch.distributed.get_world_size()`,
|
||||||
|
`torch.distributed.all_reduce(t, op=...)` (ADR-0024/0027).
|
||||||
|
- **Multi-process (rank = SIP)**:
|
||||||
|
`torch.multiprocessing.spawn(_worker, ..., nprocs=ws)` (ADR-0024 D3 /
|
||||||
|
ADR-0027).
|
||||||
|
- **Device binding**: `torch.ahbm.set_device(rank)` or
|
||||||
|
`torch.accelerator.set_device_index(rank)` (both point to the same
|
||||||
|
namespace).
|
||||||
|
- **IPCQ install**: `torch.install_ipcq(algorithm=..., ccl_yaml=...)`
|
||||||
|
(ADR-0023 D10).
|
||||||
|
- **Spec lookup**: `torch.spec` — the dict produced by the topology
|
||||||
|
builder (system / cube_mesh / HBM parameters etc.). Use it so the
|
||||||
|
bench does not hardcode topology.yaml values.
|
||||||
|
|
||||||
|
Benches must not access RuntimeContext private members (`_handles`,
|
||||||
|
`_traces`, `_allocators`, etc.) directly. This aligns with ADR-0007's
|
||||||
|
layer-boundary spirit: bench → runtime API → sim_engine flows in one
|
||||||
|
direction.
|
||||||
|
|
||||||
|
### D9. Environment-variable parameterization is allowed
|
||||||
|
|
||||||
|
Benches may parameterize themselves via `os.environ.get(...)`, as
|
||||||
|
`matmul_composite.py` does for `MATMUL_M`, `MATMUL_K`, `MATMUL_N`,
|
||||||
|
`MATMUL_DTYPE`, `MATMUL_VARIANT`. Rationale:
|
||||||
|
|
||||||
|
- The bench function signature is fixed by D3 to `def run(torch)`, so
|
||||||
|
positional/keyword arguments cannot carry parameters.
|
||||||
|
- The env-var pattern is a natural hook for operational sweeps (e.g.,
|
||||||
|
`MATMUL_VARIANT`).
|
||||||
|
- External drivers such as `scripts/gemm_sweep.py` (ADR-0044) consume
|
||||||
|
this hook (it sets `MATMUL_M/K/N/VARIANT` at
|
||||||
|
`scripts/gemm_sweep.py:115-118`).
|
||||||
|
|
||||||
|
When environment variables alter bench behavior, the module docstring
|
||||||
|
must list every variable used (`matmul_composite.py` is the canonical
|
||||||
|
example).
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
### A1. An explicit manifest file (YAML) listing benches
|
||||||
|
|
||||||
|
Rejected. The `@bench` + audit pattern guarantees "drop in file → auto-
|
||||||
|
register", concentrating cognitive cost in one place (the file itself).
|
||||||
|
A separate manifest is prone to drift, and helper separation is already
|
||||||
|
clear via the `_` prefix.
|
||||||
|
|
||||||
|
### A2. Allowing the bench's entry-point name in the decorator
|
||||||
|
(`@bench(name=..., entry="run_xxx")`)
|
||||||
|
|
||||||
|
Rejected. Breaks the simplicity of dispatch (`spec.run` is a single
|
||||||
|
callable). The `run` convention is sufficient; variants can register
|
||||||
|
multiple `@bench`-decorated functions in the same module.
|
||||||
|
|
||||||
|
### A3. A separate `@multi_device_bench` decorator for CCL
|
||||||
|
|
||||||
|
Rejected. The two patterns named in D5 (single + ADR-0024 multi-SIP)
|
||||||
|
cover all 8 current benches. A separate decorator would force dispatch
|
||||||
|
to branch and add complexity; the multi-SIP intent is already obvious
|
||||||
|
from the bench's `init_process_group(...)` call.
|
||||||
|
|
||||||
|
### A4. Make indices a stable API (registration order or explicit
|
||||||
|
`index=` argument)
|
||||||
|
|
||||||
|
Rejected. D7's trade-off favors user-friendliness — alphabetically
|
||||||
|
sorted 1-based indices read naturally in the `list` output. Scripts can
|
||||||
|
use names.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- "How to add a bench" is consolidated in one ADR — new authors only
|
||||||
|
need to read D1-D3 and D8 without grepping source.
|
||||||
|
- The `_`-prefixed helper-module pattern is legitimized at ADR level,
|
||||||
|
so future `benches/_*.py` shared helpers are free to be added.
|
||||||
|
- The CLI's single-device convention and CCL's multi-SIP exception are
|
||||||
|
shown to be consistent (D5) — they are orthogonal.
|
||||||
|
- The rationale for ADR-0044's GEMM eval harness using env-var hooks
|
||||||
|
(D9) is now ADR-pinned.
|
||||||
|
- Indices are explicitly unstable (D7), so any CI code calling
|
||||||
|
`kernbench run --bench 3` is flagged for review after this ADR is
|
||||||
|
accepted.
|
||||||
@@ -0,0 +1,327 @@
|
|||||||
|
# ADR-0046: TLContext — Kernel-side `tl.*` API Contract
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted (2026-05-22).
|
||||||
|
|
||||||
|
Documents the set of `tl.*` primitives exposed by
|
||||||
|
`src/kernbench/triton_emu/`'s `TLContext`, their semantics, and the two
|
||||||
|
execution-mode contracts (command-list / greenlet runner). ADR-0014/0020
|
||||||
|
defines the PE pipeline and the 2-pass execution model, but **the `tl.*`
|
||||||
|
surface that bench kernel functions call** had no ADR-level coverage.
|
||||||
|
|
||||||
|
## First action
|
||||||
|
|
||||||
|
When `TLContext(pe_id, num_programs, dispatch_cycles, runner, cube_id,
|
||||||
|
num_cubes, scratch_base, scratch_size)` is instantiated, the first action
|
||||||
|
is to initialize six categories of state:
|
||||||
|
|
||||||
|
- `self._pe_id`, `self._num_programs`, `self._cube_id`, `self._num_cubes` —
|
||||||
|
values that `tl.program_id` / `tl.num_programs` will return.
|
||||||
|
- `self._dispatch_cycles` — cycle count emitted as `PeCpuOverheadCmd(cycles)`
|
||||||
|
at the start of every `tl.*` API call.
|
||||||
|
- `self._runner` — `KernelRunner` instance (present → greenlet mode;
|
||||||
|
absent → command-list mode).
|
||||||
|
- `self._commands: list[PeCommand] = []` — command-list accumulator
|
||||||
|
(command-list mode only).
|
||||||
|
- `self._handle_counter = 0`, `self._completion_counter = 0` — counters
|
||||||
|
for generating TensorHandle / CompletionHandle ids.
|
||||||
|
- `self._scratch_base`, `self._scratch_size`, `self._scratch_cursor = 0` —
|
||||||
|
PE-local scratch region (used for math/dot/composite output handle
|
||||||
|
addresses).
|
||||||
|
|
||||||
|
In short, **TLContext's first act is "record where (sip/cube/pe) and at
|
||||||
|
what scale (num_programs/num_cubes) this kernel instance runs, and pick
|
||||||
|
its dispatch mode (runner present or not)"**. No SimPy event is created
|
||||||
|
and no command is emitted at this moment.
|
||||||
|
|
||||||
|
The runtime first action happens when the kernel function first calls a
|
||||||
|
`tl.<api>()`. The standard entry for every `tl.*` API is:
|
||||||
|
|
||||||
|
1. Call `self._emit_dispatch_overhead()` — if `dispatch_cycles > 0`,
|
||||||
|
immediately `_emit` a `PeCpuOverheadCmd(dispatch_cycles)`.
|
||||||
|
2. Per-API processing (TensorHandle creation, command construction).
|
||||||
|
3. `self._emit(cmd)` — in runner mode this `greenlet.switch()`es the cmd
|
||||||
|
to SimPy; in command-list mode it appends to `self._commands`.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The `tl.*` surface consists of `TLContext`'s methods, and the `tl`
|
||||||
|
parameter received by a kernel function is one of these objects. The
|
||||||
|
contract the user (bench author) sees:
|
||||||
|
|
||||||
|
- Which primitives exist.
|
||||||
|
- What data flow each primitive triggers (DMA / compute / IPCQ /
|
||||||
|
metadata-only).
|
||||||
|
- How a TensorHandle's `space` and `addr` are decided.
|
||||||
|
- The difference between command-list and greenlet modes.
|
||||||
|
|
||||||
|
ADR-0014 (PE pipeline) defines the PeCommands consumed by PE_SCHEDULER,
|
||||||
|
but how `tl.*` emits them is a code-only convention. ADR-0020 (2-pass
|
||||||
|
data execution) mentions greenlet mode in D3 but does not pin down the
|
||||||
|
signature difference (return-value handling) between the runner /
|
||||||
|
non-runner paths. This ADR fills the gap.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### D1. The `tl` parameter is a `TLContext` instance
|
||||||
|
|
||||||
|
A bench kernel function has the signature:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _kernel(arg1, arg2, ..., tl, **kwargs):
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
`tl` is a `kernbench.triton_emu.tl_context.TLContext` instance. The name
|
||||||
|
imitates real Triton's `triton.language` module; the actual Triton
|
||||||
|
module is **not** passed in.
|
||||||
|
|
||||||
|
The kernel is plain Python — no `yield` or `async`. `tl.*` calls produce
|
||||||
|
SimPy events, but to the caller they appear synchronous because in
|
||||||
|
greenlet mode the KernelRunner relays between SimPy and the kernel
|
||||||
|
(ADR-0020 D3).
|
||||||
|
|
||||||
|
### D2. Two execution modes — command-list / greenlet runner
|
||||||
|
|
||||||
|
- **Command-list mode (`runner is None`)**: `tl.*` calls append PeCommand
|
||||||
|
to `self._commands`. DMA / GEMM / Math consume no SimPy time and return
|
||||||
|
metadata-only TensorHandles (`data=None`). PE_SCHEDULER / sim_engine
|
||||||
|
later replays the command sequence in time.
|
||||||
|
|
||||||
|
- **Greenlet runner mode (`runner is not None`)**: `tl.*` calls
|
||||||
|
`self._emit(cmd)` → `runner.switch_to_simpy(cmd)`, handing control to
|
||||||
|
the parent greenlet (SimPy). The parent distributes the cmd to
|
||||||
|
components, consumes SimPy time, and (for DMA reads) returns real numpy
|
||||||
|
data. The kernel receives the result and continues to the next line
|
||||||
|
(the data-aware execution model from ADR-0020 D3).
|
||||||
|
|
||||||
|
The choice of mode is decided by whether a KernelRunner is injected into
|
||||||
|
the TLContext. The `tl.*` methods themselves are mode-blind — they go
|
||||||
|
through `_emit()` uniformly.
|
||||||
|
|
||||||
|
### D3. Primitive categories
|
||||||
|
|
||||||
|
#### D3.1. Reference (no DMA, metadata only)
|
||||||
|
|
||||||
|
- `tl.ref(ptr, shape, dtype="f16") -> TensorHandle`: create a handle
|
||||||
|
referencing HBM data without issuing DMA. Used when the scheduler
|
||||||
|
streams the data per-tile (e.g., the b operand of a composite GEMM).
|
||||||
|
|
||||||
|
#### D3.2. Data movement (blocking, DMA engine)
|
||||||
|
|
||||||
|
- `tl.load(ptr, shape, dtype="f16") -> TensorHandle`: HBM → handle.
|
||||||
|
Emits `DmaReadCmd`. In greenlet mode the returned handle's `.data`
|
||||||
|
carries real numpy data; in command-list mode it is a placeholder.
|
||||||
|
The handle has `space="hbm"`, `pinned=True`.
|
||||||
|
- `tl.store(ptr, handle) -> None`: TCM → HBM. Emits `DmaWriteCmd`. In
|
||||||
|
greenlet mode, when `handle.data` is present, `_store.write("hbm",
|
||||||
|
ptr, data)` runs first (visibility = issue time, ADR-0020 D3).
|
||||||
|
|
||||||
|
#### D3.3. GEMM / compute (blocking)
|
||||||
|
|
||||||
|
- `tl.dot(a, b) -> TensorHandle`: `a @ b`. Both operands must live in
|
||||||
|
TCM; shapes `(M,K) × (K,N) → (M,N)`. Emits `GemmCmd`; the output
|
||||||
|
handle is allocated from PE-local scratch via
|
||||||
|
`_make_compute_out(shape, dtype)`.
|
||||||
|
- `tl.composite(op, a, b=None, out_ptr=0, math_op=None, epilogue=None,
|
||||||
|
acc_dtype=None, tile_shape=None) -> CompletionHandle`: non-blocking
|
||||||
|
tiled pipeline. Emits `CompositeCmd`. `epilogue` is a list of dicts,
|
||||||
|
each with `"op"` plus op-specific fields and an optional `"scope"`
|
||||||
|
(k_tile / output_tile). Unknown ops or missing fields raise
|
||||||
|
ValueError immediately. The returned CompletionHandle synchronizes
|
||||||
|
via `tl.wait(h)`.
|
||||||
|
|
||||||
|
#### D3.4. Math: unary (blocking)
|
||||||
|
|
||||||
|
- `tl.exp(x)`, `tl.log(x)`, `tl.sqrt(x)`, `tl.abs(x)`, `tl.sigmoid(x)`,
|
||||||
|
`tl.cos(x)`, `tl.sin(x)` — each emits `MathCmd(op=<name>,
|
||||||
|
inputs=(x,), out=)`. `out` is scratch-allocated with the same
|
||||||
|
shape/dtype as `x`.
|
||||||
|
|
||||||
|
#### D3.5. Math: binary (blocking)
|
||||||
|
|
||||||
|
- `tl.maximum(a, b)`, `tl.minimum(a, b)` — `_binary_math`.
|
||||||
|
- `tl.fma(a, b, c)` — `a*b + c`. Three inputs.
|
||||||
|
- `tl.clamp(x, min, max)` — `MathCmd(op="clamp", inputs=(x, min, max))`.
|
||||||
|
- `tl.where(cond, a, b)` — `MathCmd(op="where", inputs=(cond, a, b))`.
|
||||||
|
- `tl.softmax(x, axis=-1)` — a single `MathCmd(op="softmax")` so timing
|
||||||
|
accounts at one dispatch. Phase 2 DataExecutor expands it to the
|
||||||
|
canonical (x-max → exp → sum → div) sequence.
|
||||||
|
|
||||||
|
#### D3.6. Reduction (blocking)
|
||||||
|
|
||||||
|
- `tl.sum(x, axis)`, `tl.max(x, axis)`, `tl.min(x, axis)` — return an
|
||||||
|
output handle with the axis size collapsed to 1. Emit
|
||||||
|
`MathCmd(op=<name>, inputs=(x,), out=, axis=axis)`.
|
||||||
|
|
||||||
|
#### D3.7. Index / scalar (PE_CPU, no engine)
|
||||||
|
|
||||||
|
- `tl.program_id(axis=0) -> int`: `axis==0` → pe_id (cube-local PE
|
||||||
|
index), `axis==1` → cube_id (ADR-0022).
|
||||||
|
- `tl.num_programs(axis=0) -> int`: `axis==0` → num_programs (PEs per
|
||||||
|
cube), `axis==1` → num_cubes.
|
||||||
|
- `tl.arange(start, end, dtype="i32") -> TensorHandle`: an index range
|
||||||
|
in TCM. No command emitted.
|
||||||
|
- `tl.zeros(shape, dtype="f16") -> TensorHandle`, `tl.full(shape,
|
||||||
|
value, dtype="f16") -> TensorHandle`: TCM placeholder. No command
|
||||||
|
emitted.
|
||||||
|
|
||||||
|
#### D3.8. Scalar helpers (no command, no engine)
|
||||||
|
|
||||||
|
- `TLContext.cdiv(a, b) -> int` (static): ceiling division
|
||||||
|
`-(-a // b)`. Mirrors real Triton's `tl.cdiv`.
|
||||||
|
|
||||||
|
#### D3.9. Metadata-only (no compute, no DMA)
|
||||||
|
|
||||||
|
- `tl.trans(x) -> TensorHandle`: a new handle with the last two dims
|
||||||
|
swapped. Shares `addr` and `data`; no command emitted.
|
||||||
|
|
||||||
|
#### D3.10. IPCQ (CCL) primitives (ADR-0023 D4)
|
||||||
|
|
||||||
|
- `tl.send(dir, src=None, *, src_addr=None, nbytes=None, shape=None,
|
||||||
|
dtype="f16", space="tcm") -> None`: blocking send. Accepts either
|
||||||
|
handle form or raw-address form. Emits `IpcqSendCmd`. The handle's
|
||||||
|
`.data` snapshot rides along on the command — avoiding the race
|
||||||
|
where a later inbound IPCQ overwrites the slot before the outbound
|
||||||
|
PE_DMA reads it.
|
||||||
|
- `tl.recv(dir=None, shape=(), dtype="f16", space="tcm", dst_addr=None,
|
||||||
|
dst_space=None) -> TensorHandle`: blocking recv. Providing both
|
||||||
|
`dst_addr` and `dst_space` enters "copy_to_dst" mode; otherwise
|
||||||
|
"return_slot" mode. In greenlet mode the handle's `.data` carries
|
||||||
|
the real data.
|
||||||
|
- `tl.recv_no_consume(dir=None, shape=(), dtype="f16") -> TensorHandle`:
|
||||||
|
**DIAGNOSTIC ONLY**. Has the same blocking-arrival semantics as
|
||||||
|
`tl.recv` but skips the slot-read latency charge (slot-IO + PE↔bank
|
||||||
|
fabric drain). Used in the pe2pe overview plot for an apples-to-apples
|
||||||
|
comparison against `tl.store`. Production kernels MUST NOT use it —
|
||||||
|
the diagnostic flag is isolated in its own command branch
|
||||||
|
(`consume=False`) so it cannot be accidentally enabled.
|
||||||
|
- `tl.recv_async(dir, shape=(), dtype="f16") -> RecvFuture`: non-blocking
|
||||||
|
recv. Returns a `RecvFuture`; resolved later by `tl.wait(future)`.
|
||||||
|
|
||||||
|
#### D3.11. Composite + control
|
||||||
|
|
||||||
|
- `tl.composite(...)`: see D3.3.
|
||||||
|
- `tl.wait(handle=None)`: wait on a `CompletionHandle` (composite), a
|
||||||
|
`RecvFuture` (async recv), or `None` (all pending composites).
|
||||||
|
- `tl.cycles(n)`: declare a scalar PE_CPU overhead. Emits
|
||||||
|
`PeCpuOverheadCmd(cycles=n)`.
|
||||||
|
|
||||||
|
### D4. TensorHandle arithmetic operators — thread-local TLContext
|
||||||
|
|
||||||
|
At module load, `tl_context.py::_enable_tensor_ops()` runs and patches
|
||||||
|
`TensorHandle.__add__`, `__sub__`, `__mul__`, `__truediv__`. Each
|
||||||
|
operator calls `_binary_math` on the active TLContext stored in a
|
||||||
|
module-level thread-local `_ctx`.
|
||||||
|
|
||||||
|
So inside a kernel, `c = a + b` is equivalent to emitting
|
||||||
|
`MathCmd(op="add", inputs=(a, b), out=)` and returning a new
|
||||||
|
TensorHandle.
|
||||||
|
|
||||||
|
Active-TLContext management:
|
||||||
|
|
||||||
|
- `TLContext._set_active(ctx)`: set the active ctx for the current
|
||||||
|
thread/greenlet.
|
||||||
|
- `TLContext._get_active()`: read it (RuntimeError if unset).
|
||||||
|
- `run_kernel(kernel_fn, tl_ctx, *args, **kwargs)`: helper. Sets active
|
||||||
|
on entry, runs the kernel, restores `None` on exit.
|
||||||
|
|
||||||
|
`KernelRunner` re-asserts `_set_active(tl)` inside its `_switch_kernel`
|
||||||
|
just before resuming the kernel, so a sibling PE runner that overwrote
|
||||||
|
the thread-local context is correctly recovered.
|
||||||
|
|
||||||
|
### D5. Scratch allocator — compute output handles
|
||||||
|
|
||||||
|
Ops that produce a result — `tl.dot`, `tl.exp`, `tl.add` (via
|
||||||
|
TensorHandle `__add__`), etc. — call `_make_compute_out(shape, dtype)`
|
||||||
|
to obtain a 16-byte-aligned scratch address. The address is published
|
||||||
|
with `space="tcm"`, so the handle can later be the source of a
|
||||||
|
`tl.send` / `tl.store`.
|
||||||
|
|
||||||
|
When `_scratch_base == 0` (e.g., command-list mode), the address is 0
|
||||||
|
and the handle cannot be a send/store source (in that case, only
|
||||||
|
`tl.load`-returned handles are valid sources).
|
||||||
|
|
||||||
|
When the cursor exceeds `_scratch_size` (default 1 MiB), a
|
||||||
|
RuntimeError is raised. The cursor must reset between kernel
|
||||||
|
invocations (current code naturally satisfies this: KernelRunner
|
||||||
|
creates a fresh TLContext each time).
|
||||||
|
|
||||||
|
### D6. Dispatch overhead — `PeCpuOverheadCmd(dispatch_cycles)`
|
||||||
|
|
||||||
|
Every non-metadata `tl.*` call starts with `_emit_dispatch_overhead()`,
|
||||||
|
which — when `dispatch_cycles > 0` — emits
|
||||||
|
`PeCpuOverheadCmd(dispatch_cycles)`. This models the cycles PE_CPU
|
||||||
|
spends dispatching the command.
|
||||||
|
|
||||||
|
Defaults:
|
||||||
|
|
||||||
|
- `TLContext.__init__`'s `dispatch_cycles` parameter default: `1` cycle.
|
||||||
|
- TLContext built by `KernelRunner`: `0` cycles (greenlet mode handles
|
||||||
|
cycle accounting differently — aligned with ADR-0020 D3 intent).
|
||||||
|
|
||||||
|
### D7. Kernel registry (`triton_emu/registry.py`)
|
||||||
|
|
||||||
|
A separate `_kernels: dict[str, Callable]` holds the name → function
|
||||||
|
mapping:
|
||||||
|
|
||||||
|
- `register_kernel(name, fn)`: ValueError on duplicate.
|
||||||
|
- `get_kernel(name)`: KeyError if missing.
|
||||||
|
- `clear_registry()`: test-only.
|
||||||
|
|
||||||
|
`RuntimeContext.launch(kernel_name, kernel_fn, *args)` overwrites
|
||||||
|
`_kernels[kernel_name] = kernel_fn` on every call (last-call-wins,
|
||||||
|
idempotent) — consistent with ADR-0045 D8's `launch` behavior.
|
||||||
|
|
||||||
|
PE_CPU looks up `KernelRef.name` in the registry and runs the function
|
||||||
|
through KernelRunner.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
### A1. Fold `tl.*` into ADR-0014 / ADR-0020
|
||||||
|
|
||||||
|
Rejected. ADR-0014 covers the PE pipeline (sim_engine-side consumption
|
||||||
|
of PeCommands); ADR-0020 covers 2-pass execution (Phase 1 timing /
|
||||||
|
Phase 2 data). The `tl.*` surface is what the kernel author touches; a
|
||||||
|
dedicated ADR improves findability and onboarding.
|
||||||
|
|
||||||
|
### A2. Deprecate command-list mode
|
||||||
|
|
||||||
|
Rejected (currently). Simple unit tests and kernel verification benefit
|
||||||
|
from the lighter command-list path — it exposes a PeCommand sequence
|
||||||
|
inspector without requiring greenlet machinery. When greenlet-mode
|
||||||
|
semantics (real data, Phase 2) are needed, D2 explicitly selects them.
|
||||||
|
|
||||||
|
### A3. Remove TensorHandle arithmetic operators
|
||||||
|
|
||||||
|
Rejected. They mimic real Triton kernel ergonomics (e.g., `c = a + b`),
|
||||||
|
and the thread-local active-ctx pattern works cleanly. The explicit
|
||||||
|
function-form (`tl.add(a, b)`) is also exposed in D3.5, so the
|
||||||
|
operators are syntactic sugar.
|
||||||
|
|
||||||
|
### A4. Expand softmax into the explicit sequence (max → exp → sum → div)
|
||||||
|
|
||||||
|
Partially adopted. `tl.softmax` is a single `MathCmd(op="softmax")` for
|
||||||
|
timing accounting (D3.5), but Phase 2 DataExecutor expands it to the
|
||||||
|
canonical sequence for real-data computation. Timing model atomic,
|
||||||
|
data model expanded — the two split intentionally.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Every `tl.*` primitive a bench author meets is classified and defined
|
||||||
|
in a single ADR. Paired with ADR-0045 D8's host-side surface
|
||||||
|
(`torch.empty` etc.), the inside-kernel and outside-kernel authoring
|
||||||
|
guides are now complete.
|
||||||
|
- The command-list / greenlet difference is pinned in D2, so any new
|
||||||
|
`tl.*` primitive that follows the `_emit()` pattern auto-supports
|
||||||
|
both modes.
|
||||||
|
- The thread-local active-ctx pattern (D4) is justified at ADR level,
|
||||||
|
clarifying who owns the reset responsibility when multiple PE
|
||||||
|
runners share a thread (KernelRunner.run's contract restores active
|
||||||
|
inside `_switch_kernel`).
|
||||||
|
- `tl.recv_no_consume`'s diagnostic isolation (D3.10) is hardened in
|
||||||
|
ADR form — accidental production use is blocked by a separate
|
||||||
|
command branch.
|
||||||
|
- The registry (D7) gets its own D-section, formalizing the
|
||||||
|
name-collision and dynamic-re-registration semantics.
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
# ADR-0047: AHBM CCL Backend — `torch.distributed`-compat shim
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted (2026-05-22).
|
||||||
|
|
||||||
|
Pins down what `runtime_api/distributed.py`'s `AhbmCCLBackend` +
|
||||||
|
`DistributedContext` actually install — i.e., the entry point
|
||||||
|
`torch.distributed.init_process_group(backend="ahbm")` — and how
|
||||||
|
`all_reduce`/`barrier`/`get_rank` etc. are implemented. ADR-0023 D11
|
||||||
|
mentions the "torch.distributed compatibility" intent, but **the backend
|
||||||
|
itself** had no ADR-level coverage.
|
||||||
|
|
||||||
|
## First action
|
||||||
|
|
||||||
|
`RuntimeContext.__post_init__` automatically constructs a
|
||||||
|
`DistributedContext()` and attaches it to `self.distributed`. The first
|
||||||
|
action at that moment:
|
||||||
|
|
||||||
|
1. `self._backend: AhbmCCLBackend | None = None` — uninitialized.
|
||||||
|
2. `self._rank_by_greenlet: dict = {}` — greenlet-local rank registry
|
||||||
|
(ADR-0024 D2).
|
||||||
|
3. The caller (RuntimeContext) sets `dc._ctx_ref = self` so subsequent
|
||||||
|
`init_process_group` can reach `ctx.engine` / `ctx.spec` / `ctx.launch`.
|
||||||
|
|
||||||
|
In short, **DistributedContext's first act is "attach to RuntimeContext
|
||||||
|
with a back-reference and leave the backend slot empty"**. Actual
|
||||||
|
backend installation (IPCQ install, world_size derivation, algorithm
|
||||||
|
module import) happens only when user code calls
|
||||||
|
`torch.distributed.init_process_group(backend="ahbm")`.
|
||||||
|
|
||||||
|
At that moment, `init_process_group`'s first action is:
|
||||||
|
|
||||||
|
1. If `backend != "ahbm"`, raise `ValueError("Unsupported backend ...")`
|
||||||
|
immediately.
|
||||||
|
2. If `getattr(self, "_ctx_ref", None)` is None,
|
||||||
|
`RuntimeError("DistributedContext not bound to a RuntimeContext")`.
|
||||||
|
3. `self._backend = AhbmCCLBackend(torch_ctx=ctx)` — inside this
|
||||||
|
constructor, ccl.yaml is loaded, the algorithm module is imported,
|
||||||
|
world_size is derived, SFR is configured, and IPCQ is installed.
|
||||||
|
4. `self._backend._dist_ctx = self` — the backend gets a back-reference
|
||||||
|
so it can read `_rank_by_greenlet`.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The `AhbmCCLBackend` exists so that PyTorch DDP collective calls
|
||||||
|
(`init_process_group`, `all_reduce`, etc.) work unchanged and bench code
|
||||||
|
reads identically to a real DDP training script (in line with
|
||||||
|
ADR-0024 + ADR-0027's launcher model).
|
||||||
|
|
||||||
|
The backend's responsibilities:
|
||||||
|
|
||||||
|
- At `init_process_group` time, install the **IPCQ neighbor table once**
|
||||||
|
(analogous to NCCL communicator creation).
|
||||||
|
- For each `all_reduce(tensor, op="sum")`, dispatch the configured
|
||||||
|
algorithm's kernel function via `ctx.launch(...)`.
|
||||||
|
- Answer `get_world_size` / `get_rank` consistently from the
|
||||||
|
greenlet-local rank registry plus ccl.yaml/topology.
|
||||||
|
|
||||||
|
ADR-0023 D10 (IPCQ install plan) and ADR-0024 (SIP launcher) touch
|
||||||
|
parts of this, but **the backend's own responsibility scope and decision
|
||||||
|
order** are not pinned anywhere. This ADR fills that gap.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### D1. The backend is created only at `init_process_group(backend="ahbm")` time
|
||||||
|
|
||||||
|
`DistributedContext` starts with `_backend = None`. The backend object
|
||||||
|
does not exist until the user calls
|
||||||
|
`dist.init_process_group(backend="ahbm")`. Any other API
|
||||||
|
(`is_initialized`, `get_world_size`, `all_reduce`, `barrier`) called
|
||||||
|
while `_backend` is None raises
|
||||||
|
`RuntimeError("Default process group has not been initialized...")` via
|
||||||
|
the `_ensure_initialized` helper.
|
||||||
|
|
||||||
|
`backend != "ahbm"` raises `ValueError` immediately. Other backend names
|
||||||
|
(`nccl`, `gloo`, etc.) are not recognized.
|
||||||
|
|
||||||
|
### D2. world_size resolution priority — algorithm > defaults > topology
|
||||||
|
|
||||||
|
`AhbmCCLBackend._resolve_world_size` (ADR-0024 D1):
|
||||||
|
|
||||||
|
1. If `ccl.yaml`'s algorithm entry has `world_size`, use it.
|
||||||
|
2. Else if `defaults.world_size` is set, use it.
|
||||||
|
3. Else fall back to `spec.system.sips.count` (the topology's SIP count).
|
||||||
|
|
||||||
|
The default interpretation is **rank = SIP** (ADR-0024). Cube/PE-level
|
||||||
|
parallelism is expressed inside each rank via DPPolicy and does not
|
||||||
|
affect world_size. An explicit `ccl.yaml` override is preserved for the
|
||||||
|
legacy "rank = flat PE index" test path.
|
||||||
|
|
||||||
|
User arguments to `init_process_group(world_size=..., rank=...)` are
|
||||||
|
**accepted but ignored** (same as real PyTorch's `RANK` / `WORLD_SIZE`
|
||||||
|
env vars).
|
||||||
|
|
||||||
|
### D3. `init_process_group` performs four installation steps
|
||||||
|
|
||||||
|
Inside `AhbmCCLBackend.__init__`, in order:
|
||||||
|
|
||||||
|
1. **Load ccl.yaml**: `kernbench.ccl.install.load_ccl_config()` →
|
||||||
|
`resolve_algorithm_config(_cfg_all)` produces the merged config for
|
||||||
|
`defaults.algorithm` (or the user-specified algorithm).
|
||||||
|
2. **Import algorithm module**:
|
||||||
|
`importlib.import_module(self._merged["module"])`. The module must
|
||||||
|
expose a `kernel` function, a `kernel_args(world_size, n_elem,
|
||||||
|
cube_w, cube_h)` helper, and optionally a `TOPO_NAME_TO_KIND` map.
|
||||||
|
3. **Resolve world_size** (D2).
|
||||||
|
4. **Collect topology metadata** from `spec`: `n_sips`, `sip_topo`
|
||||||
|
(`ring_1d` default), `cube_w`/`cube_h`, `sips.w`/`sips.h`. When the
|
||||||
|
SIP topology is not `ring_1d`, derive `_sip_topo_w/h` from explicit
|
||||||
|
`w`/`h` or via square-root (require `w*h == n_sips`). Mismatch raises
|
||||||
|
`ValueError`.
|
||||||
|
5. **Install SFR + IPCQ**:
|
||||||
|
`kernbench.ccl.sfr_config.configure_sfr_intercube_multisip(engine,
|
||||||
|
spec, self._merged)`. This pushes IPCQ neighbor tables to every
|
||||||
|
SIP/cube's pe0 (one-time setup analogous to NCCL communicator
|
||||||
|
creation).
|
||||||
|
|
||||||
|
If the order changes (e.g., SFR runs before the algorithm module
|
||||||
|
loads), partial initialization can result. So D3 is treated as an
|
||||||
|
atomic 4-step block — on failure the backend remains uninstalled.
|
||||||
|
|
||||||
|
### D4. Greenlet-local rank binding (ADR-0024 D2)
|
||||||
|
|
||||||
|
`DistributedContext._rank_by_greenlet: dict[greenlet, int]` maps spawned
|
||||||
|
worker greenlets to their ranks. When the bench launcher (e.g.,
|
||||||
|
`torch.multiprocessing.spawn`) spawns a worker, it registers via
|
||||||
|
`dc._bind_rank(g, rank)`.
|
||||||
|
|
||||||
|
`get_rank()` looks up `getcurrent()`'s greenlet. Unregistered greenlets
|
||||||
|
fall back to 0 — preserves single-driver / test compatibility.
|
||||||
|
|
||||||
|
The backend reads the current greenlet's rank from
|
||||||
|
`_dist_ctx._rank_by_greenlet` during `all_reduce` (D5).
|
||||||
|
|
||||||
|
### D5. `all_reduce(tensor, op="sum")` behavior
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
- `op != "sum"` → `NotImplementedError`. Current kernels only
|
||||||
|
implement add reduction.
|
||||||
|
- `tensor._handle is None` → `RuntimeError("not deployed")`.
|
||||||
|
- `tensor._handle.shards` empty → `RuntimeError("no shards")`.
|
||||||
|
|
||||||
|
Preparation:
|
||||||
|
|
||||||
|
- `n_elem = shards[0].nbytes // tensor.itemsize` — element count of a
|
||||||
|
single shard.
|
||||||
|
- `kernel_fn = self._algo_module.kernel` — the algorithm module's entry
|
||||||
|
function (imported in D3).
|
||||||
|
- Decide effective cube dims: if the first SIP has just 1 cube, use
|
||||||
|
`(1, 1)`; otherwise use the topology's `cube_w`/`cube_h`. This
|
||||||
|
naturally absorbs TP runs that use only a subset of cubes.
|
||||||
|
- `kernel_args = self._algo_module.kernel_args(world_size, n_elem,
|
||||||
|
cube_w, cube_h)` — the algorithm decides which arguments to pass to
|
||||||
|
its kernel.
|
||||||
|
|
||||||
|
Dispatch:
|
||||||
|
|
||||||
|
- Resolve the current greenlet's rank via
|
||||||
|
`_rank_by_greenlet.get(g, 0)`.
|
||||||
|
- Append `extra_args = (sip_rank, sip_topo_kind, sip_topo_w,
|
||||||
|
sip_topo_h)`.
|
||||||
|
- `pending = self.ctx.launch(algorithm_name, kernel_fn, tensor,
|
||||||
|
*kernel_args, *extra_args, _defer_wait=True)` — `_defer_wait=True`
|
||||||
|
delegates collective drain to the main scheduler (ADR-0027 D0.4).
|
||||||
|
|
||||||
|
Drain:
|
||||||
|
|
||||||
|
- If the parent greenlet is alive (multi-greenlet mode), enqueue
|
||||||
|
`_pending_collective_handles` and switch to parent. The main
|
||||||
|
scheduler drains after all ranks have launched.
|
||||||
|
- If single-driver mode, drain inline:
|
||||||
|
`for h, _sip_id, meta in pending: self.ctx.wait(h, _meta=meta)`.
|
||||||
|
|
||||||
|
### D6. `barrier()` is a no-op (single-driver model)
|
||||||
|
|
||||||
|
kernbench runs all ranks as greenlets inside a single Python process,
|
||||||
|
so no cross-process synchronization is needed. `barrier()` is callable
|
||||||
|
but does no synchronization. Kept for real-PyTorch API compatibility so
|
||||||
|
callers don't get `NotImplementedError`.
|
||||||
|
|
||||||
|
If multi-process kernbench (SimPy event loop per process) is introduced
|
||||||
|
in the future, D6 needs a superseding ADR.
|
||||||
|
|
||||||
|
### D7. Semantics of `get_rank` / `get_world_size` / `get_backend`
|
||||||
|
|
||||||
|
- `get_rank()` (D4): the current greenlet's bound rank; unregistered → 0.
|
||||||
|
- `get_world_size()` (D2): the world_size resolved by the backend in D3.
|
||||||
|
- `get_backend()`: always the literal string `"ahbm"`. Calling before
|
||||||
|
backend exists triggers `_ensure_initialized`'s RuntimeError.
|
||||||
|
|
||||||
|
Differences vs. real PyTorch:
|
||||||
|
|
||||||
|
- Real PyTorch `get_rank()` is a process-global value; here it is
|
||||||
|
greenlet-local. Inside a spawned worker → the worker's rank; in the
|
||||||
|
main thread → 0. Bench authors should expect meaningful ranks only
|
||||||
|
inside worker functions.
|
||||||
|
|
||||||
|
### D8. Supported API surface (final)
|
||||||
|
|
||||||
|
`DistributedContext` exposes:
|
||||||
|
|
||||||
|
- `init_process_group(backend="ahbm", world_size=None, rank=None,
|
||||||
|
**kwargs)`
|
||||||
|
- `is_initialized() -> bool`
|
||||||
|
- `get_world_size() -> int`
|
||||||
|
- `get_rank() -> int`
|
||||||
|
- `get_backend() -> str`
|
||||||
|
- `all_reduce(tensor, op="sum") -> None`
|
||||||
|
- `barrier() -> None`
|
||||||
|
- (internal) `_bind_rank(g, rank)`
|
||||||
|
|
||||||
|
Other PyTorch distributed APIs (`broadcast`, `reduce`, `all_gather`,
|
||||||
|
`gather`, `scatter`, point-to-point `send/recv`, etc.) are **not
|
||||||
|
implemented**. Kernel-level expression is available via
|
||||||
|
`tl.send`/`tl.recv` (ADR-0046 D3.10), but the `dist.*` surface does not
|
||||||
|
expose them. If additional collectives are needed, add a paired
|
||||||
|
(algorithm module, `DistributedContext` method) and extend D8.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
### A1. Create the backend in `RuntimeContext.__init__`
|
||||||
|
|
||||||
|
Rejected. If `ccl.yaml` is missing or the algorithm module can't be
|
||||||
|
imported, RuntimeContext construction would fail even when the bench
|
||||||
|
does not use distributed features. Lazy creation at call time (D1) is
|
||||||
|
the right semantics.
|
||||||
|
|
||||||
|
### A2. Always derive world_size from topology (no override)
|
||||||
|
|
||||||
|
Rejected. ADR-0024 D1's "explicit override" path is used by legacy
|
||||||
|
tests. Diagnostic scenarios that define PE-level ranks within a single
|
||||||
|
SIP also need this escape hatch.
|
||||||
|
|
||||||
|
### A3. Silent fallback for unsupported `op`
|
||||||
|
|
||||||
|
Rejected. If the user intends `op="prod"` / `"max"` / `"avg"` and silent
|
||||||
|
`sum` runs instead, result validation gets very hard. Explicit
|
||||||
|
`NotImplementedError` is safer.
|
||||||
|
|
||||||
|
### A4. Implement `barrier` as a SimPy event
|
||||||
|
|
||||||
|
Rejected (currently). With single-driver semantics there is no
|
||||||
|
cross-process synchronization to express, so a no-op is meaningfully
|
||||||
|
correct. A fake-barrier SimPy event would add code complexity for no
|
||||||
|
semantic gain. Revisit when multi-process kernbench arrives.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The 4-step installation (D3) for
|
||||||
|
`torch.distributed.init_process_group(backend="ahbm")` is locked in,
|
||||||
|
making clear where future collective algorithms must hook.
|
||||||
|
- The priority order in D2 (algorithm > defaults > topology) makes the
|
||||||
|
blast radius of ccl.yaml changes quickly knowable.
|
||||||
|
- The no-op `barrier` (D6) is recorded so multi-process kernbench, if
|
||||||
|
introduced, must explicitly supersede this ADR.
|
||||||
|
- D8's list of unsupported APIs explicitly grounds the rejection
|
||||||
|
message when users call, e.g., `dist.broadcast(...)`.
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
# ADR-0048: Memory Allocator Algorithms — VirtualAllocator + PEMemAllocator
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted (2026-05-22).
|
||||||
|
|
||||||
|
Pins down the free-list algorithm, page alignment, and coalescing rules
|
||||||
|
used by `policy/address/allocator.py`'s `_FreeList` / `PEMemAllocator`
|
||||||
|
and `va_allocator.py`'s `VirtualAllocator`. ADR-0001 (PhysAddr layout)
|
||||||
|
and ADR-0011 (PA/VA/LA models) define the address schemes; the
|
||||||
|
**allocation algorithms** had no ADR-level coverage.
|
||||||
|
|
||||||
|
## First action
|
||||||
|
|
||||||
|
### `_FreeList(capacity)`
|
||||||
|
|
||||||
|
On construction: `self._capacity = capacity`, `self._used = 0`,
|
||||||
|
`self._free = [(0, capacity)]`. The first act is **establishing the
|
||||||
|
entire region as one free block** — the tuple `(offset=0,
|
||||||
|
size=capacity)` is the sole entry in the free list.
|
||||||
|
|
||||||
|
### `PEMemAllocator(sip_id, die_id, pe_id, cfg)`
|
||||||
|
|
||||||
|
On construction, builds two `_FreeList`s:
|
||||||
|
|
||||||
|
- `self._hbm = _FreeList(cfg.hbm_slice_bytes)` — the size of this PE's
|
||||||
|
HBM slice (`hbm_bytes_per_cube // hbm_slices_per_cube`).
|
||||||
|
- `self._tcm = _FreeList(cfg.tcm_allocatable_bytes)` — equals
|
||||||
|
`tcm_bytes_per_pe - tcm_scheduler_reserved_bytes` (the scheduler
|
||||||
|
reservation is pre-deducted).
|
||||||
|
|
||||||
|
So PEMemAllocator's first act is **constructing single-free-block
|
||||||
|
HBM-slice and TCM regions for this PE**.
|
||||||
|
|
||||||
|
### `VirtualAllocator(va_base, va_size, page_size=2*1024*1024)`
|
||||||
|
|
||||||
|
On construction: `self._va_base = va_base`, `self._va_size = va_size`,
|
||||||
|
`self._page_size = page_size`, `self._used = 0`, `self._free =
|
||||||
|
[(va_base, va_size)]`. The first act is **establishing one block from
|
||||||
|
va_base to va_size and stashing page_size**.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
`runtime_api/context.py::_ensure_allocators` builds the allocator set
|
||||||
|
in these stages:
|
||||||
|
|
||||||
|
1. Read `hbm_total_gb_per_cube`, `hbm_slices_per_cube`, `tcm_size_mb`,
|
||||||
|
per-target_device SIP range, etc. from `spec`.
|
||||||
|
2. Pack everything into a frozen `AddressConfig`.
|
||||||
|
3. For every combination in the target SIP range × cubes × PEs,
|
||||||
|
construct one `PEMemAllocator(sip, cube, pe, cfg)` instance.
|
||||||
|
4. Construct one `VirtualAllocator(va_base=0x1_0000_0000, va_size=64
|
||||||
|
GiB, page_size=pe_mmu.page_size)`.
|
||||||
|
|
||||||
|
Allocator responsibilities:
|
||||||
|
|
||||||
|
- **PEMemAllocator**: PA-space allocation in the PE-local HBM slice /
|
||||||
|
TCM (including PhysAddr encoding).
|
||||||
|
- **VirtualAllocator**: device-wide VA allocation, page-aligned.
|
||||||
|
`RuntimeContext._create_tensor` then pushes VA → PA mappings to
|
||||||
|
components via `MmuMapMsg`.
|
||||||
|
|
||||||
|
These algorithms are:
|
||||||
|
|
||||||
|
- **First-fit**, kept simple.
|
||||||
|
- The free-block list is **sorted by start offset**.
|
||||||
|
- On `free()`, **adjacent blocks coalesce**.
|
||||||
|
|
||||||
|
The rationale was not documented anywhere, so when someone asks "why
|
||||||
|
not best-fit?", "why not a buddy allocator?", "why does partial-overlap
|
||||||
|
free pass silently?", there was no anchor to answer from. This ADR
|
||||||
|
provides it.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### D1. `_FreeList` — offset-keyed first-fit + coalescing
|
||||||
|
|
||||||
|
`policy/address/allocator.py::_FreeList`:
|
||||||
|
|
||||||
|
- Internal representation: `list[tuple[int, int]] = [(start_offset,
|
||||||
|
size), ...]` — sorted by start offset.
|
||||||
|
- `alloc(nbytes)`:
|
||||||
|
1. Iterate the free list from the front (first-fit).
|
||||||
|
2. Take from the first block with `size >= nbytes`.
|
||||||
|
3. Exact match → drop the block; otherwise shrink it to `(start +
|
||||||
|
nbytes, size - nbytes)`.
|
||||||
|
4. `_used += nbytes`; return the taken `start`.
|
||||||
|
5. If no block fits, `AllocationError("overflow ... largest free
|
||||||
|
block ...")`.
|
||||||
|
- `free(offset, nbytes)`:
|
||||||
|
1. `_used -= nbytes`.
|
||||||
|
2. `bisect_left(self._free, (offset,))` finds the insertion index.
|
||||||
|
3. If adjacent to the previous block (`prev_start + prev_size ==
|
||||||
|
offset`), merge.
|
||||||
|
4. If adjacent to the next block (`offset + nbytes == next_start`),
|
||||||
|
merge.
|
||||||
|
5. Insert the coalesced range at the right sorted position.
|
||||||
|
|
||||||
|
This algorithm is weaker than best-fit / buddy on fragmentation, but
|
||||||
|
the simulator's workload (mostly stack-like deploy/free) tolerates it.
|
||||||
|
If the workload shape changes, D1 is a supersession candidate.
|
||||||
|
|
||||||
|
### D2. Partial-overlap free is **not** validated
|
||||||
|
|
||||||
|
`_FreeList.free(offset, nbytes)` trusts the caller to pass the exact
|
||||||
|
`(offset, nbytes)`. It does **not** verify:
|
||||||
|
|
||||||
|
- That the range was actually allocated.
|
||||||
|
- That the range does not overlap another allocated region.
|
||||||
|
|
||||||
|
Reason: in a simulator context, callers always store the return value
|
||||||
|
of `alloc()` and pass it back to `free()` — there is no external user
|
||||||
|
input. Adding a safety check would cost O(N) per free and impact
|
||||||
|
simulation wall-clock.
|
||||||
|
|
||||||
|
If this trust model breaks (e.g., a code path lets two tensors point
|
||||||
|
at the same PA), this ADR must be revisited.
|
||||||
|
|
||||||
|
### D3. `PEMemAllocator` — two channels for HBM/TCM
|
||||||
|
|
||||||
|
`PEMemAllocator(sip_id, die_id, pe_id, cfg)` holds two `_FreeList`s:
|
||||||
|
|
||||||
|
- `_hbm`: size `cfg.hbm_slice_bytes`.
|
||||||
|
- `_tcm`: size `cfg.tcm_allocatable_bytes` (= `tcm_bytes_per_pe -
|
||||||
|
tcm_scheduler_reserved_bytes`).
|
||||||
|
|
||||||
|
`alloc_hbm(nbytes) -> PhysAddr`:
|
||||||
|
|
||||||
|
- `_hbm.alloc(nbytes)` → offset.
|
||||||
|
- `PhysAddr.pe_hbm_addr(sip_id, die_id, pe_id,
|
||||||
|
pe_local_hbm_offset=offset, slice_size_bytes=cfg.hbm_slice_bytes)`.
|
||||||
|
- Failure raises `AllocationError("HBM overflow ...")`.
|
||||||
|
|
||||||
|
`free_hbm(pa, nbytes)`:
|
||||||
|
|
||||||
|
- Recover PE-local offset via `pa.hbm_offset - pe_id *
|
||||||
|
cfg.hbm_slice_bytes`.
|
||||||
|
- `_hbm.free(offset, nbytes)`.
|
||||||
|
|
||||||
|
`alloc_tcm(nbytes) -> PhysAddr`: similar; uses `PhysAddr.pe_tcm_addr`.
|
||||||
|
|
||||||
|
`free_tcm(pa, nbytes)`: uses `pa.sub_offset` directly (TCM's PE-local
|
||||||
|
offset equals its sub_offset).
|
||||||
|
|
||||||
|
The allocator does not see the scheduler-reserved TCM region
|
||||||
|
(`cfg.tcm_scheduler_reserved_bytes`) — it is pre-subtracted from the
|
||||||
|
`_tcm` capacity. This is consistent with ADR-0014's PE_SCHEDULER
|
||||||
|
internal-buffer reservation.
|
||||||
|
|
||||||
|
### D4. `VirtualAllocator` — page-aligned first-fit + coalescing
|
||||||
|
|
||||||
|
`policy/address/va_allocator.py::VirtualAllocator`:
|
||||||
|
|
||||||
|
- Internal representation: same sorted `list[tuple[int, int]]` as
|
||||||
|
`_FreeList`. Initially `[(va_base, va_size)]`.
|
||||||
|
- `_align_up(nbytes) = ceil(nbytes / page_size) * page_size`.
|
||||||
|
- `alloc(nbytes) -> int`:
|
||||||
|
1. `aligned = _align_up(nbytes)`.
|
||||||
|
2. First-fit a block with `size >= aligned`.
|
||||||
|
3. Take `aligned` from the block's front; remove if exact.
|
||||||
|
4. `_used += aligned`. Return the block's `start` (which is page-
|
||||||
|
aligned).
|
||||||
|
5. Failure → `VaAllocationError`.
|
||||||
|
- `free(va, nbytes)`: free `_align_up(nbytes)` worth. Coalesces with
|
||||||
|
the same algorithm as `_FreeList`.
|
||||||
|
|
||||||
|
`page_size` has different defaults in two places:
|
||||||
|
|
||||||
|
- `VirtualAllocator.__init__`'s parameter default: `2 MiB`. Direct-call
|
||||||
|
tests receive this.
|
||||||
|
- `RuntimeContext._ensure_allocators` when constructing the instance:
|
||||||
|
`pe_mmu.attrs.get("page_size", 4096)` — uses
|
||||||
|
`topology.yaml`'s `pe_mmu.attrs.page_size` if set, else falls back
|
||||||
|
to `4 KiB`.
|
||||||
|
|
||||||
|
The two defaults differ on purpose: `VirtualAllocator`'s standalone
|
||||||
|
default (`2 MiB`) aligns with ADR-0039's PE_MMU stopgap default for
|
||||||
|
direct-test ergonomics; the context fallback (`4 KiB`) is the safe
|
||||||
|
minimum when `topology.yaml` doesn't specify a page size. The
|
||||||
|
production path is always the latter (via `_ensure_allocators`), and
|
||||||
|
when `topology.yaml` sets `page_size`, that value flows consistently
|
||||||
|
into both the MMU and the VA allocator.
|
||||||
|
|
||||||
|
If consistency breaks (e.g., VirtualAllocator instantiated with a
|
||||||
|
page_size different from PE_MMU's), MMU `map()` falls into the
|
||||||
|
sub-page region mode (ADR-0039 D3).
|
||||||
|
|
||||||
|
VA range defaults: `va_base = 0x1_0000_0000` (= 4 GiB), `va_size = 64
|
||||||
|
GiB`. These are hardcoded in `_ensure_allocators` and have no
|
||||||
|
semantic meaning in ADR-0011's VA model — they simply reserve enough
|
||||||
|
device-wide space without colliding with host code.
|
||||||
|
|
||||||
|
### D5. Lifecycle of allocator instances
|
||||||
|
|
||||||
|
- `RuntimeContext._ensure_allocators` is lazy — called on the first
|
||||||
|
`_create_tensor`.
|
||||||
|
- The allocator dict (`self._allocators`) lives for the
|
||||||
|
RuntimeContext's lifetime. A second deploy in the same process
|
||||||
|
does not construct new objects.
|
||||||
|
- `RuntimeContext.cleanup()` walks living tensors and calls
|
||||||
|
`_free_tensor()`, which issues MMU unmaps + `va_allocator.free` +
|
||||||
|
`pemem_allocator.free_hbm` — restoring the free lists. A subsequent
|
||||||
|
RuntimeContext starts fresh.
|
||||||
|
|
||||||
|
This per-RuntimeContext isolation guarantees deterministic deploy →
|
||||||
|
cleanup → deploy sequences within a single process.
|
||||||
|
|
||||||
|
### D6. Allocator failure raises (no silent OOM)
|
||||||
|
|
||||||
|
Both `_FreeList.alloc` and `VirtualAllocator.alloc` raise
|
||||||
|
`AllocationError` / `VaAllocationError` when no block fits. The message
|
||||||
|
includes "required size + largest available block" to distinguish
|
||||||
|
fragmentation from true OOM.
|
||||||
|
|
||||||
|
A silent fallback (e.g., allocating only as much as the largest free
|
||||||
|
block) is strictly forbidden — a partially-allocated tensor reaching
|
||||||
|
SimPy would cause routing / DMA to see incorrect PAs and silently
|
||||||
|
corrupt simulation results.
|
||||||
|
|
||||||
|
### D7. One allocator per address space
|
||||||
|
|
||||||
|
Physical address spaces are separated by PhysAddr sub-units (ADR-0001
|
||||||
|
D2.3); each sub-unit gets its own allocator instance:
|
||||||
|
|
||||||
|
- HBM slice → `PEMemAllocator._hbm`.
|
||||||
|
- PE TCM → `PEMemAllocator._tcm`.
|
||||||
|
- (Currently unused) M_CPU local memory, CUBE SRAM → would need their
|
||||||
|
own allocators. Today these are handled as IPCQ-only slots (ADR-0023
|
||||||
|
D9.7) and do not share PA space, so no free-list exists for them.
|
||||||
|
|
||||||
|
When a cube-level SRAM allocator is needed,
|
||||||
|
`_FreeList(cfg.sram_bytes_per_cube)` is added per-cube
|
||||||
|
(`cfg.sram_bytes_per_cube` is already defined in `AddressConfig` —
|
||||||
|
the data model is ready).
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
### A1. Best-fit / buddy allocator
|
||||||
|
|
||||||
|
Rejected (currently). The workload's alloc/free pattern is stack-like
|
||||||
|
(deploy order ≈ free order), so first-fit + coalescing controls
|
||||||
|
fragmentation well enough. If long-running fragmentation appears in LLM
|
||||||
|
kernel sweeps, a buddy-allocator ADR will replace D1.
|
||||||
|
|
||||||
|
### A2. Add partial-overlap free validation
|
||||||
|
|
||||||
|
Rejected. D2's trust model plus the O(N) per-free cost makes this
|
||||||
|
unattractive. A debug mode (e.g., `KERNBENCH_DEBUG` env var) that
|
||||||
|
enables the check could be added later.
|
||||||
|
|
||||||
|
### A3. A unified allocator for VA and PA
|
||||||
|
|
||||||
|
Rejected. VA space (64 GiB device-wide) and PA space (per-slice ~6
|
||||||
|
GiB) have different semantic dimensions — VA is the kernel's view, PA
|
||||||
|
is the device sub-unit's view. ADR-0011's VA model (MMU maps between
|
||||||
|
the two) calls for separated allocators.
|
||||||
|
|
||||||
|
### A4. Multi-tier page sizes (large pages + small pages)
|
||||||
|
|
||||||
|
Rejected (currently). A single page size (2 MiB) matches LLM kernel
|
||||||
|
tensor sizes (a few MiB to GiB); smaller mappings are absorbed by
|
||||||
|
ADR-0039 D3's sub-page region mode. Multi-tier paging would require
|
||||||
|
extending the MMU model itself — a separate ADR candidate.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The allocator algorithm is pinned at ADR level (D1, D3, D4), so any
|
||||||
|
future simulation scenario hitting fragmentation has a clear "we're
|
||||||
|
using first-fit + coalescing" anchor to inspect.
|
||||||
|
- D2's trust model is explicit, so any future code path that exposes
|
||||||
|
alloc/free to direct user input will trigger this ADR's supersession
|
||||||
|
early.
|
||||||
|
- D7's one-allocator-per-sub-unit mapping is on record, so when M_CPU
|
||||||
|
or SRAM need their own free-list, the addition point is obvious.
|
||||||
|
- D4 captures the page_size dual-default and its production path
|
||||||
|
(`_ensure_allocators` always wins), letting future `topology.yaml`
|
||||||
|
`page_size` changes be assessed against ADR-0039's stopgap
|
||||||
|
interaction quickly.
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
# ADR-0049: `kernbench probe` Subcommand — Traffic-Pattern Verification Harness
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted (2026-05-22).
|
||||||
|
|
||||||
|
Pins down the traffic-pattern catalog, formula-vs-actual comparison, and
|
||||||
|
invariant checks (monotonicity, D2H ≥ H2D, etc.) exposed by
|
||||||
|
`probes/probe.py::run_probe(...)`. ADR-0010 (CLI surface) enumerates the
|
||||||
|
`kernbench probe` subcommand, but **what probe actually measures** and
|
||||||
|
**which invariants it judges PASS/FAIL** had no ADR-level coverage.
|
||||||
|
|
||||||
|
## First action
|
||||||
|
|
||||||
|
`run_probe(topology_path, case_filter=None)` performs four startup steps:
|
||||||
|
|
||||||
|
1. `Path(topology_path).expanduser().resolve()` → absolute path.
|
||||||
|
2. `load_topology(path)` → `TopologyGraph` (graph + spec).
|
||||||
|
3. `_build_edge_map(graph)` → a `{(src, dst): Edge}` lookup table.
|
||||||
|
4. Instantiate `AddressResolver(graph)` + `PathRouter(graph)`.
|
||||||
|
|
||||||
|
Then it sets `nbytes = 32768` (= 32 KiB, the summary-table reference
|
||||||
|
size) and `show_all = (case_filter is None or case_filter == "all")`.
|
||||||
|
|
||||||
|
In short, **probe's first act is "load the topology once and prepare
|
||||||
|
edge map / resolver / router, plus pin 32 KiB as the standard measurement
|
||||||
|
size"**. After that, the H2D → D2H → PE DMA categories execute in
|
||||||
|
separate `GraphEngine` instances (no cross-talk between cases).
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
`kernbench probe` was introduced as a verification tool for these
|
||||||
|
purposes:
|
||||||
|
|
||||||
|
- **Manual ground truth**: when a real-simulation result (`kernbench run
|
||||||
|
--bench ...`) shows abnormal latency, derive the answer for a simple
|
||||||
|
traffic pattern in isolation and compare.
|
||||||
|
- **Formula vs actual**: check whether the analytical model
|
||||||
|
(wire latency + overhead + drain) matches the simulator's
|
||||||
|
`total_ns`. A mismatch points to which simplifying assumption in
|
||||||
|
ADR-0033 is missing.
|
||||||
|
- **Monotonicity check**: latency should grow monotonically with hop
|
||||||
|
count.
|
||||||
|
- **Utilization sweep**: a BW-utilization table across data sizes
|
||||||
|
(4 KiB ~ 1 MiB).
|
||||||
|
|
||||||
|
Without an ADR for this tool:
|
||||||
|
|
||||||
|
- Adding a new traffic-pattern category (e.g., MCpuDma, IPCQ) is hard
|
||||||
|
because the table format / measurement units of existing categories
|
||||||
|
aren't documented at the ADR level.
|
||||||
|
- The basis for the monotonicity check (hop count? cube distance? wire
|
||||||
|
length?) is ambiguous.
|
||||||
|
- The reference size 32 KiB and the sweep `[4 KiB, 16 KiB, 64 KiB, 256
|
||||||
|
KiB, 1 MiB]` are only discoverable by reading source.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### D1. Three case categories — H2D / D2H / PE DMA
|
||||||
|
|
||||||
|
Each category has a distinct data path in the topology and gets its own
|
||||||
|
summary table + sweep table + route-detail block.
|
||||||
|
|
||||||
|
- **H2D (Host → Device Write)**: `MemoryWriteMsg(dst_sip=0, dst_cube,
|
||||||
|
dst_pe=0, pattern="zero")` flows along `pcie_ep → io_cpu → m_cpu →
|
||||||
|
hbm_ctrl`. The cube index varies the hop count:
|
||||||
|
- h2d-1hop: cube=0, hops=1
|
||||||
|
- h2d-2hop: cube=4, hops=2
|
||||||
|
- h2d-3hop: cube=8, hops=3
|
||||||
|
- h2d-4hop: cube=12, hops=4
|
||||||
|
- **D2H (Device → Host Read)**: `MemoryReadMsg(src_sip=0, src_cube,
|
||||||
|
src_pe=0)`. Total latency = forward command path + reverse data path.
|
||||||
|
Same 4-hops category as H2D.
|
||||||
|
- **PE DMA (PE-initiated)**: `PeDmaMsg(src_sip, src_cube, src_pe,
|
||||||
|
dst_pa)`. Five cases cover varying cube/PE positions:
|
||||||
|
- pe-local-hbm: same cube, same PE
|
||||||
|
- pe-same-half-hbm: same cube, different PE (PE 1)
|
||||||
|
- pe-cross-half-hbm: same cube, far PE (PE 4)
|
||||||
|
- pe-cross-cube-hbm-best: adjacent cube (cube 1)
|
||||||
|
- pe-cross-cube-hbm-worst: diagonal far cube (cube 15)
|
||||||
|
|
||||||
|
The cube indices 4/8/12 (H2D) and 1/4/15 (PE DMA) are meaningful for a
|
||||||
|
4 × 4 cube mesh (`sip.cube_mesh.w=4, h=4`); changes to the mesh size
|
||||||
|
require these to be updated in lockstep.
|
||||||
|
|
||||||
|
### D2. Standard measurement size — `nbytes = 32768` (32 KiB)
|
||||||
|
|
||||||
|
Every case in the summary table runs once with `nbytes=32768`. 32 KiB
|
||||||
|
was chosen because:
|
||||||
|
|
||||||
|
- DMA overhead and BW drain are balanced — neither dominates.
|
||||||
|
- It compares cleanly against the one-shot transfer size of several
|
||||||
|
sub-units (TCM, register file).
|
||||||
|
|
||||||
|
Per-size utilization variations are shown in a separate sweep table
|
||||||
|
(D3).
|
||||||
|
|
||||||
|
### D3. Utilization sweep — `[4 KiB, 16 KiB, 64 KiB, 256 KiB, 1 MiB]`
|
||||||
|
|
||||||
|
`SWEEP_SIZES = [4096, 16384, 65536, 262144, 1048576]`,
|
||||||
|
`SWEEP_LABELS = ["4KB", "16KB", "64KB", "256KB", "1MB"]`. Per size:
|
||||||
|
|
||||||
|
```
|
||||||
|
drain = nbytes / bottleneck_bw
|
||||||
|
total = overhead + wire + drain
|
||||||
|
eff_bw = nbytes / total
|
||||||
|
util% = eff_bw / bottleneck_bw × 100
|
||||||
|
```
|
||||||
|
|
||||||
|
When `bn_bw is None or <= 0`, the column shows 0.0 %. The intent: the
|
||||||
|
table shows in one view how small transfers become overhead-bound and
|
||||||
|
large transfers become drain-bound as hop count rises.
|
||||||
|
|
||||||
|
### D4. Measured columns — actual / formula / breakdown
|
||||||
|
|
||||||
|
Per-case columns:
|
||||||
|
|
||||||
|
- `Actual` (total_ns): the SimPy run's `trace["total_ns"]`.
|
||||||
|
- `Ovhd`: sum of `node.attrs["overhead_ns"]` along the path (formula).
|
||||||
|
- `Drain`: `nbytes / min(edge.bw_gbs over path)` (formula).
|
||||||
|
- `Wire`: `Σ edge.distance_mm * (ns_per_mm from spec)`.
|
||||||
|
- `Ovhd%` / `Drain%`: each portion as a percentage of Actual. Wire is
|
||||||
|
usually too small to display.
|
||||||
|
- `Eff.BW`: `nbytes / total_ns` (measured BW).
|
||||||
|
- `BN.BW`: bottleneck bandwidth (formula). The minimum edge BW along
|
||||||
|
the path. Missing edge BW shows "-".
|
||||||
|
- `Util%`: `Eff.BW / BN.BW × 100`. 100 % means the single-stream BW
|
||||||
|
upper bound is reached.
|
||||||
|
|
||||||
|
A large gap between the formula sum (`wire + ovhd + drain`) and Actual
|
||||||
|
signals a factor the simplified model misses (a place to inspect
|
||||||
|
ADR-0033's assumptions).
|
||||||
|
|
||||||
|
### D5. Automatic invariant checks — PASS/FAIL
|
||||||
|
|
||||||
|
The following invariants are reported with `[v] PASS` / `[x] FAIL`:
|
||||||
|
|
||||||
|
- **H2D / D2H monotonic increase**: as hop count rises, actual latency
|
||||||
|
must grow monotonically. `all(lats[i] < lats[i+1] for ...)`.
|
||||||
|
- **D2H ≥ H2D**: for the same hop index, D2H ≥ H2D (D2H has both
|
||||||
|
forward command and reverse data legs). `all(d2h[i].total >=
|
||||||
|
h2d[i].total)`.
|
||||||
|
- **PE DMA best < worst**: cross-cube best (adjacent) latency must be
|
||||||
|
less than cross-cube worst (diagonal).
|
||||||
|
- **PE DMA local vs remote**: prints the local BN BW vs remote BN BW
|
||||||
|
side-by-side (informational, not PASS/FAIL).
|
||||||
|
|
||||||
|
When a check fails, a single clear line surfaces the regression for
|
||||||
|
human review.
|
||||||
|
|
||||||
|
### D6. Route detail — per-hop timestamp trace
|
||||||
|
|
||||||
|
After the summary and sweep tables, each case's path and cumulative
|
||||||
|
per-hop timestamps (`_hop_timestamps`) appear in a separate section:
|
||||||
|
|
||||||
|
- H2D: leg1 (`pcie_ep → io_cpu`) + leg2 (`io_cpu → m_cpu`) + leg3
|
||||||
|
(`m_cpu → hbm_ctrl`) + per-hop trace.
|
||||||
|
- D2H: forward (cmd, no data) and reverse (data) traces shown
|
||||||
|
separately.
|
||||||
|
- PE DMA: `pe_dma → router → hbm_ctrl` path + per-hop trace.
|
||||||
|
|
||||||
|
Each hop's timestamp is cumulative `wire_ns + overhead_ns`. The
|
||||||
|
terminal hop's annotation appends `drain:Xns`. Bottleneck edges are
|
||||||
|
marked `<BN:XXGB/s>` so they are visually identifiable.
|
||||||
|
|
||||||
|
### D7. Semantics of the `case_filter` argument
|
||||||
|
|
||||||
|
- `None` or `"all"`: run all cases (default).
|
||||||
|
- Other strings: run only the case whose name matches exactly. Example:
|
||||||
|
`kernbench probe --case h2d-2hop`.
|
||||||
|
|
||||||
|
Within a category, cases with `name != case_filter` are skipped; if
|
||||||
|
only one data point remains, the category's monotonicity / D2H ≥ H2D
|
||||||
|
comparisons are naturally skipped.
|
||||||
|
|
||||||
|
The CLI parser's `--case` default is `"all"`, so omitting it runs
|
||||||
|
everything.
|
||||||
|
|
||||||
|
### D8. Fresh GraphEngine per case
|
||||||
|
|
||||||
|
Each of the 4 H2D, 4 D2H, and 5 PE DMA cases runs in **its own
|
||||||
|
GraphEngine** (`engine = GraphEngine(graph)`). Reasons:
|
||||||
|
|
||||||
|
- Isolate accumulated state (op_log, completion tracking, allocators)
|
||||||
|
so cases do not cross-talk.
|
||||||
|
- Guarantee one case's traffic does not perturb another case's BW
|
||||||
|
measurement.
|
||||||
|
|
||||||
|
This isolation lets probe results be interpreted as **single-flow**
|
||||||
|
per-case latency. Multi-flow contention measurement is handled by
|
||||||
|
separate tooling (e.g., the `pe2pe_overview` plot or ADR-0033's
|
||||||
|
multi-flow merging model).
|
||||||
|
|
||||||
|
### D9. Output-format stability
|
||||||
|
|
||||||
|
probe's stdout is meant for humans; precise column widths, separators,
|
||||||
|
and whitespace are **not** a machine-readable contract. Automated tools
|
||||||
|
that wish to parse probe output should use a separate JSON-output mode
|
||||||
|
(not yet implemented).
|
||||||
|
|
||||||
|
The `[v]` / `[x]` prefix on PASS/FAIL lines is a stable CI grep anchor.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
### A1. Register probe as another bench (`@bench(name="probe")`)
|
||||||
|
|
||||||
|
Rejected. probe is a verification tool, not a bench — multi-engine
|
||||||
|
execution for sweeps/analysis and PASS/FAIL invariant output are
|
||||||
|
essential, none of which fits ADR-0045's "single device + single
|
||||||
|
RuntimeContext" bench model.
|
||||||
|
|
||||||
|
### A2. Exit code 1 on monotonicity violation
|
||||||
|
|
||||||
|
Rejected (currently). probe is positioned as a human inspection tool —
|
||||||
|
PASS/FAIL is printed and exit is 0. A wrapper can `grep "\[x\]"` to
|
||||||
|
decide. A future `--strict` flag could opt into non-zero exits.
|
||||||
|
|
||||||
|
### A3. Externalize the case catalog to YAML
|
||||||
|
|
||||||
|
Rejected (currently). The 8 cases (4 H2D + 4 D2H + 5 PE DMA = 13 total)
|
||||||
|
are hardcoded and their semantics are tightly bound to the mesh
|
||||||
|
topology. Moving cube-index meaning (4, 8, 12 / 1, 4, 15) into YAML
|
||||||
|
would require separate documentation and lose cohesion. Externalize
|
||||||
|
only when case additions become frequent.
|
||||||
|
|
||||||
|
### A4. Add multi-flow contention measurement
|
||||||
|
|
||||||
|
Rejected (out of probe scope). D8's single-flow isolation is probe's
|
||||||
|
core intent. Multi-flow contention belongs in a different area of the
|
||||||
|
ADR-0033 latency model — either a separate tool or a new case
|
||||||
|
category.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- probe's case catalog (D1) and measurement units (D2/D3) are pinned at
|
||||||
|
ADR level, so new traffic categories know which table format to
|
||||||
|
follow.
|
||||||
|
- The semantics of the formula-vs-actual columns (D4) are locked in, so
|
||||||
|
questions like "why is Drain% 5 % or 70 %?" can quickly be linked to
|
||||||
|
ADR-0033 assumption checks.
|
||||||
|
- Automatic invariant checks (D5) are pinned, so latency-model changes
|
||||||
|
immediately catch monotonicity / D2H ≥ H2D regressions.
|
||||||
|
- D8's case-isolation is explicit, so probe results are safe to read as
|
||||||
|
single-flow measurements. If multi-flow is needed, a separate tool
|
||||||
|
track is clearly required.
|
||||||
|
- A2's strict-mode flag is recorded as a follow-up so CI integration
|
||||||
|
has a minimal change path when requested.
|
||||||
@@ -0,0 +1,322 @@
|
|||||||
|
# ADR-0050: CCL Algorithm Module Contract — `ccl/algorithms/*.py`
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted (2026-05-22).
|
||||||
|
|
||||||
|
Pins down the interface, kernel signature, and addition workflow that a
|
||||||
|
module under `src/kernbench/ccl/algorithms/` must satisfy in order to be
|
||||||
|
used as a collective algorithm by the AHBM CCL backend (ADR-0047).
|
||||||
|
ADR-0047 D3 states only that "the algorithm module must expose `kernel`,
|
||||||
|
`kernel_args`, optionally `TOPO_NAME_TO_KIND`"; **the contract an
|
||||||
|
algorithm-module author needs to follow** has had no ADR-level coverage.
|
||||||
|
This ADR pairs with ADR-0045's bench-module contract.
|
||||||
|
|
||||||
|
## First action
|
||||||
|
|
||||||
|
An algorithm module is imported at two moments:
|
||||||
|
|
||||||
|
1. **AHBM backend entry**: when user code calls
|
||||||
|
`dist.init_process_group(backend="ahbm")`,
|
||||||
|
`AhbmCCLBackend.__init__` runs
|
||||||
|
`self._algo_module = importlib.import_module(self._merged["module"])`.
|
||||||
|
At module level, the following occur first:
|
||||||
|
- Topology-kind integer constants like `SIP_TOPO_RING/TORUS/MESH`
|
||||||
|
are bound in the module namespace.
|
||||||
|
- The `TOPO_NAME_TO_KIND` dict is bound; the backend reads it via
|
||||||
|
`getattr(self._algo_module, "TOPO_NAME_TO_KIND", None)`.
|
||||||
|
- `kernel_args` function is defined for the caller.
|
||||||
|
- The actual algorithm function (e.g.,
|
||||||
|
`allreduce_intercube_multidevice`) is defined.
|
||||||
|
- At the bottom of the module, `kernel = allreduce_intercube_multidevice`
|
||||||
|
publishes the alias.
|
||||||
|
|
||||||
|
2. **ccl.yaml install stage**:
|
||||||
|
`kernbench.ccl.install.install_ipcq` imports the same algorithm
|
||||||
|
module while pushing the IPCQ neighbor table.
|
||||||
|
|
||||||
|
In short, **the algorithm module's first act is "publish topology-kind
|
||||||
|
constants, the `TOPO_NAME_TO_KIND` dict, the `kernel_args` function, and
|
||||||
|
the `kernel` alias into the module namespace"** — all as import-time
|
||||||
|
side effects, no separate initialization call.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
`AhbmCCLBackend` (ADR-0047), at process-group creation, dynamically
|
||||||
|
imports a module path obtained from `ccl.yaml`'s `defaults.algorithm` (or
|
||||||
|
a user-specified algorithm). The backend expects four things from the
|
||||||
|
module:
|
||||||
|
|
||||||
|
- `kernel`: the collective's entry function.
|
||||||
|
- `kernel_args(world_size, n_elem, cube_w=, cube_h=) -> tuple`: a tuple
|
||||||
|
packing the kernel's positional arguments.
|
||||||
|
- `TOPO_NAME_TO_KIND` (optional): a dict mapping `topology.yaml`'s
|
||||||
|
`sips.topology` string (e.g., `"ring_1d"`, `"torus_2d"`,
|
||||||
|
`"mesh_2d_no_wrap"`) to the integer kind constants.
|
||||||
|
- (Indirectly) IPCQ neighbor-table install:
|
||||||
|
`configure_sfr_intercube_multisip` reads
|
||||||
|
the module's `TOPO_NAME_TO_KIND` plus cube dimensions to decide the
|
||||||
|
SFR.
|
||||||
|
|
||||||
|
The current corpus has one algorithm module:
|
||||||
|
`lrab_hierarchical_allreduce.py` (248 lines). The name expands to
|
||||||
|
"**l**eft-**r**ight **a**lternating **b**roadcast hierarchical allreduce".
|
||||||
|
When future modules like `ring_allreduce`, `tree_allreduce`, or
|
||||||
|
`broadcast` are added, they must follow this contract for the backend's
|
||||||
|
dispatch path to keep working.
|
||||||
|
|
||||||
|
Without an ADR-level contract:
|
||||||
|
|
||||||
|
- A new algorithm author has to infer the signature from ADR-0047 D3's
|
||||||
|
one-liner.
|
||||||
|
- The kernel-function argument order (especially `t_ptr, n_elem,
|
||||||
|
cube_w, cube_h, n_sips, sip_rank, sip_topo_kind, sip_topo_w,
|
||||||
|
sip_topo_h, tl`) is unclear without grep.
|
||||||
|
- It is conventional, but not documented, what `kernel_args` takes as
|
||||||
|
inputs and what tuple it must return.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### D1. The algorithm module exposes four public symbols
|
||||||
|
|
||||||
|
```python
|
||||||
|
# src/kernbench/ccl/algorithms/<name>.py
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
# (required) topology-kind constants — referenced internally
|
||||||
|
SIP_TOPO_RING = 0
|
||||||
|
SIP_TOPO_TORUS = 1
|
||||||
|
SIP_TOPO_MESH = 2
|
||||||
|
|
||||||
|
# (optional) topology name → kind mapping. Used by the backend to
|
||||||
|
# translate ccl.yaml/topology's string SIP topology into an integer.
|
||||||
|
TOPO_NAME_TO_KIND = {
|
||||||
|
"ring_1d": SIP_TOPO_RING,
|
||||||
|
"torus_2d": SIP_TOPO_TORUS,
|
||||||
|
"mesh_2d_no_wrap": SIP_TOPO_MESH,
|
||||||
|
}
|
||||||
|
|
||||||
|
# (required) kernel argument builder
|
||||||
|
def kernel_args(world_size: int, n_elem: int, *, cube_w: int = 4, cube_h: int = 4) -> tuple:
|
||||||
|
return (n_elem, cube_w, cube_h, world_size)
|
||||||
|
|
||||||
|
# (required) kernel function (TLContext is injected via the `tl=...`
|
||||||
|
# keyword argument).
|
||||||
|
def my_allreduce_kernel(t_ptr, n_elem, cube_w, cube_h, n_sips,
|
||||||
|
sip_rank, sip_topo_kind, sip_topo_w, sip_topo_h, *, tl):
|
||||||
|
...
|
||||||
|
|
||||||
|
# (required) kernel alias — the backend accesses `module.kernel`
|
||||||
|
kernel = my_allreduce_kernel
|
||||||
|
```
|
||||||
|
|
||||||
|
- The `kernel` alias is the entry point the backend invokes. Whatever
|
||||||
|
the function name is (e.g., `allreduce_intercube_multidevice`), it
|
||||||
|
must be exposed via `module.kernel = fn`.
|
||||||
|
- Without `kernel_args`, the backend has no way to build the
|
||||||
|
algorithm's argument list. See D2 for the signature.
|
||||||
|
- If `TOPO_NAME_TO_KIND` is absent, the backend falls back to
|
||||||
|
`sip_topo_kind = 0`. An algorithm supporting only a single topology
|
||||||
|
may omit it.
|
||||||
|
|
||||||
|
### D2. `kernel_args` signature — `(world_size, n_elem, *, cube_w, cube_h)`
|
||||||
|
|
||||||
|
```python
|
||||||
|
def kernel_args(world_size: int, n_elem: int, *,
|
||||||
|
cube_w: int = 4, cube_h: int = 4) -> tuple:
|
||||||
|
return (n_elem, cube_w, cube_h, world_size)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Positional arguments**: `world_size` (= number of ranks), `n_elem`
|
||||||
|
(= element count of a single shard, f16-based).
|
||||||
|
- **Keyword arguments**: `cube_w`, `cube_h` (= cube-mesh dimensions).
|
||||||
|
Default 4×4 — aligned with `topology.yaml`'s `sip.cube_mesh` default.
|
||||||
|
- **Return**: a tuple in the order the kernel's positional arguments
|
||||||
|
expect.
|
||||||
|
|
||||||
|
When the backend calls `all_reduce`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
kernel_args_tuple = self._algo_module.kernel_args(
|
||||||
|
self._world_size, n_elem, cube_w=eff_cube_w, cube_h=eff_cube_h,
|
||||||
|
)
|
||||||
|
extra_args = (sip_rank, sip_topo_kind, sip_topo_w, sip_topo_h)
|
||||||
|
pending = self.ctx.launch(
|
||||||
|
self._merged["algorithm"], kernel_fn, tensor,
|
||||||
|
*kernel_args_tuple, *extra_args, _defer_wait=True,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
So the kernel's full positional argument list becomes: `(tensor_ptr,
|
||||||
|
*kernel_args_tuple, sip_rank, sip_topo_kind, sip_topo_w,
|
||||||
|
sip_topo_h)`, with `tl=...` injected as a keyword. The tuple length
|
||||||
|
and order returned by `kernel_args` must **match the kernel signature
|
||||||
|
1:1**.
|
||||||
|
|
||||||
|
### D3. Kernel signature — standardized 9 + tl arguments
|
||||||
|
|
||||||
|
Recommended signature:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def my_kernel(
|
||||||
|
t_ptr: int, # VA base of the row-wise-sharded tensor on this SIP
|
||||||
|
n_elem: int, # element count per cube tile (or per shard)
|
||||||
|
cube_w: int, # cube mesh width (from kernel_args)
|
||||||
|
cube_h: int, # cube mesh height (from kernel_args)
|
||||||
|
n_sips: int, # equal to world_size (rank = SIP, ADR-0024)
|
||||||
|
sip_rank: int, # this SIP's rank
|
||||||
|
sip_topo_kind: int, # result of TOPO_NAME_TO_KIND lookup
|
||||||
|
sip_topo_w: int, # SIP mesh width (0 for ring_1d)
|
||||||
|
sip_topo_h: int, # SIP mesh height (0 for ring_1d)
|
||||||
|
*, tl, # TLContext (auto-injected)
|
||||||
|
) -> None:
|
||||||
|
```
|
||||||
|
|
||||||
|
Even if `kernel_args` chose a different positional argument order, the
|
||||||
|
kernel's **last four positional arguments are always
|
||||||
|
`(sip_rank, sip_topo_kind, sip_topo_w, sip_topo_h)`** — the backend
|
||||||
|
appends them as `extra_args` (ADR-0047 D5). A custom algorithm must
|
||||||
|
accept these four, but a single-SIP algorithm may simply ignore them.
|
||||||
|
|
||||||
|
`tl` is injected via keyword — `RuntimeContext.launch` adds `tl=tl_ctx`
|
||||||
|
just before invoking the kernel. The signature therefore exposes `tl`
|
||||||
|
as keyword-only (`*, tl`) or as the trailing keyword parameter.
|
||||||
|
|
||||||
|
### D4. Kernel body — freedom and constraints
|
||||||
|
|
||||||
|
Available inside the kernel: every `tl.*` primitive from ADR-0046 D3.
|
||||||
|
|
||||||
|
Common patterns:
|
||||||
|
|
||||||
|
- `cube_id = tl.program_id(axis=1)` — this PE's cube index.
|
||||||
|
- `pe_addr = t_ptr + cube_id * nbytes` — per-cube VA of the tile.
|
||||||
|
- `acc = tl.load(pe_addr, shape=(n_elem,), dtype="f16")` — load local
|
||||||
|
data.
|
||||||
|
- `tl.send(dir=...)` / `tl.recv(dir=..., shape=, dtype=)` — IPCQ
|
||||||
|
collective.
|
||||||
|
- `acc = acc + recv` — TensorHandle arithmetic operators (ADR-0046 D4).
|
||||||
|
- `tl.store(pe_addr, acc)` — store the result.
|
||||||
|
|
||||||
|
The kernel body is plain Python — branching and loops are fine. But:
|
||||||
|
|
||||||
|
- No SimPy `yield` or `async` (ADR-0046 D1).
|
||||||
|
- No direct access to TensorHandle `.data` — the Phase 1 timing model
|
||||||
|
doesn't see data dependencies (ADR-0020's 2-pass separation).
|
||||||
|
- Kernel execution must be deterministic — the same input must produce
|
||||||
|
the same op sequence. No random or external IO.
|
||||||
|
|
||||||
|
### D5. SIP topology semantics — meaning of `sip_topo_kind`
|
||||||
|
|
||||||
|
The backend looks up `topology.yaml`'s `system.sips.topology` string
|
||||||
|
in the algorithm module's `TOPO_NAME_TO_KIND` and passes the integer
|
||||||
|
as `sip_topo_kind`. The algorithm then branches:
|
||||||
|
|
||||||
|
```python
|
||||||
|
if sip_topo_kind == SIP_TOPO_RING:
|
||||||
|
acc = _inter_sip_ring(...)
|
||||||
|
elif sip_topo_kind == SIP_TOPO_TORUS:
|
||||||
|
acc = _inter_sip_torus_2d(...)
|
||||||
|
elif sip_topo_kind == SIP_TOPO_MESH:
|
||||||
|
acc = _inter_sip_mesh_2d(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
Each topology branch communicates with peers via IPCQ direction names
|
||||||
|
(`"global_E"`, `"W"`, `"S"`, `"N"` …). Direction semantics are defined
|
||||||
|
in ADR-0023/0025; `configure_sfr_intercube_multisip` installs the IPCQ
|
||||||
|
neighbor table accordingly.
|
||||||
|
|
||||||
|
If a topology kind not supported by the algorithm appears, prefer an
|
||||||
|
explicit `raise ValueError(f"unsupported topology kind
|
||||||
|
{sip_topo_kind}")` over a silent no-op — fail fast on misconfiguration.
|
||||||
|
|
||||||
|
### D6. The `ccl.yaml` algorithm entry
|
||||||
|
|
||||||
|
The algorithm module is paired with a `ccl.yaml` entry (ADR-0023 D10 +
|
||||||
|
ADR-0047 D3):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
defaults:
|
||||||
|
algorithm: lrab_hierarchical_allreduce
|
||||||
|
n_elem: 8
|
||||||
|
|
||||||
|
algorithms:
|
||||||
|
lrab_hierarchical_allreduce:
|
||||||
|
module: kernbench.ccl.algorithms.lrab_hierarchical_allreduce
|
||||||
|
# optional: world_size override
|
||||||
|
# optional: per-algorithm parameters consumed by configure_sfr_intercube_multisip
|
||||||
|
```
|
||||||
|
|
||||||
|
- `module`: the full Python module path; `importlib.import_module`
|
||||||
|
consumes this string as-is.
|
||||||
|
- `world_size` (optional): when set, overrides the topology fallback
|
||||||
|
(ADR-0047 D2).
|
||||||
|
- Algorithm-specific parameters are consumed by
|
||||||
|
`configure_sfr_intercube_multisip`.
|
||||||
|
|
||||||
|
Workflow to add a new algorithm:
|
||||||
|
|
||||||
|
1. Write `src/kernbench/ccl/algorithms/<name>.py` following D1.
|
||||||
|
2. Add the entry under `algorithms` in `ccl.yaml`.
|
||||||
|
3. (If needed) extend `kernbench.ccl.sfr_config` with the SFR-install
|
||||||
|
branch.
|
||||||
|
4. Add tests (e.g., `tests/sccl/test_<name>.py`, extending the
|
||||||
|
ADR-0043 eval harness).
|
||||||
|
|
||||||
|
### D7. Legacy "rank = flat PE index" mode
|
||||||
|
|
||||||
|
The `world_size` override in `ccl.yaml`, surfaced by ADR-0047 D2, is
|
||||||
|
used by legacy "rank = flat PE index" tests. The algorithm module can
|
||||||
|
assume `n_sips=world_size` ranks even in this mode — the backend
|
||||||
|
maintains the rank↔(SIP, cube, PE) mapping, so no modal branching is
|
||||||
|
needed inside the algorithm body.
|
||||||
|
|
||||||
|
In single-cube workloads (where `cube_w=cube_h=1`), the algorithm must
|
||||||
|
skip mesh-based phases — see the
|
||||||
|
`single_cube = (cube_w == 1 and cube_h == 1)` pattern in
|
||||||
|
`lrab_hierarchical_allreduce.py`.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
### A1. Organize the algorithm module as a class (`class Allreduce: kernel(...)`)
|
||||||
|
|
||||||
|
Rejected. The Python module namespace already identifies an algorithm
|
||||||
|
(see ADR-0047 D3's `importlib.import_module`). A class wrapper adds
|
||||||
|
indirection without simplifying dispatch. Module-level free functions
|
||||||
|
plus a `kernel` alias are clean and obvious.
|
||||||
|
|
||||||
|
### A2. Type `kernel_args` with an explicit dataclass
|
||||||
|
|
||||||
|
Rejected (currently). Each algorithm normally has a different argument
|
||||||
|
count; forcing one dataclass would hurt cross-algorithm interchange.
|
||||||
|
The tuple return is simple and unpacks cleanly with the backend's
|
||||||
|
`*kernel_args_tuple`. If an algorithm wants stronger internal typing,
|
||||||
|
it may define its own NamedTuple.
|
||||||
|
|
||||||
|
### A3. Move SFR installation inside the algorithm module
|
||||||
|
|
||||||
|
Rejected. SFR installation
|
||||||
|
(`configure_sfr_intercube_multisip`) is a cross-module decision
|
||||||
|
combining topology + algorithm; `kernbench.ccl.sfr_config` is a more
|
||||||
|
natural home than the algorithm module itself. D6's "extend
|
||||||
|
sfr_config if needed" workflow keeps responsibility boundaries clear.
|
||||||
|
|
||||||
|
### A4. Auto-register algorithm names via a decorator (analogous to ADR-0045's `@bench`)
|
||||||
|
|
||||||
|
Rejected. Unlike benches, algorithms are already tied to `ccl.yaml`
|
||||||
|
entries; an additional registry would be redundant. The string mapping
|
||||||
|
in `module` is sufficient.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- ADR-0047 D3's one-line contract expands to a D1–D7 author-facing
|
||||||
|
guide; new algorithm signatures no longer need to be grep-derived.
|
||||||
|
- D3's standardized 9 + tl signature couples naturally with the
|
||||||
|
backend's `extra_args` append (ADR-0047 D5). It is explicit that
|
||||||
|
even single-SIP-only algorithms must accept the four `sip_*` trailing
|
||||||
|
arguments.
|
||||||
|
- D5's fail-loud recommendation means a `ccl.yaml` topology that the
|
||||||
|
algorithm doesn't support will surface as an explicit `ValueError`
|
||||||
|
rather than a silent wrong result.
|
||||||
|
- D6's step-by-step addition workflow makes clear how far a new
|
||||||
|
algorithm has to reach into sfr_config / tests / ccl.yaml.
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
# ADR-0051: Routing Helper API — `AddressResolver` + `PathRouter`
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted (2026-05-22).
|
||||||
|
|
||||||
|
Pins down every public API, argument, return value, and adjacency-graph
|
||||||
|
selection of the two helper classes (`AddressResolver`, `PathRouter`)
|
||||||
|
exposed by `policy/routing/router.py`. ADR-0002 defines routing
|
||||||
|
distance, ordering, and bypass rules, but **the helper API surface
|
||||||
|
itself** has had no ADR-level coverage.
|
||||||
|
|
||||||
|
## First action
|
||||||
|
|
||||||
|
### `AddressResolver(graph)`
|
||||||
|
|
||||||
|
On construction, caches two pieces of state:
|
||||||
|
|
||||||
|
1. `self._node_ids = set(graph.nodes)` — a set of all node ids for
|
||||||
|
lookup.
|
||||||
|
2. `self._hbm_slice_bytes = hbm_total_gb * (1 << 30) // slices_per_cube`
|
||||||
|
— derived from `graph.spec.cube.memory_map` (default `48 GB / 8
|
||||||
|
slices = 6 GB`). `resolve()` uses this value to decode `pe_id` from
|
||||||
|
an HBM PA's `hbm_offset`.
|
||||||
|
|
||||||
|
In short, **AddressResolver's first act is "precompute the full set of
|
||||||
|
node ids and the HBM slice size"**. It does not retain the graph
|
||||||
|
itself.
|
||||||
|
|
||||||
|
### `PathRouter(graph)`
|
||||||
|
|
||||||
|
On construction, **builds four separate adjacency graphs in one pass**:
|
||||||
|
|
||||||
|
1. `self._adj_all`: every edge (used for component-to-component
|
||||||
|
routing).
|
||||||
|
2. `self._adj`: edges with `kind != "command"` (PE DMA / generic data
|
||||||
|
paths).
|
||||||
|
3. `self._adj_mcpu_dma`: excludes
|
||||||
|
`_MCPU_DMA_EXCLUDE = {"pe_internal", "pe_to_router"}` (M_CPU DMA
|
||||||
|
must not pass through PE pipeline nodes).
|
||||||
|
4. `self._adj_local`: excludes the 8-element `_UCIE_KINDS` set (UCIe
|
||||||
|
would look like a zero-distance bus to Dijkstra, which would prefer
|
||||||
|
it over the mesh — for cube-local routing this must be avoided).
|
||||||
|
|
||||||
|
Each graph is a `defaultdict(list)` of `(neighbor, weight)`. The
|
||||||
|
weight is `edge.routing_weight_mm or edge.distance_mm`.
|
||||||
|
|
||||||
|
In short, **PathRouter's first act is "classify topology edges into
|
||||||
|
four policy-specific adjacency lists simultaneously"**. Each `find_*()`
|
||||||
|
call picks the appropriate graph and runs Dijkstra.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
`policy/routing/router.py` performs two responsibilities together:
|
||||||
|
|
||||||
|
- **Naming**: it is the sole owner of the topology naming convention
|
||||||
|
(`sip{S}.cube{C}.<comp>`, `sip{S}.io{I}.pcie_ep`, etc.). Components /
|
||||||
|
probe / IPCQ install / runtime API do not build node-id strings
|
||||||
|
themselves — they call helpers.
|
||||||
|
- **Path decisions**: policy separation by `edge.kind`. For the same
|
||||||
|
src→dst, different routing intents (PE DMA vs M_CPU DMA vs general
|
||||||
|
component routing) call for different adjacencies and so produce
|
||||||
|
different paths.
|
||||||
|
|
||||||
|
This helper API is widely consumed (probe.py / distributed.py /
|
||||||
|
install.py / various components / tests), yet **the exact signatures /
|
||||||
|
return semantics / adjacency picks** are not gathered in any ADR. This
|
||||||
|
ADR closes that gap.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### D1. `AddressResolver` exposes five public methods
|
||||||
|
|
||||||
|
#### D1.1. `resolve(addr: PhysAddr) -> str`
|
||||||
|
|
||||||
|
Translates a `PhysAddr` to a destination node id in the topology:
|
||||||
|
|
||||||
|
```
|
||||||
|
addr.kind == "hbm" → f"sip{s}.cube{d}.hbm_ctrl.pe{pe_id}"
|
||||||
|
where pe_id = addr.hbm_offset // self._hbm_slice_bytes (ADR-0017 D4/D9)
|
||||||
|
|
||||||
|
addr.kind == "pe_resource":
|
||||||
|
addr.unit_type == PE → f"sip{s}.cube{d}.pe{addr.pe_id}.pe_tcm"
|
||||||
|
addr.unit_type == SRAM → f"sip{s}.cube{d}.sram"
|
||||||
|
addr.unit_type == MCPU → f"sip{s}.cube{d}.m_cpu"
|
||||||
|
others → RoutingError("unsupported unit_type")
|
||||||
|
|
||||||
|
other kinds → RoutingError("unsupported address kind")
|
||||||
|
```
|
||||||
|
|
||||||
|
If the derived node id is not in `self._node_ids`, raises
|
||||||
|
`RoutingError(f"node {node_id} not found in topology")`. So even when
|
||||||
|
the address has valid syntax, an absent node in the topology
|
||||||
|
fails-loud.
|
||||||
|
|
||||||
|
#### D1.2. `find_m_cpu(sip, cube) -> str`
|
||||||
|
|
||||||
|
Returns `f"sip{sip}.cube{cube}.m_cpu"`; absent → `RoutingError`.
|
||||||
|
|
||||||
|
#### D1.3. `find_pcie_ep(sip, io_id="io0") -> str`
|
||||||
|
|
||||||
|
Returns `f"sip{sip}.{io_id}.pcie_ep"`; absent → `RoutingError`.
|
||||||
|
|
||||||
|
#### D1.4. `find_io_cpu(sip, io_id="io0") -> str`
|
||||||
|
|
||||||
|
Returns `f"sip{sip}.{io_id}.io_cpu"`; absent → `RoutingError`.
|
||||||
|
|
||||||
|
#### D1.5. `find_all_pcie_eps() -> list[str]`
|
||||||
|
|
||||||
|
All PCIE_EP node ids across all SIPs, sorted. Filtered by
|
||||||
|
`endswith(".pcie_ep")`. Cross-SIP IPCQ uses this when enumerating
|
||||||
|
PCIE_EPs.
|
||||||
|
|
||||||
|
This class is the sole owner of the naming convention
|
||||||
|
(`sip{S}.cube{C}.<comp>`, `sip{S}.{io_id}.<comp>`) — ADR-0015 D4.
|
||||||
|
The topology builder produces nodes with the same naming convention;
|
||||||
|
components never build node-id strings directly — they go through
|
||||||
|
these helpers.
|
||||||
|
|
||||||
|
### D2. `PathRouter`'s four adjacency graphs
|
||||||
|
|
||||||
|
Constructed in one pass. `edge.kind` drives policy:
|
||||||
|
|
||||||
|
| graph | excluded edge kinds | use case |
|
||||||
|
|-------------------|--------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------|
|
||||||
|
| `_adj_all` | (none) | M_CPU↔NOC command included, IO_CPU/M_CPU routes |
|
||||||
|
| `_adj` | `"command"` | PE DMA / generic data paths |
|
||||||
|
| `_adj_mcpu_dma` | `"pe_internal"`, `"pe_to_router"` | M_CPU DMA (skips PE pipeline) |
|
||||||
|
| `_adj_local` | `_UCIE_KINDS` (`ucie_internal`, `ucie_conn_to_router`, `router_to_ucie_conn`, `ucie_conn_to_noc`, `noc_to_ucie_conn`, `ucie_mesh`, `io_to_cube`, `cube_to_io`) | same-cube routing (UCIe bus excluded) |
|
||||||
|
|
||||||
|
Each graph is `dict[node_id, list[(neighbor, weight)]]` with weight =
|
||||||
|
`edge.routing_weight_mm or edge.distance_mm`. Excluding command edges
|
||||||
|
prevents them from influencing routing; isolating `_adj_local` keeps
|
||||||
|
UCIe's "zero-distance bus" from out-competing the mesh — consistent
|
||||||
|
with ADR-0017 D7's cross-PE-slice mesh-distance requirement.
|
||||||
|
|
||||||
|
### D3. `PathRouter` exposes six public methods (+ two backward-compat shims)
|
||||||
|
|
||||||
|
#### D3.1. `find_path(src_pe: str, dst_node: str) -> list[str]`
|
||||||
|
|
||||||
|
**PE DMA routing**. `src_pe` is a PE prefix (e.g.,
|
||||||
|
`"sip0.cube0.pe0"`); the function auto-prepends `.pe_dma`, making the
|
||||||
|
true start node `"sip0.cube0.pe0.pe_dma"`.
|
||||||
|
|
||||||
|
Adjacency depends on cube-locality (`_same_cube`):
|
||||||
|
|
||||||
|
- **Same-cube** (src and dst share `sip{S}.cube{C}.` prefix): uses
|
||||||
|
`_adj_local`. Excluding UCIe lets cross-PE-slice access pay accurate
|
||||||
|
mesh distance (ADR-0017 D7).
|
||||||
|
- **Cross-cube**: uses `_adj`. UCIe naturally becomes the right choice
|
||||||
|
for the cross-cube portion.
|
||||||
|
|
||||||
|
#### D3.2. `find_path_with_distance(src_pe, dst_node) -> tuple[list[str], float]`
|
||||||
|
|
||||||
|
Same adjacency policy as D3.1, but returns `(path, total_distance)`.
|
||||||
|
Used by probe and analysis tools that need the distance metric.
|
||||||
|
|
||||||
|
#### D3.3. `find_mcpu_dma_path(m_cpu_id: str, dst_hbm_id: str) -> list[str]`
|
||||||
|
|
||||||
|
**M_CPU DMA path**. Same cube → `_adj_local` (stay within the mesh);
|
||||||
|
different cube → `_adj_all` (cross via UCIe). The
|
||||||
|
`_MCPU_DMA_EXCLUDE` set ensures PE-pipeline nodes never appear on
|
||||||
|
M_CPU's routes.
|
||||||
|
|
||||||
|
#### D3.4. `find_memory_path(src: str, dst: str) -> list[str]`
|
||||||
|
|
||||||
|
Direct memory path like
|
||||||
|
`pcie_ep → io_noc → cube → router mesh → hbm_ctrl`. Uses
|
||||||
|
`_adj_mcpu_dma` to exclude `pe_internal` and `pe_to_router`, so
|
||||||
|
host-issued reads/writes never leak into the PE pipeline. Probe
|
||||||
|
(ADR-0049 D1's H2D/D2H cases) calls this directly.
|
||||||
|
|
||||||
|
#### D3.5. `find_node_path(src: str, dst: str) -> list[str]`
|
||||||
|
|
||||||
|
Generic routing between arbitrary nodes, **including command edges**
|
||||||
|
(via `_adj_all`). IoCpuComponent / MCpuComponent use this when they
|
||||||
|
need to route through M_CPU ↔ NOC command-kind links.
|
||||||
|
|
||||||
|
#### D3.6. Backward-compat shims
|
||||||
|
|
||||||
|
- `_dijkstra(start, goal) -> list[str]` — thin wrapper for
|
||||||
|
`_run_dijkstra(self._adj, …)`.
|
||||||
|
- `_dijkstra_with_dist(start, goal) -> tuple[list[str], float]` —
|
||||||
|
distance-aware variant.
|
||||||
|
|
||||||
|
Despite the underscore prefixes (suggesting internal API), existing
|
||||||
|
tests call these directly. New code should prefer D3.1–D3.5; these two
|
||||||
|
shims are deprecation candidates.
|
||||||
|
|
||||||
|
### D4. Dijkstra — single-source shortest path
|
||||||
|
|
||||||
|
`_run_dijkstra_with_dist(adj, start, goal)`:
|
||||||
|
|
||||||
|
- `heapq` priority queue.
|
||||||
|
- `best: dict[node, distance]` — best known distance to each node.
|
||||||
|
- `prev: dict[node, predecessor]` — for path reconstruction.
|
||||||
|
- Edge weight = `routing_weight_mm or distance_mm`. The separation
|
||||||
|
matters because UCIe (and a few others) declare an explicit
|
||||||
|
`routing_weight_mm` distinct from physical `distance_mm`.
|
||||||
|
|
||||||
|
`start == goal` short-circuits to `([start], 0.0)`. Unreachable target
|
||||||
|
→ `RoutingError(f"no path from {start} to {goal}")`.
|
||||||
|
|
||||||
|
The algorithm is **deterministic**: identical graph + start/goal gives
|
||||||
|
the same path, satisfying SPEC R1 ("routing MUST be deterministic").
|
||||||
|
Tie-breaks follow `heapq`'s push order (Python list order is
|
||||||
|
deterministic).
|
||||||
|
|
||||||
|
### D5. Single-owner principle for helper-API decisions
|
||||||
|
|
||||||
|
The following decisions live only inside router.py:
|
||||||
|
|
||||||
|
- Naming convention: `sip{S}.cube{C}.<comp>`,
|
||||||
|
`sip{S}.{io_id}.<comp>`,
|
||||||
|
`sip{S}.cube{C}.hbm_ctrl.pe{pe_id}`.
|
||||||
|
- Adjacency policy: which edge kinds belong to which graph.
|
||||||
|
- Algorithm for recovering PE id from an HBM slice size.
|
||||||
|
- Dijkstra weight selection
|
||||||
|
(`routing_weight_mm or distance_mm`).
|
||||||
|
|
||||||
|
Breaking single ownership (e.g., a component starting to build
|
||||||
|
`f"sip{s}..."` itself) would explode the blast radius of naming-
|
||||||
|
convention changes. This aligns with ADR-0015 D4.
|
||||||
|
|
||||||
|
### D6. Consumers of the helper API
|
||||||
|
|
||||||
|
Methods listed in this ADR are called from (current corpus):
|
||||||
|
|
||||||
|
- `probes/probe.py` (ADR-0049): `find_pcie_ep`, `find_io_cpu`,
|
||||||
|
`find_m_cpu`, `find_node_path`, `find_mcpu_dma_path`,
|
||||||
|
`find_memory_path`, `find_path`, `resolve`.
|
||||||
|
- `runtime_api/distributed.py` (ADR-0047): indirectly (engine-internal
|
||||||
|
routing).
|
||||||
|
- `ccl/install.py` (ADR-0023): `find_all_pcie_eps`, `resolve`.
|
||||||
|
- `sim_engine/event_log.py`: like probe — `find_pcie_ep`,
|
||||||
|
`find_memory_path`.
|
||||||
|
- `components/builtin/m_cpu.py`, `components/builtin/io_cpu.py`:
|
||||||
|
`find_node_path`, `find_mcpu_dma_path`.
|
||||||
|
- Tests (test_routing.py, test_cross_sip_routing.py, …): most of
|
||||||
|
D3.1–D3.5.
|
||||||
|
|
||||||
|
When a new consumer arrives, D1/D3 act as a first-pass guide on
|
||||||
|
whether an existing method matches the intent or a new one is needed.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
### A1. One adjacency graph + per-call edge-kind filtering
|
||||||
|
|
||||||
|
Rejected. Re-filtering the graph on every `find_*()` call hurts
|
||||||
|
Dijkstra cache locality. Constructing four graphs in one pass (D2)
|
||||||
|
has modest memory cost (edges ≤ a few × 10⁴), and selection happens
|
||||||
|
in O(1) at call time.
|
||||||
|
|
||||||
|
### A2. Drive adjacency separation by separate edge metadata rather than `kind`
|
||||||
|
|
||||||
|
Rejected. `edge.kind` is already assigned by the topology builder
|
||||||
|
(ADR-0015 D4 + ADR-0017); a parallel metadata field would force
|
||||||
|
synchronization between two systems.
|
||||||
|
|
||||||
|
### A3. Use BFS with uniform weights instead of Dijkstra
|
||||||
|
|
||||||
|
Rejected. With per-edge `routing_weight_mm` (mesh link / UCIe /
|
||||||
|
IO-internal), BFS minimizes hop count rather than total
|
||||||
|
latency/distance. SPEC R1 + R2 require deterministic and accurate
|
||||||
|
routing, which BFS does not deliver.
|
||||||
|
|
||||||
|
### A4. Express the helper API as module functions instead of classes
|
||||||
|
|
||||||
|
Rejected. Each class
|
||||||
|
(`AddressResolver`, `PathRouter`) maintains caches
|
||||||
|
(`_node_ids`, `_hbm_slice_bytes`, four adjacency graphs) reused across
|
||||||
|
many routing queries on the same graph. Module functions would have
|
||||||
|
to rebuild state per call or go global, hurting safety and
|
||||||
|
performance.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- When components / probe / IPCQ install / runtime API all go through
|
||||||
|
router.py helpers, a naming-convention change (e.g., `.io0.` →
|
||||||
|
`.iochiplet0.`) is a one-file edit (D5).
|
||||||
|
- D2's four-graph split is now ADR-locked, so when a new edge kind is
|
||||||
|
added (e.g., a new inter-die UCIe-link kind), the right adjacency
|
||||||
|
category is decided explicitly rather than by default.
|
||||||
|
- D3.1's same-cube vs cross-cube branching (ADR-0017 D7) is explicit,
|
||||||
|
so anyone changing routing knows which adjacency to touch.
|
||||||
|
- D6's consumer list bounds PR-review scope for helper-API changes,
|
||||||
|
and the backward-compat shims (D3.6) are flagged as deprecation
|
||||||
|
candidates.
|
||||||
@@ -0,0 +1,371 @@
|
|||||||
|
# ADR-0052: OpLog + MemoryStore Schemas — sim_engine internals
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted (2026-05-22).
|
||||||
|
|
||||||
|
Pins down the `OpRecord` schema and the `record_start` / `record_end` /
|
||||||
|
`record_copy` behavior in `sim_engine/op_log.py`, plus the
|
||||||
|
(space, addr) namespace and read/write semantics of `MemoryStore` in
|
||||||
|
`sim_engine/memory_store.py`. ADR-0020 (2-pass data execution) declares
|
||||||
|
that these two facilities exist, but **the precise record fields and
|
||||||
|
semantics** had no ADR-level coverage, and several recent ADRs
|
||||||
|
(ADR-0046 D3.2's `tl.store` visibility, ADR-0023 D9's IPCQ copy
|
||||||
|
record) depend on these semantics.
|
||||||
|
|
||||||
|
## First action
|
||||||
|
|
||||||
|
### `OpLogger(memory_store=None)`
|
||||||
|
|
||||||
|
On construction, initialize three fields:
|
||||||
|
|
||||||
|
1. `self._records: list[OpRecord] = []` — accumulated records.
|
||||||
|
2. `self._pending: dict[int, dict] = {}` — partial records keyed by
|
||||||
|
`id(msg)` (created at `record_start`, completed at `record_end`).
|
||||||
|
3. `self._memory_store = memory_store` — optional MemoryStore
|
||||||
|
reference. Used to capture math-op input snapshots and dma_write
|
||||||
|
HBM-source snapshots.
|
||||||
|
|
||||||
|
Records and pending are empty; the `record_*` calls accumulate data
|
||||||
|
over time.
|
||||||
|
|
||||||
|
### `MemoryStore()`
|
||||||
|
|
||||||
|
On construction, initialize a single field:
|
||||||
|
`self._storage: dict[str, dict[int, np.ndarray]] = {}` — a two-level
|
||||||
|
dict (`space → addr → ndarray`). Inner dicts are created lazily as new
|
||||||
|
spaces appear.
|
||||||
|
|
||||||
|
In short, **both facilities' first act is "set up an empty accumulator
|
||||||
|
buffer plus a sparse, per-space dict"**. The first record / write
|
||||||
|
fills the fields when it arrives.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
ADR-0020 D2/D5/D7 (2-pass data execution) declares:
|
||||||
|
|
||||||
|
- During Phase 1 (timing), `ComponentBase._on_process_start/end` hooks
|
||||||
|
call `OpLogger.record_start/end`, recording the time and metadata of
|
||||||
|
every data op.
|
||||||
|
- Phase 2 (data) replays the op log in `t_start` order to compute real
|
||||||
|
data.
|
||||||
|
- Data payloads live in `MemoryStore`, keyed by (space, addr).
|
||||||
|
|
||||||
|
Subsequent ADRs (ADR-0023 D9's IPCQ atomic write, ADR-0027's Megatron
|
||||||
|
TP scratch-overwrite avoidance, ADR-0046 D3.2's `tl.store` visibility)
|
||||||
|
depend on op_log and MemoryStore behavior, but **the exact record
|
||||||
|
fields / space names / snapshot timing** are only discoverable via
|
||||||
|
source grep. This ADR codifies them.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### D1. `OpRecord` schema — seven fields
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass
|
||||||
|
class OpRecord:
|
||||||
|
t_start: float
|
||||||
|
t_end: float
|
||||||
|
component_id: str
|
||||||
|
op_kind: str # "memory" | "gemm" | "math" | "unknown"
|
||||||
|
op_name: str # e.g. "dma_read", "gemm_f16", "exp",
|
||||||
|
# "TileToken/DMA_READ", "composite_gemm",
|
||||||
|
# "ipcq_copy"
|
||||||
|
params: dict[str, Any]
|
||||||
|
dependency_ids: list[int] = field(default_factory=list)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`t_start` / `t_end`**: SimPy time (float ns). `t_start` is when the
|
||||||
|
component begins the op; `t_end` is completion. Duration =
|
||||||
|
`t_end - t_start`.
|
||||||
|
- **`component_id`**: the node id where the op occurred (e.g.,
|
||||||
|
`"sip0.cube0.pe0.pe_dma"`).
|
||||||
|
- **`op_kind`**: one of four. Phase 2 DataExecutor branches on this.
|
||||||
|
- **`op_name`**: a debug/analysis-friendly name. For a TileToken,
|
||||||
|
expands to `"TileToken/{stage_type}"` (e.g.,
|
||||||
|
`"TileToken/DMA_READ"`) to disambiguate stages.
|
||||||
|
- **`params`**: op-specific metadata dict (see D3).
|
||||||
|
- **`dependency_ids`**: currently unused (default `[]`). Reserved for
|
||||||
|
future cross-op dependency tracking.
|
||||||
|
|
||||||
|
### D2. `OpLogger.records` — guaranteed `t_start` sort
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def records(self) -> list[OpRecord]:
|
||||||
|
self._records.sort(key=lambda r: r.t_start)
|
||||||
|
return self._records
|
||||||
|
```
|
||||||
|
|
||||||
|
A stable sort by `t_start` runs on each access. Records with the same
|
||||||
|
`t_start` preserve insertion order. Aligns with ADR-0020 D5's
|
||||||
|
"t_start stable ordering" requirement.
|
||||||
|
|
||||||
|
Phase 2 DataExecutor always accesses via the `records` property, so
|
||||||
|
even when `record_end` calls arrive out of `t_start` order (e.g., a
|
||||||
|
short op started later but finished earlier), the sequence handed to
|
||||||
|
Phase 2 is consistent.
|
||||||
|
|
||||||
|
### D3. `params` schema per `op_name` (matrix from `_extract_op_info`)
|
||||||
|
|
||||||
|
#### D3.1. `op_kind="memory", op_name="dma_read"` (DmaReadCmd)
|
||||||
|
|
||||||
|
```python
|
||||||
|
{"src_addr": int, "nbytes": int, "handle_id": str}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### D3.2. `op_kind="memory", op_name="dma_write"` (DmaWriteCmd)
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"src_space": str, # handle.space ("tcm"|"hbm"|"sram"), default "tcm"
|
||||||
|
"src_addr": int, # handle.addr
|
||||||
|
"shape": tuple, "dtype": str,
|
||||||
|
"dst_space": "hbm", # DmaWrite always targets HBM
|
||||||
|
"dst_addr": int,
|
||||||
|
"nbytes": int,
|
||||||
|
"handle_id": str,
|
||||||
|
# When src_space == "hbm" at record_end, a snapshot is added (D4)
|
||||||
|
"snapshot": np.ndarray | None,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### D3.3. `op_kind="gemm", op_name=f"gemm_{dtype_a}"` (GemmCmd)
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"src_a_addr": int, "src_b_addr": int, "dst_addr": int,
|
||||||
|
"shape_a": tuple, "shape_b": tuple, "shape_out": tuple,
|
||||||
|
"dtype_in": str, "dtype_out": str,
|
||||||
|
"m": int, "k": int, "n": int,
|
||||||
|
# ADR-0027: per-operand + output spaces preserved
|
||||||
|
"src_a_space": str, "src_b_space": str, "dst_space": str,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### D3.4. `op_kind="math", op_name=msg.op` (MathCmd; op = "exp", "sum", "add", "where", …)
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"input_addrs": list[int], # addrs of input handles
|
||||||
|
"input_shapes": list[tuple],
|
||||||
|
"input_spaces": list[str],
|
||||||
|
"input_dtypes": list[str],
|
||||||
|
"dst_addr": int, "dst_space": str,
|
||||||
|
"shape_out": tuple, "dtype": str,
|
||||||
|
"axis": int | None, # only meaningful for reductions
|
||||||
|
# All inputs get snapshots at record_end (D4)
|
||||||
|
"input_snapshots": list[np.ndarray | None],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### D3.5. `op_kind="gemm" or "math", op_name=f"composite_{op}"` (CompositeCmd)
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"op": str, # "gemm" | "math"
|
||||||
|
"out_addr": int, "out_nbytes": int,
|
||||||
|
# If op == "gemm", same fields as GemmCmd are added:
|
||||||
|
"src_a_addr": int, "src_b_addr": int,
|
||||||
|
"shape_a": tuple, "shape_b": tuple,
|
||||||
|
"dtype_in": str, "dtype_out": str,
|
||||||
|
"src_a_space": str, "src_b_space": str,
|
||||||
|
"dst_space": "hbm", "dst_addr": int, # = out_addr
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If `op == "gemm"`, `op_kind = "gemm"`; otherwise `"math"`. An alias so
|
||||||
|
Phase 2 replays composite-gemm on the same path as `GemmCmd`.
|
||||||
|
|
||||||
|
#### D3.6. `op_kind="memory", op_name="ipcq_copy"` (record_copy path)
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"src_space": str, "src_addr": int,
|
||||||
|
"dst_space": str, "dst_addr": int,
|
||||||
|
"shape": tuple, "dtype": str, "nbytes": int,
|
||||||
|
"snapshot": np.ndarray | None, # passed by caller; if None, record_copy reads fresh
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`PE_DMA._handle_ipcq_inbound` (ADR-0023 D9) emits this record so Phase
|
||||||
|
2 can replay the IPCQ slot's inbound copy. It bypasses
|
||||||
|
`record_start` / `record_end` and pushes directly via `record_copy()`.
|
||||||
|
|
||||||
|
#### D3.7. `op_kind="unknown", op_name=type(msg).__name__`
|
||||||
|
|
||||||
|
Fallback for messages `_extract_op_info` doesn't recognize. `params =
|
||||||
|
{}`. If DataExecutor encounters this kind, it skips — Phase 2 replay
|
||||||
|
is unaffected.
|
||||||
|
|
||||||
|
### D4. Snapshot capture timing
|
||||||
|
|
||||||
|
When `OpLogger._memory_store` is set, `record_end` performs:
|
||||||
|
|
||||||
|
- **Math op**: read every input
|
||||||
|
(addr/shape/space/dtype) from `self._memory_store.read(...)` and
|
||||||
|
attach an ndarray copy to `params["input_snapshots"]`. Read failure
|
||||||
|
→ `None`.
|
||||||
|
- **`dma_write` op**: snapshot the source **only if `src_space ==
|
||||||
|
"hbm"`** and attach to `params["snapshot"]`. TCM (PE scratch)
|
||||||
|
sources are **deliberately skipped** — TCM is repopulated by Phase 2
|
||||||
|
math/gemm replay, and a Phase-1-time snapshot would capture a
|
||||||
|
previous kernel's stale value (ADR-0027 postmortem: TP gemm →
|
||||||
|
all_reduce race).
|
||||||
|
- **`ipcq_copy`**: the caller passes the in-flight snapshot via
|
||||||
|
`snapshot=token.data`. If absent, `record_copy` attempts a fresh
|
||||||
|
read from MemoryStore.
|
||||||
|
|
||||||
|
Snapshots are taken with `.copy()` (fresh allocation), making them
|
||||||
|
safe against later storage mutation. This is the foundation of
|
||||||
|
ADR-0027's "cross-PE Phase 2 ordering" race-avoidance.
|
||||||
|
|
||||||
|
When `memory_store` is `None` (Phase 1 timing-only mode), all
|
||||||
|
snapshot steps are skipped. Only the timing portion of the record is
|
||||||
|
preserved; data replay is unavailable.
|
||||||
|
|
||||||
|
### D5. TileToken handling — `record_start` captures stage info
|
||||||
|
|
||||||
|
ADR-0014 D6's self-routing tile token (pipeline mode) may have already
|
||||||
|
advanced its `stage_idx` by the time `record_end` runs (the TileToken
|
||||||
|
caches the next stage's params as it moves to the next component).
|
||||||
|
Therefore:
|
||||||
|
|
||||||
|
`record_start` pre-saves the following in `pending[id(msg)]["snap"]`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
snap["stage_type"] = stage.stage_type.name # "DMA_READ", "GEMM", ...
|
||||||
|
snap["stage_params"] = dict(stage.params) # copy of params at start time
|
||||||
|
```
|
||||||
|
|
||||||
|
`record_end` retrieves this snap and merges into params:
|
||||||
|
|
||||||
|
- Adds `params["stage_type"]` to final params.
|
||||||
|
- Merges `stage_params` keys (keeps existing values if any).
|
||||||
|
- If `op_name == "TileToken"`, rewrites it to
|
||||||
|
`f"TileToken/{stage_type}"` (e.g., `"TileToken/DMA_READ"`),
|
||||||
|
disambiguating different stages emitted by the same component.
|
||||||
|
|
||||||
|
Thanks to this, DMA_READ vs DMA_WRITE, FETCH vs STORE coming from the
|
||||||
|
same component (e.g., pe_dma) are distinguishable in reports.
|
||||||
|
|
||||||
|
### D6. `MemoryStore` — two-level (space, addr) dict
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MemoryStore:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._storage: dict[str, dict[int, np.ndarray]] = {}
|
||||||
|
|
||||||
|
def write(self, space, addr, data): self._storage[space][addr] = data
|
||||||
|
def read(self, space, addr, shape=None, dtype=None) -> np.ndarray: ...
|
||||||
|
def has(self, space, addr) -> bool: ...
|
||||||
|
def snapshot(self) -> MemoryStore: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### D6.1. Space namespace
|
||||||
|
|
||||||
|
A string key. Standard values:
|
||||||
|
|
||||||
|
- `"hbm"`: HBM data (deploy_tensor + Phase 2 dma_write results).
|
||||||
|
- `"tcm"`: PE-local TCM (Phase 2 math/gemm output).
|
||||||
|
- `"sram"`: cube-level SRAM (ADR-0023 D9.7's IPCQ slot tier).
|
||||||
|
|
||||||
|
Other spaces (e.g., `"reg"`) are allowed — `_storage` is a lazy dict
|
||||||
|
that creates a new space when `write` first touches it.
|
||||||
|
|
||||||
|
#### D6.2. Address keying
|
||||||
|
|
||||||
|
`addr` is an integer. It may be a **physical address (PA) or a virtual
|
||||||
|
address (VA)** — `MemoryStore` itself doesn't know address-space
|
||||||
|
semantics; it just uses them as keys. Phase 1's `MemoryWriteMsg`
|
||||||
|
writes both PA and VA
|
||||||
|
(`_create_tensor` zero-inits at PA and at the VA base too); Phase 2
|
||||||
|
reads/writes via the addresses captured by op_log.
|
||||||
|
|
||||||
|
The caller decides `addr`'s meaning — `MemoryStore` provides only
|
||||||
|
lookup.
|
||||||
|
|
||||||
|
#### D6.3. read/write semantics — reference store (no copy)
|
||||||
|
|
||||||
|
`write(space, addr, data)`: stores the ndarray reference. **No copy.**
|
||||||
|
If the caller later mutates the same ndarray, the stored value
|
||||||
|
changes.
|
||||||
|
|
||||||
|
`read(space, addr, shape=None, dtype=None)`: returns the stored
|
||||||
|
ndarray reference. If `shape`/`dtype` are provided:
|
||||||
|
|
||||||
|
- `dtype != stored.dtype`: `arr.view(np_dtype)` reinterprets as a
|
||||||
|
view (no copy).
|
||||||
|
- `shape != stored.shape`: if `nbytes` matches, `arr.reshape(shape)`
|
||||||
|
is a view.
|
||||||
|
- `nbytes` mismatch → `ValueError`.
|
||||||
|
|
||||||
|
To detach the data, the caller must call `arr.copy()`. ADR-0027's
|
||||||
|
race-avoidance requires explicit `.copy()` in op_log snapshot steps
|
||||||
|
for exactly this reason.
|
||||||
|
|
||||||
|
#### D6.4. `has(space, addr) -> bool`
|
||||||
|
|
||||||
|
Existence check; does not materialize data.
|
||||||
|
|
||||||
|
#### D6.5. `snapshot() -> MemoryStore`
|
||||||
|
|
||||||
|
Shallow copy. Creates a new instance of inner dicts but shares
|
||||||
|
ndarray references. Used at Phase 2 init to fork from Phase 1's
|
||||||
|
store, so Phase 2 mutations don't affect Phase 1's remaining
|
||||||
|
consumers.
|
||||||
|
|
||||||
|
### D7. op_log assumes a single-threaded SimPy
|
||||||
|
|
||||||
|
`OpLogger`'s `_records` and `_pending` are lock-free. SimPy is
|
||||||
|
single-threaded, so nothing else can intrude between `record_start`
|
||||||
|
and `record_end` for the same message.
|
||||||
|
|
||||||
|
When multi-process kernbench (ADR-0047 D6) arrives, OpLogger must be
|
||||||
|
split per process — one OpLogger instance cannot receive records from
|
||||||
|
multiple processes.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
### A1. Externalize op_log to SQLite / parquet
|
||||||
|
|
||||||
|
Rejected (currently). The in-memory list minimizes Phase 1 → Phase 2
|
||||||
|
hand-off latency. Externalization makes sense for long-running batch
|
||||||
|
runs but adds overhead for the current single-run workload.
|
||||||
|
|
||||||
|
### A2. Capture snapshots at `record_start`
|
||||||
|
|
||||||
|
Rejected. At `record_start`, inputs are often not yet populated (e.g.,
|
||||||
|
a math op's input is the output of a just-issued previous op).
|
||||||
|
`record_end` is the correct point.
|
||||||
|
|
||||||
|
### A3. Per-component MemoryStore
|
||||||
|
|
||||||
|
Rejected. The (space, addr) key already disambiguates effectively, and
|
||||||
|
splitting per component would complicate cross-PE IPCQ copy (ADR-0023
|
||||||
|
D9), which needs access to both source and destination stores.
|
||||||
|
|
||||||
|
### A4. Explicit dependency edges in op_log
|
||||||
|
|
||||||
|
Partially adopted. The `dependency_ids` field exists on `OpRecord` but
|
||||||
|
is currently unused (D1). Phase 2 DataExecutor orders via `t_start` +
|
||||||
|
a secondary sort (memory ops before math at the same `t_start`). When
|
||||||
|
an explicit dependency graph is required, this field is the home.
|
||||||
|
Current ordering rules are sufficient, so it remains unused.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- ADR-0020's op_log / MemoryStore declarations are expanded into the
|
||||||
|
concrete D1–D6 schemas, so writing/modifying Phase 2 DataExecutor
|
||||||
|
doesn't need source-grep to learn field semantics.
|
||||||
|
- D3's per-`op_name` params matrix makes adding new ops (e.g., a new
|
||||||
|
reduction type) a question of branching in `_extract_op_info`.
|
||||||
|
- D4's per-op snapshot policy (math = input snapshot, dma_write =
|
||||||
|
HBM-only snapshot) is ADR-locked, so ADR-0027's race-avoidance
|
||||||
|
decision won't silently regress on future refactors.
|
||||||
|
- D6.3's reference-store semantics are explicit, putting mutation
|
||||||
|
safety on the caller. ADR-0027's explicit `.copy()` pattern is
|
||||||
|
justified.
|
||||||
|
- D7's single-thread assumption is recorded, so multi-process
|
||||||
|
kernbench (ADR-0047 D6's supersession candidate) will need OpLogger
|
||||||
|
separation when introduced.
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
# ADR-0053: Topology Builder + Visualizer Algorithms
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted (2026-05-22).
|
||||||
|
|
||||||
|
Pins down the key algorithmic choices of the topology compile and
|
||||||
|
visualization pipeline jointly implemented by `topology/builder.py`,
|
||||||
|
`topology/mesh_gen.py`, and `topology/visualizer.py` —
|
||||||
|
placement-driven router attachment, mesh auto-layout, the source_hash
|
||||||
|
cache, view projections, and SVG rendering. ADR-0006 defines the
|
||||||
|
high-level intent of topology compilation (compiled topology, distance
|
||||||
|
extraction, automatic diagram generation), but **which algorithms the
|
||||||
|
builder actually uses** was only discoverable via source grep.
|
||||||
|
|
||||||
|
## First action
|
||||||
|
|
||||||
|
When `resolve_topology(path_str)` is called, four steps run in order:
|
||||||
|
|
||||||
|
1. **Path validation** (`builder.py::resolve_topology`):
|
||||||
|
`Path(path_str).expanduser().resolve()`, existence check, file
|
||||||
|
check. Failure → `FileNotFoundError` or `ValueError`.
|
||||||
|
2. **YAML parsing** (`_read_spec`): `yaml.safe_load`. Parse errors
|
||||||
|
yield a `ValueError` with line/column. Non-dict roots are
|
||||||
|
rejected.
|
||||||
|
3. **Auto-generate the mesh** (`mesh_gen.ensure_mesh_file`): create or
|
||||||
|
reuse a `cube_mesh.yaml` next to the topology file. Cache hit on
|
||||||
|
matching source_hash; miss triggers regeneration. This step decides
|
||||||
|
the cube NoC's router grid and attachment information.
|
||||||
|
4. **Compile the graph** (`_compile_graph`): system → IO chiplets →
|
||||||
|
cubes → inter-cube edges → IO↔cube edges → system↔IO edges, then
|
||||||
|
build four view projections (system, sip, cube, pe) and wrap into
|
||||||
|
a `TopologyGraph`.
|
||||||
|
|
||||||
|
In short, **topology compilation's first act is "read topology.yaml as
|
||||||
|
a dict, create/validate cube_mesh.yaml in the same directory, then
|
||||||
|
build the flat graph + 4-view projection in system → sip → cube → pe
|
||||||
|
order"**.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
`topology/` package responsibilities:
|
||||||
|
|
||||||
|
- **builder.py** (1207 lines): turns topology.yaml into a
|
||||||
|
`TopologyGraph` (nodes + edges + 4 view projections).
|
||||||
|
- **mesh_gen.py** (305 lines): auto-decides the cube NoC's router
|
||||||
|
grid and PE/UCIe/M_CPU/SRAM attachment positions and caches them in
|
||||||
|
`cube_mesh.yaml`.
|
||||||
|
- **visualizer.py** (887 lines): generates four SVG diagrams (system /
|
||||||
|
sip / cube / pe) from a `TopologyGraph`.
|
||||||
|
|
||||||
|
ADR-0006 makes the high-level decision that "the result of topology
|
||||||
|
compilation is the single source for distance metadata and diagram
|
||||||
|
generation", but specific algorithms (e.g., placement-driven nearest-
|
||||||
|
router attachment, the HBM exclusion zone, which fields in source_hash
|
||||||
|
trigger regeneration) are not in any ADR.
|
||||||
|
|
||||||
|
In particular, these decisions are absent at ADR level:
|
||||||
|
|
||||||
|
- Why is mesh_gen cached in a separate file (`cube_mesh.yaml`)?
|
||||||
|
- Which fields are in source_hash, and which changes force
|
||||||
|
regeneration?
|
||||||
|
- Why placement coordinates in mm rather than cube coordinates?
|
||||||
|
- How are the HBM exclusion zone and UCIe N/S/E/W distribution
|
||||||
|
decided inside the mesh?
|
||||||
|
- What is the abstraction-level difference among the four view
|
||||||
|
projections (system/sip/cube/pe)?
|
||||||
|
|
||||||
|
This ADR captures these decisions in one place.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### D1. Compile pipeline — six stages
|
||||||
|
|
||||||
|
`_compile_graph(spec)`:
|
||||||
|
|
||||||
|
1. **System nodes** (`_instantiate_system`): add system-level nodes
|
||||||
|
like `fabric.switch0` and the host CPU.
|
||||||
|
2. **Per-SIP loop** (`for sip_id in range(system.sips.count)`):
|
||||||
|
- **IO chiplets** (`_instantiate_io_chiplets`): create pcie_ep /
|
||||||
|
io_cpu / io_noc / io_ucie PHYs / conn nodes and their bidirectional
|
||||||
|
internal edges.
|
||||||
|
- **Cube instantiation** (`_instantiate_cube`): using
|
||||||
|
cube_mesh.yaml's router grid, instantiate cube routers, PE
|
||||||
|
sub-components (pe_cpu, pe_dma, pe_fetch_store, pe_gemm, pe_math,
|
||||||
|
pe_mmu, pe_tcm, pe_scheduler, pe_ipcq), m_cpu, sram, hbm_ctrl,
|
||||||
|
and their internal edges.
|
||||||
|
- **Inter-cube edges** (`_add_inter_cube_edges`): the UCIe
|
||||||
|
N/S/E/W mesh edges.
|
||||||
|
- **IO ↔ cube edges** (`_add_io_to_cube_edges`): connect io_noc to
|
||||||
|
each cube's edge UCIe phy.
|
||||||
|
3. **Switch ↔ IO edges** (`_add_system_to_io_edges`): bidirectional
|
||||||
|
edges between `fabric.switch0` and each SIP's `pcie_ep` (the
|
||||||
|
cross-SIP IPCQ path of ADR-0038 D3 + ADR-0010).
|
||||||
|
4. **Build four view projections**:
|
||||||
|
- `_build_system_view(spec)` — Tray level: SIPs and the system
|
||||||
|
switch.
|
||||||
|
- `_build_sip_view(spec)` — inside one SIP: cube mesh + IO
|
||||||
|
chiplet.
|
||||||
|
- `_build_cube_view(spec)` — inside one cube: router grid + PE /
|
||||||
|
M_CPU / SRAM / HBM_CTRL attachments.
|
||||||
|
- `_build_pe_view(spec)` — inside one PE: nine sub-components +
|
||||||
|
internal edges (pe_internal kind).
|
||||||
|
5. **Return `TopologyGraph`**: `TopologyGraph(spec, nodes, edges,
|
||||||
|
system_view, sip_view, cube_view, pe_view)`.
|
||||||
|
|
||||||
|
The six stages are **ordered for a reason**: only after cubes exist
|
||||||
|
do inter-cube edges have valid src/dst, and IO chiplets must precede
|
||||||
|
the IO ↔ cube edges that reference them. New node types must slot in
|
||||||
|
the right spot.
|
||||||
|
|
||||||
|
### D2. `cube_mesh.yaml` — a separate file with a source_hash cache
|
||||||
|
|
||||||
|
`mesh_gen.ensure_mesh_file(cube_spec, mesh_path)`:
|
||||||
|
|
||||||
|
1. Compute `source_hash = _compute_source_hash(cube_spec)` from these
|
||||||
|
input fields:
|
||||||
|
- `geometry` (cube_mm.w/h …).
|
||||||
|
- `pe_layout` (corners, pe_per_corner).
|
||||||
|
- `ucie.n_connections`.
|
||||||
|
- `memory_map.hbm_mapping_mode`.
|
||||||
|
- `placement` (m_cpu/sram pos_mm).
|
||||||
|
2. If `mesh_path` (= `cube_mesh.yaml` next to topology.yaml) exists
|
||||||
|
and `existing.source_hash == source_hash`, reuse it (cache hit).
|
||||||
|
3. Otherwise, generate a new mesh via
|
||||||
|
`_generate_mesh(cube_spec, source_hash)` and write to yaml.
|
||||||
|
|
||||||
|
Caching as a separate file because:
|
||||||
|
|
||||||
|
- Mesh generation involves nontrivial PE/UCIe/router attachment math
|
||||||
|
and is too expensive to redo every time.
|
||||||
|
- Multiple runs with the same cube spec must guarantee an identical
|
||||||
|
mesh.
|
||||||
|
- The resulting mesh is itself an inspectable / debuggable artifact.
|
||||||
|
|
||||||
|
The five fields listed in source_hash are the ones that determine
|
||||||
|
mesh shape; other changes (e.g., bandwidth, overhead_ns) do not
|
||||||
|
trigger mesh regeneration.
|
||||||
|
|
||||||
|
### D3. Cube NoC mesh auto-layout
|
||||||
|
|
||||||
|
`_generate_mesh(cube_spec)`:
|
||||||
|
|
||||||
|
#### D3.1. Rows / columns
|
||||||
|
|
||||||
|
- `pe_positions = _corner_pe_positions(cube_w, cube_h)`: PE-center
|
||||||
|
coordinates (mm) per corner (NW/NE/SW/SE). Hardcoded patterns like
|
||||||
|
`(1.5, 1.5)` and `(cube_w-1.5, cube_h-1.5)`; with `pe_per_corner=2`,
|
||||||
|
each corner has two PE positions.
|
||||||
|
- `col_xs = _compute_col_positions(...)`: union of PE x-coordinates,
|
||||||
|
plus relay columns inserted when any gap exceeds
|
||||||
|
`max_spacing = 3.0 mm`.
|
||||||
|
- `row_ys, rows_per_half = _compute_row_positions(cube_h,
|
||||||
|
n_connections, pe_positions)`:
|
||||||
|
- `n_conn = max(n_connections, 2)` (hot-path minimum).
|
||||||
|
- `rows_per_half = ceil(n_conn / 2)`.
|
||||||
|
- Top half + two HBM rows + bottom half. HBM sits at
|
||||||
|
`(cube_h/2 - 1.5, cube_h/2 + 1.5)`. The gap between PE rows and
|
||||||
|
HBM rows is `hbm_gap = 1.5 mm`.
|
||||||
|
|
||||||
|
#### D3.2. HBM exclusion zone
|
||||||
|
|
||||||
|
`hbm_row_start = rows_per_half`,
|
||||||
|
`hbm_row_end = rows_per_half + 1`.
|
||||||
|
`hbm_col_start = n_cols // 2 - 1`,
|
||||||
|
`hbm_col_end = n_cols // 2`.
|
||||||
|
|
||||||
|
Router slots inside this (row, col) rectangle are marked `None` (no
|
||||||
|
router). HBM controllers are added separately as
|
||||||
|
`hbm_ctrl.pe{X}` nodes following ADR-0017 D9's per-PE partition
|
||||||
|
pattern.
|
||||||
|
|
||||||
|
#### D3.3. PE attachment
|
||||||
|
|
||||||
|
Each corner's PEs map to a row:
|
||||||
|
|
||||||
|
- Top half: NW → row 0, NE → row 1 (top_corners index).
|
||||||
|
- Bottom half: SW → row `hbm_row_end + 1`, SE → row
|
||||||
|
`hbm_row_end + 2`.
|
||||||
|
|
||||||
|
Each PE's x-coordinate attaches to the nearest column's router
|
||||||
|
(`min(range(n_cols), key=lambda c: abs(col_xs[c] - pe_x))`).
|
||||||
|
Attachment items are `pe{pe_idx}.dma`, `pe{pe_idx}.cpu`,
|
||||||
|
`pe{pe_idx}.hbm` (pushed into the router's attach list).
|
||||||
|
|
||||||
|
#### D3.4. M_CPU / SRAM attachment — nearest router by Euclidean distance
|
||||||
|
|
||||||
|
For `placement.m_cpu.pos_mm` (default `[1.5, 5.5]`) and
|
||||||
|
`placement.sram.pos_mm` (default `[1.5, 8.5]`), find the router with
|
||||||
|
the smallest Euclidean distance and append `"m_cpu"` / `"sram"` to
|
||||||
|
its attach list.
|
||||||
|
|
||||||
|
#### D3.5. UCIe N/S/E/W distribution
|
||||||
|
|
||||||
|
`ucie_pe_rows = top_pe_rows + bot_pe_rows` (total
|
||||||
|
`2 * rows_per_half`).
|
||||||
|
|
||||||
|
- UCIe-E: one PE row at a time, attach `ucie_e.c{i}` to the rightmost
|
||||||
|
column's router.
|
||||||
|
- UCIe-W: attach `ucie_w.c{i}` to the leftmost column's router (E's
|
||||||
|
mirror).
|
||||||
|
- UCIe-N/S: split PE columns into left and right halves; attach to
|
||||||
|
the top row's / bottom row's matching columns.
|
||||||
|
|
||||||
|
Each UCIe connection is suffixed `c{i}`, distributing
|
||||||
|
ucie_n_connections PHYs (ADR-0017 D5+).
|
||||||
|
|
||||||
|
### D4. Node naming convention — single ownership
|
||||||
|
|
||||||
|
builder.py creates nodes with the following naming convention (the
|
||||||
|
single-owner principle from ADR-0051 D5):
|
||||||
|
|
||||||
|
- `fabric.switch0` — system-level switch.
|
||||||
|
- `sip{S}.{io_id}.{pcie_ep|io_cpu|io_noc|io_ucie.{dir}|conn.{id}}` —
|
||||||
|
IO chiplet.
|
||||||
|
- `sip{S}.cube{C}.{m_cpu|sram|hbm_ctrl.pe{X}|noc.r{R}c{C}|...}` —
|
||||||
|
inside cube.
|
||||||
|
- `sip{S}.cube{C}.pe{P}.{pe_cpu|pe_dma|pe_fetch_store|pe_gemm|pe_math|pe_mmu|pe_tcm|pe_scheduler|pe_ipcq}` —
|
||||||
|
PE sub-components.
|
||||||
|
|
||||||
|
Changing this convention requires updating both builder.py and
|
||||||
|
router.py's helpers (ADR-0051). Components never know the convention
|
||||||
|
directly — they only call the helpers.
|
||||||
|
|
||||||
|
### D5. Edge `kind` classification
|
||||||
|
|
||||||
|
Every edge gets a `kind`; routing policy (ADR-0051 D2) reads it. Major
|
||||||
|
kinds:
|
||||||
|
|
||||||
|
- `"pe_internal"` — within a PE between sub-components.
|
||||||
|
- `"pe_to_router"` — PE_DMA ↔ cube NoC router.
|
||||||
|
- `"router_mesh"` — between cube NoC routers.
|
||||||
|
- `"router_to_hbm"`, `"router_to_mcpu"`, `"router_to_sram"`,
|
||||||
|
`"sram_to_router"`, etc. — between cube-attached components.
|
||||||
|
- `"ucie_internal"`, `"ucie_conn_to_router"`,
|
||||||
|
`"router_to_ucie_conn"`, `"ucie_conn_to_noc"`,
|
||||||
|
`"noc_to_ucie_conn"`, `"ucie_mesh"` — UCIe-related.
|
||||||
|
- `"io_internal"` — inside IO chiplet.
|
||||||
|
- `"io_to_cube"`, `"cube_to_io"` — at the IO ↔ cube boundary.
|
||||||
|
- `"pcie"` — switch ↔ pcie_ep.
|
||||||
|
- `"command"` — control-plane edges only (e.g., M_CPU ↔ NOC; excluded
|
||||||
|
from PE DMA paths).
|
||||||
|
|
||||||
|
Adding a new edge kind requires picking a category in router.py's
|
||||||
|
four adjacency graphs (ADR-0051 D2). If you forget, it defaults to
|
||||||
|
`_adj_all` only, which can produce unintended routes.
|
||||||
|
|
||||||
|
### D6. View projection — four abstraction levels
|
||||||
|
|
||||||
|
`TopologyGraph` keeps four view projections alongside the flat
|
||||||
|
nodes+edges:
|
||||||
|
|
||||||
|
- **system_view** (`_build_system_view`): Tray level. SIP blocks and
|
||||||
|
`fabric.switch0`. PCIe links shown. For external high-level
|
||||||
|
overview.
|
||||||
|
- **sip_view** (`_build_sip_view`): inside one SIP — cube mesh + IO
|
||||||
|
chiplet (pcie_ep + io_cpu + io_noc). UCIe N/S/E/W appear as
|
||||||
|
cube-cube links.
|
||||||
|
- **cube_view** (`_build_cube_view`): inside one cube — router grid +
|
||||||
|
PE / M_CPU / SRAM / HBM_CTRL attachments + UCIe PHY edges. For
|
||||||
|
intra-cube routing / placement debugging.
|
||||||
|
- **pe_view** (`_build_pe_view`): inside one PE — nine sub-components
|
||||||
|
+ internal edges (pe_internal kind). For detailed PE-internal
|
||||||
|
dataflow review.
|
||||||
|
|
||||||
|
Views are selectively rendered via the spec's
|
||||||
|
`visualization.emit_views: [system, sip, cube]` (ADR-0006). The pe
|
||||||
|
view is omitted from default output but the code is retained for
|
||||||
|
detailed debugging.
|
||||||
|
|
||||||
|
### D7. visualizer.py — SVG diagram output
|
||||||
|
|
||||||
|
`emit_diagrams(graph, out_dir)` renders every view as SVG. Key
|
||||||
|
functions:
|
||||||
|
|
||||||
|
- `_render_view_svg(view)` — generic view render (no router grid).
|
||||||
|
- `_render_cube_view_svg(view, spec)` — cube-view specific (HBM block,
|
||||||
|
router grid layout, PE/M_CPU/SRAM/HBM placement).
|
||||||
|
- `_draw_node`, `_draw_edge` — node/edge visual representation.
|
||||||
|
- `_pick_scale`, `_compute_node_sizes` — auto-scaling.
|
||||||
|
|
||||||
|
The visualizer is a **derived artifact** (ADR-0006); changes here do
|
||||||
|
not pass production checks. Aligns with CLAUDE.md's "Derived
|
||||||
|
Artifacts" guidance.
|
||||||
|
|
||||||
|
### D8. Blast radius of spec changes
|
||||||
|
|
||||||
|
| spec field | effect | mesh regenerated? |
|
||||||
|
|---------------------------------------|---------------------|-------------------|
|
||||||
|
| `system.sips.count` | SIP count, node count | No |
|
||||||
|
| `sip.cube_mesh.w/h` | cube mesh shape | No |
|
||||||
|
| `cube.geometry.cube_mm.w/h` | cube size (mm) | **Yes** |
|
||||||
|
| `cube.pe_layout.corners/pe_per_corner`| PE attachment positions | **Yes** |
|
||||||
|
| `cube.ucie.n_connections` | UCIe PHY distribution | **Yes** |
|
||||||
|
| `cube.memory_map.hbm_mapping_mode` | HBM distribution mode | **Yes** |
|
||||||
|
| `cube.placement` | M_CPU/SRAM positions | **Yes** |
|
||||||
|
| `cube.memory_map.*` (besides above) | HBM capacity / BW | No |
|
||||||
|
| `*.links.*.bw_gbs` | edge bandwidth | No |
|
||||||
|
| `*.attrs.overhead_ns` | component latency | No |
|
||||||
|
|
||||||
|
The table mirrors D2's `_compute_source_hash` inputs. Changes that
|
||||||
|
require mesh regeneration automatically invalidate `cube_mesh.yaml`'s
|
||||||
|
source_hash.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
### A1. Regenerate the mesh on every compile without a cache file
|
||||||
|
|
||||||
|
Rejected. The cost of mesh generation would be paid repeatedly (CLI
|
||||||
|
runs, probe, tests) for the same spec, and the human-inspectable
|
||||||
|
artifact would disappear.
|
||||||
|
|
||||||
|
### A2. Merge mesh generation into builder.py
|
||||||
|
|
||||||
|
Rejected (currently). It is a 305-line algorithm of its own, and the
|
||||||
|
mesh-layout decisions (placement-driven router attachment, HBM
|
||||||
|
exclusion zone) are different from builder's general node/edge
|
||||||
|
emission. Keeping it separate respects single-responsibility.
|
||||||
|
|
||||||
|
### A3. Express placement coordinates in cube coordinates (col/row)
|
||||||
|
|
||||||
|
Rejected. mm coordinates flow consistently between the visualizer and
|
||||||
|
mesh layout (for nearest-router computation). Cube coordinates are
|
||||||
|
undefined until the router grid is fixed, so they are unsuitable as
|
||||||
|
placement input.
|
||||||
|
|
||||||
|
### A4. Lazy view projection generation
|
||||||
|
|
||||||
|
Rejected (currently). The four views are cheap to build (typically <
|
||||||
|
100 ms), and eager construction guarantees `TopologyGraph` as the
|
||||||
|
single source of truth.
|
||||||
|
|
||||||
|
### A5. Visualizer output in formats besides SVG (PNG/PDF)
|
||||||
|
|
||||||
|
Rejected. SVG is vector + text-searchable + directly renderable in
|
||||||
|
browsers. PNG conversion, when required, is downstream
|
||||||
|
post-processing (e.g., rsvg-convert).
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- ADR-0006's high-level intent is fleshed out via D1–D7; topology
|
||||||
|
changes can be assessed quickly via D8's table.
|
||||||
|
- D3's mesh-layout algorithm is ADR-locked, so future PE attachment
|
||||||
|
patterns (e.g., a 6-zone HBM split) make clear which stage they
|
||||||
|
affect.
|
||||||
|
- D5's edge-kind list and D7's view structure are explicit, giving PR
|
||||||
|
reviewers a quick map of where (builder + router + visualizer) a
|
||||||
|
new component type ripples through.
|
||||||
|
- D2's source_hash invalidation rules are explicit, so a stale
|
||||||
|
`cube_mesh.yaml` (e.g., when only bandwidth changed) is recognized
|
||||||
|
as correct behavior.
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
# ADR-0054: Milestone Eval Benches — self-contained sweep + figure benches
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted (2026-05-22).
|
||||||
|
|
||||||
|
Amends ADR-0044 (D1/D2) and ADR-0045 (D5) and supersedes the "logic lives
|
||||||
|
in `scripts/` + `tests/`" arrangement of ADR-0043/0044: the GEMM and
|
||||||
|
allreduce evaluation harnesses are now self-contained **benches** that a
|
||||||
|
user runs to regenerate every result + figure.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
ADR-0043 (allreduce eval) and ADR-0044 (GEMM eval) split each harness into
|
||||||
|
a **sweep** (a manual `scripts/` driver, or — for allreduce — the
|
||||||
|
parametrized tests themselves) plus **figure tests** that render committed
|
||||||
|
data. The sweep/render logic therefore lived under `scripts/gemm_sweep.py`,
|
||||||
|
`tests/gemm/_gemm_plot_helpers.py`, and `tests/sccl/_allreduce_helpers.py`.
|
||||||
|
|
||||||
|
A milestone requirement ("refactor allreduce + GEMM evaluation so a user
|
||||||
|
can run *one bench* to generate all the results and plots") cannot be met
|
||||||
|
by that layout: a bench is production code and **must not import from
|
||||||
|
`tests/`** (ADR-0007 layer direction). The eval logic had to move into
|
||||||
|
production, reachable from a bench.
|
||||||
|
|
||||||
|
The chosen home is the bench module itself — not a separate
|
||||||
|
`kernbench.eval` package. A bench file may contain arbitrary module-level
|
||||||
|
code; collapsing the harness into the bench keeps one file per domain and
|
||||||
|
avoids an extra package layer.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### D1. Two milestone benches own the eval logic
|
||||||
|
|
||||||
|
- `src/kernbench/benches/milestone_1h_gemm.py` — GEMM shape×variant sweep +
|
||||||
|
the three figure renderers (moved from `scripts/gemm_sweep.py` +
|
||||||
|
`tests/gemm/_gemm_plot_helpers.py`).
|
||||||
|
- `src/kernbench/benches/milestone_1h_ccl.py` — the distributed allreduce
|
||||||
|
driver, latency + buffer-kind sweeps, topology diagram, FSIM comparison,
|
||||||
|
and the direct-launch parity reference (moved from
|
||||||
|
`tests/sccl/_allreduce_helpers.py`).
|
||||||
|
|
||||||
|
Each file is the **single home** for its domain's eval logic.
|
||||||
|
|
||||||
|
### D2. The "eval bench" pattern (extends ADR-0045 D5)
|
||||||
|
|
||||||
|
ADR-0045 D5 fixed a bench to a single configuration (single-SIP, or the
|
||||||
|
ADR-0024 multi-SIP CCL exception). This ADR adds a third pattern:
|
||||||
|
|
||||||
|
- An **eval bench** may drive *many* configurations and render figures. It
|
||||||
|
builds its own per-config `GraphEngine` / `RuntimeContext` instances
|
||||||
|
(one per sweep point) rather than using the outer `run_bench` engine.
|
||||||
|
- Because the outer ctx then has no submitted handles, the bench submits a
|
||||||
|
**sentinel tensor** (`torch.zeros((1, 1), …)`) at the end to satisfy
|
||||||
|
`run_bench`'s "must submit at least one request" contract (ADR-0045 D4),
|
||||||
|
so the CLI exits 0.
|
||||||
|
|
||||||
|
### D3. Output location
|
||||||
|
|
||||||
|
Both benches write to `src/kernbench/benches/1H_milestone_output/{gemm,ccl}/`
|
||||||
|
(per user request — artifacts beside the bench). The directory holds only
|
||||||
|
generated PNG/CSV/JSON (never a `.py`/`__init__.py`), so the eager-import
|
||||||
|
audit (ADR-0045 first action) ignores it — `pkgutil.iter_modules` does not
|
||||||
|
yield non-package subdirectories. It is **committed** (like the
|
||||||
|
`docs/diagrams/` artifacts) so the figures are viewable on the remote;
|
||||||
|
rerunning the bench regenerates it in place.
|
||||||
|
|
||||||
|
### D4. GEMM heavy sweep — fresh by default, `MILESTONE_FAST` to reuse
|
||||||
|
|
||||||
|
`milestone-1h-gemm` runs the full 24-sim sweep by default (minutes; one
|
||||||
|
shape is 2048 tiles). `MILESTONE_FAST=1` reuses the committed
|
||||||
|
`docs/diagrams/gemm_sweep.json` and only re-renders (seconds). This
|
||||||
|
reverses ADR-0044 D1/D2's "heavy sweep stays a manual/`slow`-marked step":
|
||||||
|
running the bench *is* the regeneration. The slow path is exercised by a
|
||||||
|
`@pytest.mark.slow` bench test; the fast path runs by default.
|
||||||
|
|
||||||
|
### D5. Tests + script reuse via thin re-export shims (single home kept)
|
||||||
|
|
||||||
|
The pre-existing figure tests and the `scripts/gemm_sweep.py` entry point
|
||||||
|
are retained and now reuse the bench modules:
|
||||||
|
|
||||||
|
- `tests/gemm/_gemm_plot_helpers.py` → re-exports the renderers +
|
||||||
|
`GEMM_SWEEP_JSON`/`GEMM_PLOTS_DIR`/`ROOT` from
|
||||||
|
`kernbench.benches.milestone_1h_gemm`.
|
||||||
|
- `tests/sccl/_allreduce_helpers.py` → re-exports the driver core, config
|
||||||
|
writers, sweep constants, renderers, and disk aggregators from
|
||||||
|
`kernbench.benches.milestone_1h_ccl`, and keeps the **pytest-only** pieces
|
||||||
|
local: the `pytest.param` matrices (`CONFIGS` / `_sweep_params` /
|
||||||
|
`_bk_params`) and the fixture-coupled `_run_distributed`
|
||||||
|
(`monkeypatch.chdir` + `_drive_distributed`) wrapper.
|
||||||
|
- `scripts/gemm_sweep.py` → thin wrapper over the bench's `run_sweep`.
|
||||||
|
|
||||||
|
Tests importing a bench module is permitted (tests sit above production,
|
||||||
|
ADR-0007); it triggers the whole-package eager audit, which already runs on
|
||||||
|
every `kernbench` invocation. matplotlib stays lazily imported inside the
|
||||||
|
renderers, so the audit's startup cost is unchanged.
|
||||||
|
|
||||||
|
### D6. Flat module naming (no `benches/` subfolder)
|
||||||
|
|
||||||
|
A `benches/` subpackage named `1H_milestone…` is impossible — a Python
|
||||||
|
package name cannot start with a digit. The benches are therefore flat
|
||||||
|
modules `milestone_1h_gemm.py` / `milestone_1h_ccl.py` with bench names
|
||||||
|
`milestone-1h-gemm` / `milestone-1h-ccl` (kebab-case, letter-first per
|
||||||
|
ADR-0045 D1).
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
|
||||||
|
- `kernbench run --bench milestone-1h-gemm` (or `…-ccl`) regenerates all of
|
||||||
|
a domain's results + figures in one command — the milestone requirement.
|
||||||
|
- Single source for the eval logic (the bench), reused by tests and the
|
||||||
|
script via shims; no duplication.
|
||||||
|
- The figure tests and `scripts/gemm_sweep.py` keep working unchanged.
|
||||||
|
|
||||||
|
### Negative / limitations
|
||||||
|
|
||||||
|
- The two bench files are large (the CCL one mixes the distributed driver,
|
||||||
|
sweeps, and matplotlib drawing). A "bench" that is mostly an eval harness
|
||||||
|
is unusual; this ADR legitimizes it.
|
||||||
|
- Generated artifacts live inside the source tree (`src/kernbench/benches/`)
|
||||||
|
by explicit request and are committed (so the figures are viewable on the
|
||||||
|
remote); rerunning the bench regenerates them.
|
||||||
|
- `milestone-1h-ccl` (and the default `milestone-1h-gemm`) take minutes —
|
||||||
|
acceptable for an on-demand milestone artifact, not for routine runs.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **ADR-0007**: layer direction (why tests may import production but a bench
|
||||||
|
may not import tests).
|
||||||
|
- **ADR-0043 / ADR-0044**: the allreduce / GEMM eval harnesses this ADR
|
||||||
|
relocates into benches.
|
||||||
|
- **ADR-0045**: bench module contract; D2 here extends its D5 (single-device
|
||||||
|
rule) with the eval-bench pattern, and relies on D4 (NO_REQUESTS) for the
|
||||||
|
sentinel.
|
||||||
|
- **ADR-0024**: rank = SIP launcher driven by the allreduce sweeps.
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
- Should the GEMM theoretical-model constants (ADR-0044 D5) be sourced from
|
||||||
|
ADR-0033/0014 rather than copied? Unchanged by this ADR.
|
||||||
|
- Should `build_overview_slides.py` consume the milestone output PNGs
|
||||||
|
instead of drawing GEMM bars natively? Still open (ADR-0044 D6 / Negative).
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
# ADR-0062: Lazy `tl.load` — non-blocking HBM load with auto-wait on first use
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
> Supporting ADR for **ADR-0060** (AHBM GQA Fused Attention). Decode and
|
||||||
|
> long-context attention are **KV-load-bound**; load/compute overlap is a
|
||||||
|
> dominant lever. This ADR makes `tl.load` itself **lazy** (non-blocking,
|
||||||
|
> with the wait automatically inserted at first use) rather than adding a
|
||||||
|
> separate `load_async` op. Today the only async primitive is for IPCQ
|
||||||
|
> comms, not for HBM loads.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
### What overlap requires
|
||||||
|
|
||||||
|
FlashAttention streams operands: while the GEMM/MATH engine works on the
|
||||||
|
current tile, the DMA engine should already be pulling the next operand.
|
||||||
|
With that overlap a bandwidth-bound kernel runs at roughly
|
||||||
|
`max(compute, dma)` per tile instead of `compute + dma`.
|
||||||
|
|
||||||
|
In ADR-0060's hybrid design the two GEMMs are issued as
|
||||||
|
`tl.composite(op="gemm")` whose K/V operands are `tl.ref` (HBM-resident,
|
||||||
|
streamed per tile by PE_SCHEDULER), so the **per-tile K/V prefetch is
|
||||||
|
handled by the composite scheduler**. Lazy `tl.load` covers the remaining
|
||||||
|
explicit loads (the Q group, and any non-composite kernel) so those also
|
||||||
|
overlap the compute that follows instead of stalling the greenlet.
|
||||||
|
|
||||||
|
### What exists
|
||||||
|
|
||||||
|
- `tl.load(ptr, shape, dtype)` is **blocking**: it emits `DmaReadCmd` and
|
||||||
|
the greenlet kernel suspends until PE_DMA signals completion
|
||||||
|
(`tl_context.py:177-203`; greenlet drive `kernel_runner.py:146-153`).
|
||||||
|
No two `tl.load`s can be in flight from one kernel.
|
||||||
|
- An async pattern **does** exist, but only for IPCQ:
|
||||||
|
`tl.recv_async(dir, ...) -> RecvFuture` + a deferred wait
|
||||||
|
(`kernel_runner.py:248-285`). It proves the machinery — a non-blocking
|
||||||
|
command that returns a future, resolved later by a wait check
|
||||||
|
(`if not future.event.triggered: yield future.event`) — works in the
|
||||||
|
greenlet model.
|
||||||
|
- The DMA engine models a read channel as a SimPy resource (capacity 1,
|
||||||
|
`pe_dma.py:45`) separate from the write channel, so in-flight reads are
|
||||||
|
representable; they serialise on the single read channel while
|
||||||
|
overlapping compute on PE_GEMM/PE_MATH. Only the *kernel-facing API*
|
||||||
|
currently serialises load-vs-compute.
|
||||||
|
|
||||||
|
`tl.load` cannot today overlap with the compute that follows it.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**Make `tl.load` lazy: it issues the `DmaReadCmd` and returns a handle
|
||||||
|
immediately (non-blocking); the runtime auto-inserts the wait at the
|
||||||
|
first point the loaded data is actually consumed.** The kernel-facing API
|
||||||
|
is unchanged — authors keep writing `tl.load` — so this is a *semantics*
|
||||||
|
change, not a new op. It generalises the existing `recv_async`/wait
|
||||||
|
machinery (1) to the HBM-load path and (2) from an explicit `tl.wait`
|
||||||
|
call to an implicit, dependency-driven wait at first use.
|
||||||
|
|
||||||
|
### D1. `tl` surface — unchanged
|
||||||
|
|
||||||
|
`tl.load(ptr, shape, dtype)` keeps its signature and `TensorHandle`
|
||||||
|
return. What changes is *when* it blocks: never at issue, only implicitly
|
||||||
|
when its result is first read by a consuming op.
|
||||||
|
|
||||||
|
### D2. Mechanism — non-blocking issue + auto-wait on use
|
||||||
|
|
||||||
|
- `tl.load` posts the `DmaReadCmd` to PE_DMA without yielding its
|
||||||
|
completion event, and records the pending event on the returned handle
|
||||||
|
(the `recv_async` pattern, applied to loads).
|
||||||
|
- When a consuming op (`tl.dot`, a MATH op, `tl.store`, a `tl.composite`
|
||||||
|
operand, …) is dispatched, the runtime checks each input handle for a
|
||||||
|
pending load event and yields it first if not yet triggered — i.e. the
|
||||||
|
wait is inserted automatically at the latest correct point (first use).
|
||||||
|
- The op_log entry is unchanged (`memory/dma_read`): asynchrony is a
|
||||||
|
scheduling property, not a new op kind, so `dma_read_count` and existing
|
||||||
|
op_log consumers keep working.
|
||||||
|
|
||||||
|
### D3. Scope — global
|
||||||
|
|
||||||
|
`tl.load` is lazy **everywhere**, not behind an opt-in flag. This is the
|
||||||
|
faithful model: blocking on every load is a property of a *naive* kernel;
|
||||||
|
an efficient kernel (and a real compiler) hoists the load and waits only
|
||||||
|
at use. Consequence: existing kernels that have independent work between a
|
||||||
|
`tl.load` and its first use see **lower (faster) latency** — a
|
||||||
|
correctness improvement of the model, not a behaviour regression. Golden
|
||||||
|
latencies that change must be **regenerated**; kernels that load then
|
||||||
|
immediately use see no change (the auto-wait fires at once, identical to
|
||||||
|
blocking).
|
||||||
|
|
||||||
|
### D4. Latency / overlap semantics
|
||||||
|
|
||||||
|
- `tl.load` charges the **issue** (descriptor push) only; the kernel
|
||||||
|
proceeds. (Per-op issue cost is `dispatch_cycles`, currently 0; an
|
||||||
|
op-type-differentiated issue cost is tracked separately — ADR-0060 §1,
|
||||||
|
§9.)
|
||||||
|
- The DMA transfer occupies the read channel (capacity 1) for its
|
||||||
|
modelled duration in parallel with whatever compute the kernel issues
|
||||||
|
next; multiple in-flight loads serialise on the channel but overlap
|
||||||
|
compute.
|
||||||
|
- The auto-inserted wait blocks only if the transfer has not finished.
|
||||||
|
- Determinism is preserved: the wait point is fixed by program order
|
||||||
|
(first use), and completion is a scheduled event on the modelled DMA
|
||||||
|
channel (SPEC §0.1, R8). No latency subtraction — the overlap is real
|
||||||
|
modelled concurrency.
|
||||||
|
|
||||||
|
> **Modelling assumption.** Auto-wait-at-first-use models a *well-
|
||||||
|
> scheduled* kernel (the compiler places the wait at the latest correct
|
||||||
|
> point). Real compilers approximate this; some loads cannot be hoisted
|
||||||
|
> (register pressure, aliasing). For a performance simulator (SPEC §0)
|
||||||
|
> modelling the well-scheduled case is the intended behaviour.
|
||||||
|
|
||||||
|
## Alternatives
|
||||||
|
|
||||||
|
### A1. Separate `tl.load_async` op (earlier proposal)
|
||||||
|
|
||||||
|
Add an explicit `load_async`/`wait` pair the kernel calls by hand (the
|
||||||
|
double-buffer dance). Rejected: it enlarges the kernel-facing API and
|
||||||
|
pushes buffer-lifetime bookkeeping onto every kernel author, when lazy
|
||||||
|
`tl.load` gives the same overlap with **no** API-surface change and a
|
||||||
|
compiler-style auto-wait.
|
||||||
|
|
||||||
|
### A2. Per-kernel opt-in lazy load
|
||||||
|
|
||||||
|
Keep `tl.load` blocking by default; make new kernels opt in. Rejected:
|
||||||
|
splits `tl.load` semantics into two variants and hides the model
|
||||||
|
improvement from existing benches; global lazy is cleaner and the
|
||||||
|
golden-regeneration cost is one-time (D3).
|
||||||
|
|
||||||
|
### A3. Rely on composite streaming only (no lazy load)
|
||||||
|
|
||||||
|
ADR-0060's composites stream their `tl.ref` K/V operands, so the GEMM
|
||||||
|
operand DMA already overlaps. But explicit loads (the Q group, and any
|
||||||
|
non-composite kernel) still stall without lazy `tl.load`. Insufficient on
|
||||||
|
its own.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
- Load/compute overlap with **zero** kernel-facing API change; authors
|
||||||
|
keep writing `tl.load`.
|
||||||
|
- Symmetric with `recv_async`/wait — low conceptual surface area.
|
||||||
|
- General: any bandwidth-bound kernel prefetches automatically.
|
||||||
|
|
||||||
|
### Negative
|
||||||
|
- Existing golden latencies shift (faster) for kernels with independent
|
||||||
|
work between load and use → one-time regeneration (D3).
|
||||||
|
- Auto-wait requires the runtime to track per-handle pending events and
|
||||||
|
check them at consuming-op dispatch (data-dependency tracking).
|
||||||
|
- Interacts with scratch lifetime (ADR-0063): an in-flight load's target
|
||||||
|
buffer must not be recycled before its auto-wait fires. The recycling
|
||||||
|
scope must exclude live (un-waited) load buffers.
|
||||||
|
|
||||||
|
## Test Requirements
|
||||||
|
|
||||||
|
1. **Overlap is real**: a kernel that issues `tl.load` then an
|
||||||
|
independent GEMM of comparable duration completes in ≈`max(load,
|
||||||
|
gemm)`, not `load+gemm` (end-to-end latency strictly below the serial
|
||||||
|
sum).
|
||||||
|
2. **Auto-wait correctness**: the loaded `TensorHandle`, when first
|
||||||
|
consumed, carries the same bytes as today's blocking `tl.load` for the
|
||||||
|
same address (Phase 2).
|
||||||
|
3. **Two in flight**: two `tl.load`s to distinct addresses, consumed
|
||||||
|
later, both resolve to correct, independent tensors; their DMAs
|
||||||
|
serialise on the read channel.
|
||||||
|
4. **op_log compatibility**: each `tl.load` still logs exactly one
|
||||||
|
`memory/dma_read`; `dma_read_count` unchanged vs the blocking version.
|
||||||
|
5. **No-overlap no-change**: a kernel that loads then immediately uses has
|
||||||
|
identical latency to the blocking model (auto-wait fires at once).
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
# ADR-0063: `tl.scratch_scope` — per-tile scratch recycling for long context
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
> Supporting ADR for **ADR-0060** (AHBM GQA Fused Attention). Long-context
|
||||||
|
> attention sweeps many K/V tiles; each tile's intermediates
|
||||||
|
> (`scores`, `P`, `exp`, partial `O`, …) currently allocate fresh
|
||||||
|
> scratch that is never freed within a kernel invocation. The 1 MiB
|
||||||
|
> per-PE scratch budget is exhausted long before a realistic context
|
||||||
|
> length.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
### The bump allocator
|
||||||
|
|
||||||
|
`TLContext` allocates every math/compute output handle from a per-PE
|
||||||
|
scratch pool with a **linear bump cursor**
|
||||||
|
(`src/kernbench/triton_emu/tl_context.py`; `_scratch_alloc`):
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _scratch_alloc(self, nbytes):
|
||||||
|
aligned = (nbytes + 15) & ~15
|
||||||
|
addr = self._scratch_base + self._scratch_cursor
|
||||||
|
self._scratch_cursor += aligned
|
||||||
|
if self._scratch_cursor > self._scratch_size: # default 1 << 20 = 1 MiB
|
||||||
|
raise RuntimeError("TLContext scratch overflow: ...")
|
||||||
|
return addr
|
||||||
|
```
|
||||||
|
|
||||||
|
The docstring states the cursor **"resets on every kernel invocation"** —
|
||||||
|
i.e. only at kernel entry, never *within* the kernel body. Every
|
||||||
|
`tl.dot`, `tl.softmax`, `tl.exp`, `tl.sum`, `a - b`, `a * b`, … grabs a
|
||||||
|
fresh slice and nothing is reclaimed until the kernel returns.
|
||||||
|
|
||||||
|
### Why this bites the GQA kernel
|
||||||
|
|
||||||
|
The current milestone bench keeps `S_q = S_kv_per_rank = 16` **explicitly
|
||||||
|
because** of this limit
|
||||||
|
(`tests/attention/test_milestone_gqa_llama70b.py:123-148`):
|
||||||
|
|
||||||
|
> "S_q_prefill and S_kv_per_rank are deliberately small (16 each) so the
|
||||||
|
> simulator's 1 MB per-PE TCM kernel scratch is not exhausted by the
|
||||||
|
> bump-allocated handle outputs of softmax/exp/dot/sum chains over
|
||||||
|
> n_ranks ring steps."
|
||||||
|
|
||||||
|
A FlashAttention sweep over `n_tiles` tiles allocates O(`n_tiles` ×
|
||||||
|
per-tile-intermediates). For any realistic context this overflows. The
|
||||||
|
*math* of flash attention needs only **O(1)** live scratch — the running
|
||||||
|
`(m, l, O)` plus the current tile's working set — because each tile's
|
||||||
|
temporaries are dead once that tile is folded into the running state. The
|
||||||
|
allocator just doesn't know they're dead.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Add a **scratch scope** that lets a kernel mark a region of allocations
|
||||||
|
as reclaimable, so per-tile temporaries are recycled while live running
|
||||||
|
state (and in-flight prefetch buffers) are preserved.
|
||||||
|
|
||||||
|
### D1. `tl` surface — context manager
|
||||||
|
|
||||||
|
```python
|
||||||
|
with tl.scratch_scope():
|
||||||
|
s = tl.dot(q, k_t) # all handles allocated inside the `with`
|
||||||
|
p = tl.softmax(s) # share a region that is rewound on exit
|
||||||
|
o_j = tl.dot(p, v)
|
||||||
|
# ... fold o_j into running (m,l,O) which live OUTSIDE the scope ...
|
||||||
|
# on __exit__: cursor rewinds to its value at __enter__
|
||||||
|
```
|
||||||
|
|
||||||
|
Semantics:
|
||||||
|
- `__enter__` records the current `_scratch_cursor` as a save-point.
|
||||||
|
- `__exit__` restores the cursor to the save-point, freeing everything
|
||||||
|
allocated inside.
|
||||||
|
- Handles allocated **outside** the scope (running `m,l,O`, prefetch
|
||||||
|
buffers held by `LoadFuture` from ADR-0062) keep their addresses —
|
||||||
|
they were allocated before the save-point or in an enclosing scope.
|
||||||
|
|
||||||
|
### D2. Safety contract
|
||||||
|
|
||||||
|
A handle allocated inside a scope **must not** be read after the scope
|
||||||
|
exits — its bytes may be overwritten by the next scope's allocations.
|
||||||
|
The flash loop respects this naturally: the only values that survive a
|
||||||
|
tile iteration are the running accumulators, which are allocated outside
|
||||||
|
the per-tile scope and updated by ops *inside* it writing to outside
|
||||||
|
addresses (the merge writes new running state — see D3).
|
||||||
|
|
||||||
|
### D3. Interaction with running accumulators
|
||||||
|
|
||||||
|
The online-softmax merge reads the old running `(m, l, O)` and the
|
||||||
|
current tile's `(m_j, l_j, O_j)`, producing new running values. To keep
|
||||||
|
the new running values outside the recycled region, the merge writes them
|
||||||
|
to **stable scratch** allocated once before the loop (a small fixed
|
||||||
|
"running-state" arena, distinct from the per-tile scope). Concretely the
|
||||||
|
kernel keeps two arenas:
|
||||||
|
|
||||||
|
- **persistent arena** (allocated once): `m, l, O` (and their
|
||||||
|
double-buffer if needed for the merge).
|
||||||
|
- **scoped arena** (rewound each tile): `scores, P, exp, O_j, scale_*`.
|
||||||
|
|
||||||
|
This mirrors how real flash-attention SRAM budgeting works: a small
|
||||||
|
persistent accumulator region + a recycled tile working set.
|
||||||
|
|
||||||
|
#### D3.1 The persistent-arena write mechanism: `tl.copy_to(dst, src)`
|
||||||
|
|
||||||
|
The merge ops (`tl.maximum`, `tl.exp`, binary `*` / `+`) all call
|
||||||
|
`_make_compute_out(...)` which allocates from the bump cursor (D1).
|
||||||
|
Inside a `scratch_scope`, their result handles therefore live **inside**
|
||||||
|
the scope and vanish on `__exit__`. To realise D3's two-arena split, the
|
||||||
|
kernel needs a way to **write a scoped result's bytes back to a
|
||||||
|
persistent address**.
|
||||||
|
|
||||||
|
The primitive that closes this gap:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def copy_to(self, dst: TensorHandle, src: TensorHandle) -> None:
|
||||||
|
"""Copy ``src``'s bytes into ``dst``'s address (both TCM).
|
||||||
|
|
||||||
|
Shapes and dtypes must match. ``dst`` is typically a handle
|
||||||
|
allocated outside any active ``scratch_scope`` (the persistent
|
||||||
|
arena); ``src`` is a scoped handle whose bytes must outlive
|
||||||
|
scope ``__exit__``.
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
Symmetric to `tl.store` (the HBM-side byte copy), kept TCM-only here so
|
||||||
|
the running-state writeback doesn't pollute op_log with spurious DMA.
|
||||||
|
|
||||||
|
**Mechanics:**
|
||||||
|
- New `CopyCmd(src, dst, nbytes, data_op=True)` command.
|
||||||
|
- op_log: `op_kind="math"`, `op_name="copy"` — runs on the vector engine.
|
||||||
|
- Latency: `pe_math._compute_ns(prod(shape))` — models on-chip register
|
||||||
|
writeback, not HBM transfer.
|
||||||
|
- Emit-time validation: `dst.shape == src.shape`, `dst.dtype == src.dtype`,
|
||||||
|
`dst.space == "tcm"`, `src.space == "tcm"`. Authoring errors surface in
|
||||||
|
Phase 1, not deep in Phase 2 data execution.
|
||||||
|
|
||||||
|
**Call-site pattern:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
m, l, O = init_running(...) # persistent (outside scope)
|
||||||
|
for j in range(n_tiles):
|
||||||
|
with tl.scratch_scope():
|
||||||
|
... # per-tile work (recycled)
|
||||||
|
m_new = tl.maximum(m, mj) # scoped scratch
|
||||||
|
l_new = l * scale_old + l_step * scale_step
|
||||||
|
O_new = O * scale_old + O_step * scale_step
|
||||||
|
|
||||||
|
tl.copy_to(m, m_new) # ← persist new running state
|
||||||
|
tl.copy_to(l, l_new)
|
||||||
|
tl.copy_to(O, O_new)
|
||||||
|
# exit: scoped m_new/l_new/O_new gone; their bytes live in persistent m/l/O
|
||||||
|
```
|
||||||
|
|
||||||
|
The copy happens **before** `__exit__`, so the read of `src` (scoped) is
|
||||||
|
valid; after exit only `dst` (persistent) is read, satisfying D2's
|
||||||
|
no-read-after-exit safety contract.
|
||||||
|
|
||||||
|
**Why a dedicated primitive rather than `dst=` kwargs on every math op**
|
||||||
|
(considered, rejected): adding `dst=` to `tl.maximum`, `tl.exp`,
|
||||||
|
`_binary_math`, `_unary_math`, `_reduction` is ~25 LOC across 5
|
||||||
|
op-families and breaks the uniform "call returns a fresh handle"
|
||||||
|
pattern. `tl.copy_to` is one primitive, one command, one executor
|
||||||
|
handler — minimal surface area for the same effect.
|
||||||
|
|
||||||
|
### D4. Nesting
|
||||||
|
|
||||||
|
Scopes nest (stack of save-points). Inner scope exit rewinds to the inner
|
||||||
|
save-point; outer exit rewinds further. This supports an outer
|
||||||
|
"per-query-block" scope around an inner "per-KV-tile" scope for prefill.
|
||||||
|
|
||||||
|
## Alternatives
|
||||||
|
|
||||||
|
### A1. Round-trip temporaries through HBM
|
||||||
|
|
||||||
|
Store intermediates to HBM and reload to "free" TCM scratch. Rejected:
|
||||||
|
turns a TCM-resident streaming kernel into an HBM-bandwidth-bound one —
|
||||||
|
the opposite of the goal, and it pollutes op_log with spurious DMA.
|
||||||
|
|
||||||
|
### A2. Tiled `tl.composite` (scheduler-managed scratch)
|
||||||
|
|
||||||
|
`tl.composite` recycles per-tile scratch inside PE_SCHEDULER
|
||||||
|
automatically. As in ADR-0062 A1, this is attractive but blocked on a
|
||||||
|
flash-capable composite kind (two GEMMs + carried `(m,l,O)`), a much
|
||||||
|
larger change. `scratch_scope` gives the same memory behaviour for the
|
||||||
|
greenlet kernel with a tiny, general primitive. The two can coexist.
|
||||||
|
|
||||||
|
### A3. Grow the scratch budget
|
||||||
|
|
||||||
|
Bump `pe_tcm.kernel_scratch_mb`. Rejected as a fix: it only pushes the
|
||||||
|
context-length ceiling out linearly while still leaking O(`n_tiles`)
|
||||||
|
scratch, and it misrepresents the hardware (real TCM is small; the point
|
||||||
|
of flash attention is O(1) working set). Useful only as a coarse knob,
|
||||||
|
not a substitute for recycling.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
- Removes the artificial `S = 16` validation-scale ceiling; enables
|
||||||
|
realistic context lengths in both timing and data modes.
|
||||||
|
- Faithfully models flash attention's O(1) working-set property.
|
||||||
|
- Small, general primitive (any tiled kernel benefits).
|
||||||
|
|
||||||
|
### Negative
|
||||||
|
- A use-after-scope bug silently reads stale bytes. Mitigated by the D2
|
||||||
|
contract, by keeping the scope discipline inside a shared attention
|
||||||
|
helper, and (optionally) by a debug build that poisons rewound regions.
|
||||||
|
- Must coordinate with ADR-0062: prefetch buffers are live across tile
|
||||||
|
iterations, so they belong to the persistent arena, not the scoped one.
|
||||||
|
|
||||||
|
## Test Requirements
|
||||||
|
|
||||||
|
1. **Recycling**: a loop of `N` tiles inside `tl.scratch_scope()` keeps
|
||||||
|
peak `_scratch_cursor` bounded by one tile's footprint, independent of
|
||||||
|
`N` (today it grows linearly and overflows).
|
||||||
|
2. **Correctness**: a flash sweep with scopes produces the same `O` as
|
||||||
|
the same sweep without scopes at a small `N` that fits without
|
||||||
|
recycling (Phase 2).
|
||||||
|
3. **Long context**: a sweep at an `N` that would overflow 1 MiB without
|
||||||
|
scopes completes (the exact failure the `S=16` cap avoids today).
|
||||||
|
4. **Persistent-vs-scoped isolation**: running `(m,l,O)` allocated
|
||||||
|
outside the scope retains correct values across `__exit__`.
|
||||||
|
5. **Nesting**: nested scopes rewind to the correct save-points.
|
||||||
@@ -0,0 +1,471 @@
|
|||||||
|
# ADR-0064: Structural CPU dispatch cost model (`logical_bytes` + FIXED + R)
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted (Revision 2)
|
||||||
|
|
||||||
|
> Supporting ADR for **ADR-0060** (AHBM GQA Fused Attention) and **ADR-0065**
|
||||||
|
> (flat-ops composite + first stateful recipe). The hybrid decision there
|
||||||
|
> (GEMMs via `tl.composite`, softmax merge in the kernel) wins by
|
||||||
|
> **offloading tiling to PE_SCHEDULER so the CPU issues coarse descriptors
|
||||||
|
> and runs ahead, keeping the engines saturated**. That win is currently
|
||||||
|
> **invisible in the simulator** because per-op CPU issue cost is zero.
|
||||||
|
>
|
||||||
|
> Revision 2 replaces the **op-type calibration table** (the original
|
||||||
|
> proposal) with a **structural formula** derived from each command's
|
||||||
|
> `logical_bytes` — no per-op-type calibration needed; new op kinds are
|
||||||
|
> covered automatically.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
### What exists today
|
||||||
|
|
||||||
|
- Every `tl.*` op calls `_emit_dispatch_overhead()` before emitting its
|
||||||
|
command (`tl_context.py:196-212`), which emits
|
||||||
|
`PeCpuOverheadCmd(cycles=dispatch_cycles)` **only if** `dispatch_cycles
|
||||||
|
> 0`. The knob is **uniform** across op kinds and hardcoded to **0**
|
||||||
|
in both live execution paths (`pe_cpu.py:101` greenlet, `:195` replay).
|
||||||
|
- ⇒ issuing a command — *constructing the descriptor and pushing it to
|
||||||
|
the scheduler queue* — currently costs **0 ns** on PE_CPU.
|
||||||
|
- `PeCpuOverheadCmd` is consumed as `yield env.timeout(cmd.cycles)` on
|
||||||
|
PE_CPU (`kernel_runner.py:131-132`).
|
||||||
|
|
||||||
|
### Why uniform-and-zero is wrong for the hybrid
|
||||||
|
|
||||||
|
ADR-0060 §1's argument is that **one** composite descriptor offloads
|
||||||
|
`N_tiles` worth of GEMM tiling, so the CPU issues `O(1)` coarse commands
|
||||||
|
instead of `O(N_tiles × ops/tile)` fine ones. With issue cost = 0, the
|
||||||
|
model cannot show:
|
||||||
|
- that the primitive path may **fail to saturate** the engines when the
|
||||||
|
CPU cannot push fast enough, nor
|
||||||
|
- that a composite **costs more to construct** than a single primitive
|
||||||
|
but far less than the many primitives it replaces.
|
||||||
|
|
||||||
|
### Why per-op-type calibration (Revision 1) was over-shaped
|
||||||
|
|
||||||
|
The original proposal had a `cost_table[kind]` keyed by op kind
|
||||||
|
(`composite`, `load`, `dot`, `math`, …). That required:
|
||||||
|
- a value per kind (calibration cost ≥ |kinds|),
|
||||||
|
- a new entry every time a new kind appears,
|
||||||
|
- and yet the *ratio* it tried to capture — "composite ≫ primitive,
|
||||||
|
but ≪ the primitives it replaces" — is structurally a function of
|
||||||
|
**how many fields the command carries**, not of the op kind.
|
||||||
|
|
||||||
|
A command's *byte footprint* is the natural proxy: a composite carrying
|
||||||
|
N OpSpecs has ~N× the bytes of a primitive op with one OpSpec. The
|
||||||
|
*fixed* part (queue head update, completion register, MMIO-class
|
||||||
|
latency) is per-command. The two together compose: `FIXED + bytes × R`.
|
||||||
|
|
||||||
|
### What this model actually exposes
|
||||||
|
|
||||||
|
The **primary** signal is **command-count reduction** through the
|
||||||
|
per-command FIXED cost. The **byte** term is a secondary refinement
|
||||||
|
that prevents pathologically-large composites from looking free. With
|
||||||
|
realistic on-die queue bandwidth (16 B/cycle, D3), FIXED accounts for
|
||||||
|
≥85% of the dispatch cost differential between opt3 (≈10 cmds/tile)
|
||||||
|
and opt2 (≈2 cmds/tile) for decode opt2.
|
||||||
|
|
||||||
|
This framing also bounds the model: if a single composite were allowed
|
||||||
|
to grow unboundedly large, the byte term alone would not stop the
|
||||||
|
formula from rewarding ever-bigger fused commands beyond what real HW
|
||||||
|
supports — hence the descriptor-size cap in **D7**.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### D1. Structural dispatch cost formula
|
||||||
|
|
||||||
|
Each PE command going to PE_SCHEDULER incurs PE_CPU dispatch cycles:
|
||||||
|
|
||||||
|
```
|
||||||
|
dispatch_cycles(cmd) = FIXED_PER_CMD + cmd.logical_bytes × R
|
||||||
|
```
|
||||||
|
|
||||||
|
where:
|
||||||
|
- `FIXED_PER_CMD` (cycles per command) models queue-tail update, MMIO-class
|
||||||
|
RTT, completion-event registration — fixed per command regardless of size.
|
||||||
|
- `R` (cycles per byte) models the queue-write bandwidth — bytes of the
|
||||||
|
command serialized into the scheduler queue.
|
||||||
|
- `cmd.logical_bytes` (int) is each command's *HW-logical* byte size,
|
||||||
|
computed from D2 below — not Python's `sys.getsizeof`.
|
||||||
|
|
||||||
|
PE_CPU emits `PeCpuOverheadCmd(cycles=dispatch_cycles(cmd))` before
|
||||||
|
dispatching, exactly as the existing hook (`tl_context.py:_emit_dispatch_
|
||||||
|
overhead`) — only the cycle value changes.
|
||||||
|
|
||||||
|
### D2. `logical_bytes` rule
|
||||||
|
|
||||||
|
Each PE command dataclass exposes `logical_bytes: int` (property). The
|
||||||
|
counting rule (HW-friendly, ignores Python overhead):
|
||||||
|
|
||||||
|
| Field kind | Bytes |
|
||||||
|
|---|---|
|
||||||
|
| Command framing (cmd-type discriminator + completion id ref) | 4 |
|
||||||
|
| Opcode (op kind enum) | 1 |
|
||||||
|
| Enum (scope, etc.) | 1 |
|
||||||
|
| `TensorHandle` reference (address only — shape/dtype assumed in descriptor table) | 8 |
|
||||||
|
| Scalar (int/float) | 4 |
|
||||||
|
| Tuple length marker | 1 |
|
||||||
|
|
||||||
|
`CompositeCmd` recursively sums its `ops` and `rw_handles`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def logical_bytes(self) -> int:
|
||||||
|
return (
|
||||||
|
4 # framing
|
||||||
|
+ 1 + sum(op.logical_bytes for op in self.ops)
|
||||||
|
+ 1 + 8 * len(self.rw_handles)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
`OpSpec`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def logical_bytes(self) -> int:
|
||||||
|
return (
|
||||||
|
1 + 1 # opcode + scope
|
||||||
|
+ 1 + 8 * len(self.operands) # named operand handles
|
||||||
|
+ (8 if self.out is not None else 0) # out handle
|
||||||
|
+ 1 + sum(_extra_bytes(v) for v in self.extra.values())
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _extra_bytes(v) -> int:
|
||||||
|
"""Type-aware byte count for OpSpec.extra values."""
|
||||||
|
if isinstance(v, bool): return 1
|
||||||
|
if isinstance(v, (int, float)): return 4
|
||||||
|
if isinstance(v, (tuple, list)): return 1 + 4 * len(v) # shape, axes, …
|
||||||
|
if isinstance(v, str): return 1 # opcode-like tag
|
||||||
|
return 4 # default scalar
|
||||||
|
|
||||||
|
|
||||||
|
# Example types in extra:
|
||||||
|
# m, k, n int → 4 each
|
||||||
|
# reduce_axis int → 4
|
||||||
|
# shape=(64, 64) tuple → 1 + 8 = 9
|
||||||
|
# factor=1.0 float → 4
|
||||||
|
```
|
||||||
|
|
||||||
|
(Identical rule for `DmaReadCmd`, `MathCmd`, etc. — one property per
|
||||||
|
dataclass, ~3 lines each.)
|
||||||
|
|
||||||
|
**Counting rule — per-op summation, no deduplication.** Each
|
||||||
|
`TensorHandle` reference inside an `OpSpec` is counted independently,
|
||||||
|
even when the same handle appears in multiple OpSpecs or in
|
||||||
|
`rw_handles`. The `rw_handles` block is metadata for cross-composite
|
||||||
|
hazard tracking (ADR-0065 D6.3) and is counted separately from
|
||||||
|
operand references in `ops`. There is no deduplication. This matches
|
||||||
|
HW reality: the descriptor encodes each operand slot as an independent
|
||||||
|
address field, and the dispatcher tracks `rw_handles` as a distinct
|
||||||
|
metadata block. Example: `OpSpec(kind="mul_bcast", operands={"src_a":
|
||||||
|
O, "src_b": corr}, out=O)` counts handle `O` *twice* (once for
|
||||||
|
`src_a`, once for `out`); if `O` is also in the enclosing
|
||||||
|
CompositeCmd's `rw_handles`, it is counted a third time.
|
||||||
|
|
||||||
|
### D3. Defaults — anchored on a typical composite ≈ 43 ns
|
||||||
|
|
||||||
|
Anchor: a **single-OpSpec composite for a DMA-staged GEMM path** —
|
||||||
|
one `OpSpec(kind="gemm", ...)`; DMA stages are auto-inserted by
|
||||||
|
PE_SCHEDULER from operand `space` (ADR-0065 D4) and **do not appear in
|
||||||
|
`logical_bytes`** (the kernel does not issue them as separate cmds).
|
||||||
|
Breakdown:
|
||||||
|
|
||||||
|
```
|
||||||
|
framing 4
|
||||||
|
ops tuple length 1
|
||||||
|
GEMM OpSpec 40 (opcode 1 + scope 1 + 1 + 2 handles 16
|
||||||
|
+ out 8 + 1 + extra m/k/n 12)
|
||||||
|
rw_handles tuple length 1
|
||||||
|
rw_handles content 8 (one RW handle for the output)
|
||||||
|
─────────────────────────────
|
||||||
|
total ~54 bytes
|
||||||
|
```
|
||||||
|
|
||||||
|
Target dispatch = ~43 ns. On-die producer→consumer queue at 16 bytes/cycle
|
||||||
|
(typical on-die descriptor queue width).
|
||||||
|
|
||||||
|
```
|
||||||
|
FIXED_PER_CMD = 40 cycles
|
||||||
|
R = 0.0625 cycles/byte (= 16 bytes/cycle)
|
||||||
|
```
|
||||||
|
|
||||||
|
Verification: `40 + 54 × 0.0625 = 43.375 cycles ≈ 43 ns` ✓
|
||||||
|
|
||||||
|
Cycle→ns conversion uses the PE node's existing `clock_freq_ghz` attr
|
||||||
|
(the same one used by PE_MATH `_compute_ns`). The cost-model knobs are
|
||||||
|
**cycle-domain only** — they do not duplicate the clock setting.
|
||||||
|
|
||||||
|
### D4. Topology config override
|
||||||
|
|
||||||
|
Defaults are baked into `pe_cpu.py`. Topology yaml may override under a
|
||||||
|
`pe_cost_model:` section at the PE node attrs (cycle-domain knobs only;
|
||||||
|
clock comes from the PE's existing `clock_freq_ghz`):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
pe:
|
||||||
|
attrs:
|
||||||
|
clock_freq_ghz: 1.0 # existing, used for cycle→ns
|
||||||
|
pe_cost_model:
|
||||||
|
fixed_per_cmd_cycles: 40
|
||||||
|
byte_cycles_recip: 0.0625 # = 16 bytes/cycle
|
||||||
|
max_composite_logical_bytes: 1024 # D7 — descriptor size cap
|
||||||
|
```
|
||||||
|
|
||||||
|
Missing keys fall back to defaults. The dispatch formula reads from
|
||||||
|
`node.attrs["pe_cost_model"]` at PE_CPU init.
|
||||||
|
|
||||||
|
### D5. Scope — what does and does not pay
|
||||||
|
|
||||||
|
| Path | Pays dispatch cost? |
|
||||||
|
|---|---|
|
||||||
|
| PE_CPU → PE_SCHEDULER for any `PeCommand` | **Yes** |
|
||||||
|
| `PeCpuOverheadCmd` itself (already cycles-explicit) | **No** (formula bypass) |
|
||||||
|
| Stages auto-generated by PE_SCHEDULER (DMA_READ/WRITE/FETCH/STORE) | **No** (PE_SCHEDULER-internal) |
|
||||||
|
| Engine compute latency (DMA `drain_ns`, GEMM/MATH `_compute_ns`) | **No change** — stays on engines (SPEC §0.1) |
|
||||||
|
|
||||||
|
This preserves the "latency on modelled components" invariant — dispatch
|
||||||
|
cost is *additional* CPU-side time, not folded into engine times.
|
||||||
|
|
||||||
|
### D6. Configurable values; goldens regenerate
|
||||||
|
|
||||||
|
Turning issue cost non-zero changes **every** bench's latency. Golden
|
||||||
|
latencies are **regenerated once** when this ADR lands — same posture as
|
||||||
|
ADR-0062 D3 lazy-load. After regeneration, the same calibration is in
|
||||||
|
effect for ADR-0065 opt2 measurement.
|
||||||
|
|
||||||
|
### D7. Composite size cap (hard limit — validation error)
|
||||||
|
|
||||||
|
Each `CompositeCmd`'s `logical_bytes` is capped at
|
||||||
|
**`max_composite_logical_bytes`** (default **1024 bytes**, overridable
|
||||||
|
per D4). A composite whose `logical_bytes` exceeds the cap is **rejected
|
||||||
|
at emit time** by the host-side TLContext with a `ValueError` — there is
|
||||||
|
**no automatic segmentation**. The kernel author must restructure the
|
||||||
|
recipe (e.g., split it into multiple smaller composites explicitly) so
|
||||||
|
each `CompositeCmd` fits within the descriptor capacity.
|
||||||
|
|
||||||
|
**Why a cap is needed.** Real hardware imposes hard limits — descriptor
|
||||||
|
queue entry size, scheduler parser buffer, command SRAM, firmware
|
||||||
|
input. Without a cap, the `FIXED + bytes × R` model would reward
|
||||||
|
arbitrarily large fused composites beyond what hardware accepts (e.g.,
|
||||||
|
fusing 100 primitive ops into one composite, paying one `FIXED`).
|
||||||
|
|
||||||
|
**Why 1024 bytes specifically.** This is a **safe engineering limit**,
|
||||||
|
not a measured HW number — intentionally chosen to be well above all
|
||||||
|
currently known composites (decode opt2's `#2` at ~322 bytes is the
|
||||||
|
largest in the kernbench codebase) while still representing a *finite*
|
||||||
|
descriptor capacity that future recipes must respect. The number is
|
||||||
|
overridable per topology (D4); when a real HW reference appears, the
|
||||||
|
value should be recalibrated. The role of this default is to make the
|
||||||
|
cap *exist as a discipline*, not to fit a specific HW.
|
||||||
|
|
||||||
|
**Rationale for a hard error over auto-segmentation.** Auto-splitting an
|
||||||
|
oversized composite (the original Revision 2 proposal) added emitter
|
||||||
|
complexity — inter-segment ordering, shared `rw_handles`, completion
|
||||||
|
chaining — to paper over what is, in practice, a kernel that asked for a
|
||||||
|
descriptor larger than the hardware allows. Surfacing it as an explicit
|
||||||
|
error keeps the emitter simple and makes the HW constraint visible to the
|
||||||
|
kernel author, who is best placed to decide how to split the work.
|
||||||
|
|
||||||
|
Decode opt2's `#2` composite (10 ops, ~322 bytes) sits comfortably
|
||||||
|
inside the 1024 cap — no error for the GQA workload.
|
||||||
|
|
||||||
|
### D8. Single-op-cmd fast-path (per-cmd-type FIXED)
|
||||||
|
|
||||||
|
Every **single-op** command — the DMA descriptors (`DmaReadCmd`,
|
||||||
|
`DmaWriteCmd`) *and* the single-op compute dispatches (`GemmCmd`,
|
||||||
|
`MathCmd`, `CopyCmd`) — carries a separate, lighter FIXED than the
|
||||||
|
general control-path cost: **`fixed_per_single_op_cmd_cycles`** (default
|
||||||
|
**8 cycles**) replaces `fixed_per_cmd_cycles` for all of them.
|
||||||
|
**`CompositeCmd` is the only command that stays on the general
|
||||||
|
control-path cost** `fixed_per_cmd_cycles` (= 40 cycles).
|
||||||
|
|
||||||
|
Concretely, the D1 formula becomes
|
||||||
|
```
|
||||||
|
dispatch_cycles(cmd) = FIXED(type(cmd)) + cmd.logical_bytes × R
|
||||||
|
FIXED(CompositeCmd) = fixed_per_cmd_cycles (= 40)
|
||||||
|
FIXED(everything else, i.e. every single-op cmd:
|
||||||
|
DmaReadCmd / DmaWriteCmd / GemmCmd / MathCmd / CopyCmd)
|
||||||
|
= fixed_per_single_op_cmd_cycles (= 8)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why a separate, lighter FIXED for single-op cmds.** D1 treated every
|
||||||
|
emitted command uniformly because that is the simplest defensible
|
||||||
|
model. In practice an single-op command — whether a DMA descriptor or a
|
||||||
|
single GEMM / elementwise / copy dispatch to an engine — is *much*
|
||||||
|
cheaper than the generic control-path cost the 40-cycle FIXED was
|
||||||
|
modeling. It is a single descriptor or instruction pushed to an engine
|
||||||
|
queue, with no scheduler-side plan to build:
|
||||||
|
|
||||||
|
- NVIDIA Hopper TMA: a single PTX instruction (`cp.async.bulk`) initiates
|
||||||
|
a bulk async DMA — ~1 ISA cycle on the SM side, with the TMA engine
|
||||||
|
doing fan-out and chunking itself.
|
||||||
|
- NVIDIA Ampere `cp.async`: ~1–2 cycles per warp-level async copy.
|
||||||
|
- A single MMA / tensor-core issue (`wgmma`, `mma.sync`): one
|
||||||
|
instruction to the math pipe, not a control-path round trip.
|
||||||
|
- AMD AQL packet write to a HSA queue: ~5–15 cycles.
|
||||||
|
- Generic descriptor-ring-push designs (Synopsys/Xilinx-style DMA IP):
|
||||||
|
~5–20 cycles for the MMIO writes.
|
||||||
|
- Tenstorrent tile descriptor emission, Habana Gaudi TPC descriptor RAM
|
||||||
|
+ start register: single-digit cycles per descriptor.
|
||||||
|
|
||||||
|
The 40-cycle FIXED is justified for `CompositeCmd` specifically, because
|
||||||
|
a composite is *not* a single dispatch: the scheduler must build a
|
||||||
|
tile-feeder plan, track per-tile read/write hazards across its internal
|
||||||
|
`DMA_READ → FETCH → GEMM → STORE → DMA_WRITE` stages, and wire a
|
||||||
|
completion handle. An single-op `GemmCmd` is one engine issue with none of
|
||||||
|
that machinery; charging it the same 40 cycles as a composite conflates
|
||||||
|
a single instruction with a whole scheduled plan.
|
||||||
|
|
||||||
|
**Why this matters for kernel evaluation.** A uniform 40-cycle FIXED
|
||||||
|
inflates the dispatch overhead of user-orchestrated single-op-primitive
|
||||||
|
kernels. A chunked-prefetching async GEMM at `K = N_K · TILE_K` emits,
|
||||||
|
per K-tile, a `DmaReadCmd` (B-chunk load) **and** a `GemmCmd` (the
|
||||||
|
per-chunk `tl.dot`) **and** a `MathCmd` (the running `tl.add`
|
||||||
|
accumulate) — so the per-command FIXED dominates its dispatch budget.
|
||||||
|
The single-op fast-path keeps the *structural* command-count signal that
|
||||||
|
D1 was designed to capture (a recipe that emits `N` single-op commands
|
||||||
|
*does* pay more than a recipe that emits one composite covering `N`
|
||||||
|
tiles internally) without conflating it with a modeling overcharge that
|
||||||
|
would be specific to a 40-cycle generic control path.
|
||||||
|
|
||||||
|
**Why 8 cycles specifically.** 8 sits at the mid-range of the survey
|
||||||
|
above (TMA ~1 to descriptor-ring ~15). It is intentionally a *modeling
|
||||||
|
default*, not a measured HW number — the role of this default is to
|
||||||
|
distinguish the single-op dispatch path from the composite control path in
|
||||||
|
qualitative behaviour. The value is overridable per topology (D4).
|
||||||
|
|
||||||
|
**Effect on composite numbers.** Composite tile-internal stages
|
||||||
|
(`DMA_READ`, `FETCH`, `GEMM`, `STORE`, `DMA_WRITE` per HW tile) are
|
||||||
|
emitted by the scheduler's tile-feeder loop, not by the host-side
|
||||||
|
TLContext, so they do **not** go through `_charge_dispatch`. The single
|
||||||
|
`CompositeCmd` itself still pays the 40-cycle control-path FIXED. D8
|
||||||
|
only changes the FIXED charged to user-side single-op primitives
|
||||||
|
(`tl.load` / `tl.store` / `tl.dot` / `tl.add` / …). Composite
|
||||||
|
measurements remain stable.
|
||||||
|
|
||||||
|
## Alternatives
|
||||||
|
|
||||||
|
### A1. Keep Revision 1's op-type calibration table
|
||||||
|
|
||||||
|
Rejected: calibration cost scales with |kinds|, and the *ratio* the
|
||||||
|
table tried to capture is structurally a function of cmd size. The
|
||||||
|
structural formula reaches the same qualitative behaviour with two
|
||||||
|
calibratable numbers instead of N.
|
||||||
|
|
||||||
|
### A2. Byte-only formula (no FIXED term)
|
||||||
|
|
||||||
|
Rejected. With FIXED = 0, opt2 (Option Y per ADR-0065) does **not** win
|
||||||
|
over opt3 — the total *bytes* dispatched per tile are similar (opt3 ≈ 232,
|
||||||
|
opt2 ≈ 380); the win is entirely in *fewer per-cmd fixed costs*. A
|
||||||
|
byte-only formula erases the very signal the model needs to expose.
|
||||||
|
|
||||||
|
### A3. Charge dispatch on PE_SCHEDULER instead of PE_CPU
|
||||||
|
|
||||||
|
Rejected: the saturation question is *"can the CPU push descriptors fast
|
||||||
|
enough to keep the engines busy?"* — that is a **PE_CPU** issue-bandwidth
|
||||||
|
property. Charging on the scheduler would not model CPU back-pressure.
|
||||||
|
|
||||||
|
### A4. Model DMA program/setup time as a separate fixed per-descriptor cost
|
||||||
|
|
||||||
|
Deferred: initially fold the descriptor-program cost into the **issuing
|
||||||
|
op's** dispatch cost. Split it out to a PE_DMA fixed setup only if
|
||||||
|
calibration shows it matters.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
- Hybrid's CPU-offload / saturation win (ADR-0060 §1) becomes
|
||||||
|
**measurable**, with a structurally honest model (no calibration table).
|
||||||
|
- Adding new op kinds (e.g., ADR-0065's `softmax_merge` 8-step recipe)
|
||||||
|
costs zero — they fit the same formula automatically.
|
||||||
|
- More faithful to hardware (queue-head MMIO RTT + queue-write bandwidth).
|
||||||
|
|
||||||
|
### Negative
|
||||||
|
- **All** bench goldens shift → one-time regeneration (D6); CI golden
|
||||||
|
fixtures update.
|
||||||
|
- Two calibration knobs (FIXED, R) need values; defaults are anchored on
|
||||||
|
a documented assumption — treat absolute latencies as provisional until
|
||||||
|
a reference exists; keep the **ratios** defensible.
|
||||||
|
- Adds a small `logical_bytes` property to each PE command dataclass.
|
||||||
|
|
||||||
|
## Open review items
|
||||||
|
|
||||||
|
1. **Calibration source for FIXED and R.** Defaults from "typical
|
||||||
|
composite ≈ 43 ns + on-die queue 16 bytes/cycle"; reasonable for an
|
||||||
|
on-die descriptor queue. Revisit when a HW reference appears.
|
||||||
|
2. **Scheduler plan-gen cost vs large composites.** Stays 0 — D5 keeps
|
||||||
|
PE_SCHEDULER's plan-generation outside the dispatch formula. The
|
||||||
|
D7 cap (1024 bytes ≈ 30–35 OpSpecs) bounds the worst case, but a
|
||||||
|
composite near the cap still costs the scheduler the same as a 1-op
|
||||||
|
composite under the current zero-cost model. If a stress test
|
||||||
|
(large-composite microbench) shows scheduler-bound behaviour,
|
||||||
|
expose `overhead_ns` per-op-count.
|
||||||
|
3. **Where the override lives.** `pe_cost_model:` block under PE node
|
||||||
|
attrs in topology yaml — keeps all knobs in one place, reviewable.
|
||||||
|
Clock comes from the PE's existing `clock_freq_ghz` attr, not
|
||||||
|
duplicated here.
|
||||||
|
4. **Path parity.** Both greenlet (`_execute_legacy` and `kernel_runner`)
|
||||||
|
and replay paths must read the same cost model. Verify.
|
||||||
|
5. **Sensitivity of conclusions to R.** opt2 < opt3 must hold across a
|
||||||
|
reasonable range of `R` (queue-bandwidth assumption). Sensitivity
|
||||||
|
sweep is part of Test Requirements (#9).
|
||||||
|
|
||||||
|
## Test Requirements
|
||||||
|
|
||||||
|
Tests are written against the **formula** (D1), not against specific
|
||||||
|
numeric anchors, so they remain valid when calibration changes or when
|
||||||
|
OpSpec/CompositeCmd fields are added.
|
||||||
|
|
||||||
|
1. **Formula preservation.** For any `CompositeCmd` `c`, PE_CPU's
|
||||||
|
recorded dispatch overhead equals `FIXED_PER_CMD + c.logical_bytes
|
||||||
|
× R` (within ±1 cycle for floor/round-off). Parametrized over
|
||||||
|
several composites: a 1-OpSpec GEMM composite, a 5-op MATH chain,
|
||||||
|
and a 10-op recipe composite. Default-calibration numbers (anchor
|
||||||
|
≈43 ns for the 1-OpSpec composite) are informative reference, not
|
||||||
|
the test gate — the test gate is the formula equality.
|
||||||
|
2. **Qualitative ratio (robust).** opt3 per-tile PE_CPU dispatch
|
||||||
|
strictly exceeds opt2 per-tile dispatch by at least a 2× margin —
|
||||||
|
`opt3 > 2 × opt2`. The default-calibration model predicts ≈4×;
|
||||||
|
the gate is the loose 2× bound so the test does not break when
|
||||||
|
calibration is moved (e.g., when a HW reference replaces the
|
||||||
|
default). Informative numbers — see ADR-0065 §verification and
|
||||||
|
DDD-0065 §11 for the model expectation.
|
||||||
|
3. **Override path.** Topology yaml `pe_cost_model:` block changes the
|
||||||
|
per-PE dispatch cost; default is recovered when block is missing.
|
||||||
|
The formula identity from #1 must hold with the override values.
|
||||||
|
4. **`PeCpuOverheadCmd` bypass.** Manual `tl.cycles(n)` issues exactly `n`
|
||||||
|
cycles, not `n + dispatch_cycles(...)`.
|
||||||
|
5. **No double-count.** PE_DMA `drain_ns`, PE_GEMM/MATH `_compute_ns`
|
||||||
|
identical to pre-ADR values.
|
||||||
|
6. **Determinism.** Identical inputs → identical op_log + latency
|
||||||
|
(SPEC §0.1).
|
||||||
|
7. **Path parity.** Greenlet and replay paths produce identical
|
||||||
|
dispatch-cycle accounting for the same kernel.
|
||||||
|
8. **Composite size cap (D7).** A composite that would emit `logical_bytes
|
||||||
|
> max_composite_logical_bytes` raises a `ValueError` at emit time (no
|
||||||
|
segmentation). A composite within the cap emits normally.
|
||||||
|
9. **Sensitivity (qualitative).** At `R ∈ {0.25, 0.0625, 0.03125}`
|
||||||
|
cycles/byte, `opt3 > opt2` at *all* three points. Direction (ratio
|
||||||
|
monotonically increases as R decreases) is also asserted, but
|
||||||
|
absolute ratio values are *not* required.
|
||||||
|
|
||||||
|
## Migration
|
||||||
|
|
||||||
|
ADR-0064 Revision 2 lands as a single PR with:
|
||||||
|
- `logical_bytes` property on each `PeCommand` dataclass (type-aware
|
||||||
|
extra-field counting per D2)
|
||||||
|
- formula application in `pe_cpu.py` dispatch path
|
||||||
|
- `pe_cost_model:` override read at PE_CPU init (cycle-domain knobs +
|
||||||
|
`max_composite_logical_bytes`)
|
||||||
|
- **composite size cap (D7)** — TLContext-side hard cap with
|
||||||
|
`max_composite_logical_bytes` default 1024: a composite over the cap
|
||||||
|
raises a `ValueError` at emit time (no auto-segmentation). Not
|
||||||
|
triggered by any existing bench (largest current composite is ~322
|
||||||
|
bytes), but the check lands so the descriptor-capacity limit is
|
||||||
|
enforced as recipes grow.
|
||||||
|
- one-time goldens regeneration
|
||||||
|
|
||||||
|
After this lands, ADR-0065 builds on top with no further goldens churn
|
||||||
|
in existing benches (ADR-0065 is a meaning-preserving refactor of
|
||||||
|
`CompositeCmd` for the existing path; only opt2 is a new bench).
|
||||||
@@ -0,0 +1,488 @@
|
|||||||
|
# ADR-0065: Flat-ops `CompositeCmd` + first stateful recipe (`softmax_merge`)
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
> Supporting ADR for **ADR-0060** (AHBM GQA Fused Attention). Implements
|
||||||
|
> the §5.6 / §8 item 4 carve-out exactly: opt2 of decode = (#1 existing
|
||||||
|
> GEMM composite for Q·Kᵀ) + (#2 a single composite carrying softmax +
|
||||||
|
> P·V + online-softmax merge). The "ex_composite" / "flash_pv_merge" name
|
||||||
|
> referenced in ADR-0060 §5.6 is *retired* — this ADR fixes the canonical
|
||||||
|
> shape using the existing `tl.composite` entry with two structural
|
||||||
|
> additions: (a) a flat `ops` tuple, and (b) a first stateful recipe
|
||||||
|
> `softmax_merge` that lowers to a sequence of MATH micro-ops.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
### What ADR-0060 left to this ADR
|
||||||
|
|
||||||
|
ADR-0060 §5.6 shipped opt3 (software pipelining) as the recommended now,
|
||||||
|
and explicitly carved out opt2 — fewer per-tile dispatches, K-before-V
|
||||||
|
DMA priority, **measurable only after ADR-0064**'s cost model — as the
|
||||||
|
follow-up.
|
||||||
|
|
||||||
|
§8 item 4 sized the carve-out: "if revisited, split it into **two**
|
||||||
|
composites — `#1` = Q·Kᵀ (existing composite + `scale`), `#2` = softmax +
|
||||||
|
P·V + online-softmax accumulator merge". This ADR implements `#2`.
|
||||||
|
|
||||||
|
### Why composite needs a structural change
|
||||||
|
|
||||||
|
Today's `CompositeCmd` has the shape `(op, a, b, out_addr, ops=(head,
|
||||||
|
*epi))` with implicit conventions:
|
||||||
|
- `op` selects the head engine (gemm | math)
|
||||||
|
- `ops[0]` is the head OpSpec, `ops[1:]` are epilogue OpSpecs that run
|
||||||
|
*after* the head per-output-tile or per-K-tile (scope-determined)
|
||||||
|
|
||||||
|
For decode opt2 we need MATH ops to run **before** the head GEMM (the
|
||||||
|
online-softmax `m, l, O` update + emit `P` for the GEMM input). The
|
||||||
|
current shape has no slot for that.
|
||||||
|
|
||||||
|
Two failed paths considered earlier:
|
||||||
|
- Embedding softmax_merge as an *epilogue* of #1 (Q·Kᵀ) — wrong: it
|
||||||
|
needs to read `Sj` (= #1's output) and writes a fresh `P` consumed by
|
||||||
|
the *next* GEMM (#2's P·V); circular.
|
||||||
|
- A single mixed-engine recipe `flash_pv_merge` absorbing MATH + GEMM +
|
||||||
|
MATH — violates the engine boundary, complicates PE_SCHEDULER.
|
||||||
|
|
||||||
|
The clean shape is: **drop the head/epilogue distinction from the
|
||||||
|
command format**. Make `ops` a flat ordered tuple; let each op's
|
||||||
|
`scope` + position determine its tile-loop placement. The "prologue"
|
||||||
|
concept stays in the *user-facing* `tl.composite(...)` API as
|
||||||
|
ergonomics — but the **HW command never sees it**.
|
||||||
|
|
||||||
|
### Why a recipe (not a generic op list)
|
||||||
|
|
||||||
|
Decode opt2's `#2` MATH chain is 8 steps with a fixed structure (online
|
||||||
|
softmax merge). Letting the kernel author wire those 8 steps would be
|
||||||
|
both verbose and a hazard surface (correctness depends on step order).
|
||||||
|
A *recipe* — a single named op (`softmax_merge`) that TLContext expands
|
||||||
|
to the 8 steps with addresses pre-filled — keeps the kernel concise and
|
||||||
|
the structure under one source of truth. The recipe table lives in
|
||||||
|
TLContext (the compiler analog); PE_SCHEDULER never reads it. See
|
||||||
|
boundary in D5.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### D1. `CompositeCmd` is a flat ordered op list
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class CompositeCmd:
|
||||||
|
completion: CompletionHandle
|
||||||
|
ops: tuple[OpSpec, ...] # ordered MATH/GEMM ops
|
||||||
|
rw_handles: tuple[TensorHandle, ...] = () # cross-composite hazard
|
||||||
|
data_op: bool = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def logical_bytes(self) -> int: ... # ADR-0064 D2
|
||||||
|
```
|
||||||
|
|
||||||
|
The previous `(op, a, b, out_addr, out_nbytes, math_op)` fields are
|
||||||
|
absorbed into `ops`. The first OpSpec in `ops` (if `kind == "gemm"`) is
|
||||||
|
the head GEMM; preceding OpSpecs are pre-loop ops, following OpSpecs
|
||||||
|
are post-loop / per-tile epilogue ops depending on `scope`.
|
||||||
|
|
||||||
|
### D2. `OpSpec` carries named operands
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class OpSpec:
|
||||||
|
kind: str # "gemm" | "rmax" | ...
|
||||||
|
scope: Scope # KERNEL | K_TILE | OUTPUT_TILE
|
||||||
|
operands: dict[str, TensorHandle] # named — see D5
|
||||||
|
extra: dict[str, Any] # scalars, axes, m/k/n, etc.
|
||||||
|
out: TensorHandle | None = None # explicit write-back handle
|
||||||
|
|
||||||
|
@property
|
||||||
|
def logical_bytes(self) -> int: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Named operands let PE_SCHEDULER route inputs to engine ports (e.g., GEMM
|
||||||
|
takes `operands["a"]`, `operands["b"]`) without positional convention.
|
||||||
|
|
||||||
|
### D3. Position + scope determines tile-loop placement
|
||||||
|
|
||||||
|
**Semantics split.** *Position* determines the **phase** (where in the
|
||||||
|
tile-loop lifecycle the op fires); *scope* determines the **repetition**
|
||||||
|
within that phase. `Scope.KERNEL` means "**once per `CompositeCmd`
|
||||||
|
invocation**", not "once per kernel launch" — its single firing happens
|
||||||
|
at the op's position in the sequence. The same KERNEL value carries
|
||||||
|
different *phases* depending on whether the OpSpec sits before, at, or
|
||||||
|
after the GEMM op.
|
||||||
|
|
||||||
|
PE_SCHEDULER scans `cmd.ops` for the GEMM op (≤1 per CompositeCmd, see
|
||||||
|
D6). Calling its index `g`:
|
||||||
|
|
||||||
|
| OpSpec position | scope | Phase | Placement in plan |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `0 .. g-1` | KERNEL | pre-tile-loop | run once before tile loop |
|
||||||
|
| `g` | KERNEL | head GEMM | drives tile loop with `extra["m"], extra["k"], extra["n"]` |
|
||||||
|
| `g+1 .. ` | K_TILE | in-accumulation | per K-tile epilogue (inside K accumulation loop) |
|
||||||
|
| `g+1 .. ` | OUTPUT_TILE | per-output-tile | per (m, n) tile epilogue (after K accumulation) |
|
||||||
|
| `g+1 .. ` | KERNEL | post-tile-loop | run once after tile loop |
|
||||||
|
|
||||||
|
A composite with no GEMM op (e.g., MATH-only composite) treats all ops
|
||||||
|
as KERNEL-scope sequential — phase collapses to "single-shot in order".
|
||||||
|
|
||||||
|
### D4. PE_SCHEDULER auto-inserts DMAs from the operand `pinned` flag
|
||||||
|
|
||||||
|
PE_SCHEDULER scans the GEMM's `operands`. For each:
|
||||||
|
- **not `pinned`** (data lives in HBM, must be streamed) → emit DMA_READ
|
||||||
|
Stage before the FETCH/GEMM Stages
|
||||||
|
- **`pinned`** (already staged in TCM via a prior `tl.load`, or a recipe's
|
||||||
|
TCM scratch / primary-out `P`) → no DMA Stage, consumed in place
|
||||||
|
|
||||||
|
The kernel never emits explicit DMA cmds for composite operands;
|
||||||
|
PE_SCHEDULER infers them from handles. `tl.ref` operands are not pinned
|
||||||
|
(DMA_READ); `tl.load` results and recipe scratch / primary-out are pinned
|
||||||
|
(in place).
|
||||||
|
|
||||||
|
> **As-built note (P3).** An earlier draft of D4 derived the DMA decision
|
||||||
|
> from a `space` tag (`hbm`→DMA, `tcm`→in place) and would have flipped the
|
||||||
|
> current `space` values (today `tl.load`=`hbm`, `tl.ref`=`tcm`). The
|
||||||
|
> implementation **keeps the existing `pinned` flag** as the DMA signal
|
||||||
|
> instead — `pinned` already encodes "already in TCM, skip DMA", flipping
|
||||||
|
> `space` would change existing bench Stage sequences for no functional
|
||||||
|
> gain. Unifying `space` and retiring `pinned` is a deferred low-priority
|
||||||
|
> cleanup.
|
||||||
|
|
||||||
|
**Prologue recipe ops are TCM-only (Phase 1 limit, enforced at TLContext
|
||||||
|
emit).** Every operand of a *prologue recipe* (non-GEMM) OpSpec **must** be
|
||||||
|
TCM-resident; passing an HBM handle as a recipe operand raises a validation
|
||||||
|
error at emit time. This bounds DMA staging to the head op's operands in
|
||||||
|
Phase 1. The **head op itself** (gemm *or* math) keeps the existing
|
||||||
|
DMA-staged-from-HBM behavior — D4's TCM-only rule does **not** apply to it.
|
||||||
|
This invariant is also restated in D6 #7.
|
||||||
|
|
||||||
|
### D5. RECIPE_DESCRIPTORS — TLContext-internal, NOT in `pe_commands.py`
|
||||||
|
|
||||||
|
Recipes live in a new TLContext-adjacent module
|
||||||
|
(`src/kernbench/triton_emu/tl_recipes.py`). PE_SCHEDULER does **not**
|
||||||
|
import it. The first recipe:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class RecipeDescriptor:
|
||||||
|
# Operand contract: name → "R" (read-only) | "RW" (read-write)
|
||||||
|
operands: dict[str, str]
|
||||||
|
# Type / shape rule for the implicit primary output (auto-bound to
|
||||||
|
# head's first matrix operand). None means recipe emits no value.
|
||||||
|
primary_out: PrimaryOutSpec | None
|
||||||
|
# Single-shot prologue (full reduction) vs tile-aligned execution.
|
||||||
|
tile_alignment: Literal["single_shot", "tile_aligned"]
|
||||||
|
# Internal scratch budget (bytes), function of operand dims.
|
||||||
|
internal_scratch_bytes_fn: Callable[..., int]
|
||||||
|
# Engine micro-op sequence: TLContext expands this into flat OpSpecs.
|
||||||
|
engine_seq: tuple[EngineOp, ...]
|
||||||
|
|
||||||
|
|
||||||
|
RECIPE_DESCRIPTORS["softmax_merge"] = RecipeDescriptor(
|
||||||
|
operands={"s": "R", "m": "RW", "l": "RW", "O": "RW"},
|
||||||
|
primary_out=PrimaryOutSpec(
|
||||||
|
from_shape="s", from_dtype="s", transform="identity",
|
||||||
|
),
|
||||||
|
tile_alignment="single_shot",
|
||||||
|
internal_scratch_bytes_fn=lambda G, TILE, d, bpe: bpe * (
|
||||||
|
G + G + G + G * TILE + G # m_loc + m_new + corr + P + l_loc
|
||||||
|
),
|
||||||
|
engine_seq=(
|
||||||
|
EngineOp("MATH", "rmax", src="s", dst="m_loc", reduce_axis=-1),
|
||||||
|
EngineOp("MATH", "max_elem", src_a="m", src_b="m_loc", dst="m_new"),
|
||||||
|
EngineOp("MATH", "exp_diff", src_a="m", src_b="m_new", dst="corr"),
|
||||||
|
EngineOp("MATH", "exp_diff", src_a="s", src_b="m_new", dst="P", bcast_axis=0),
|
||||||
|
EngineOp("MATH", "rsum", src="P", dst="l_loc", reduce_axis=-1),
|
||||||
|
EngineOp("MATH", "fma", src_a="l", src_b="corr", src_c="l_loc", dst="l"),
|
||||||
|
EngineOp("MATH", "mul_bcast", src_a="O", src_b="corr", dst="O", bcast_axis=1),
|
||||||
|
EngineOp("MATH", "copy", src="m_new", dst="m"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
TLContext, on `tl.composite(prologue=[{"op": "softmax_merge", ...}],
|
||||||
|
op="gemm", ...)`:
|
||||||
|
1. Validates operand R/RW against `RECIPE_DESCRIPTORS["softmax_merge"]`.
|
||||||
|
2. Allocates scratch (m_loc, m_new, corr, P, l_loc) via the scratch_scope
|
||||||
|
helper (ADR-0063).
|
||||||
|
3. Derives the primary output P's shape from `s.shape` (identity).
|
||||||
|
4. Expands `engine_seq` into 8 flat MATH OpSpecs with all addresses /
|
||||||
|
sizes filled. Each OpSpec gets `scope=KERNEL`.
|
||||||
|
5. **Auto-binds (with conflict check, D6.6).** If the head GEMM does
|
||||||
|
*not* explicitly provide `operands["a"]`, auto-bind
|
||||||
|
`operands["a"] = P_handle`. If the kernel *did* provide `a`
|
||||||
|
explicitly, emit a validation error — ambiguous primary-output
|
||||||
|
replacement.
|
||||||
|
6. Emits `CompositeCmd(ops=(8 MATH + 1 GEMM + epilogue), rw_handles=
|
||||||
|
(m, l, O))`.
|
||||||
|
|
||||||
|
**RECIPE_DESCRIPTORS appears nowhere in the HW path**. PE_SCHEDULER
|
||||||
|
sees only the flat ops list.
|
||||||
|
|
||||||
|
### D6. Invariants
|
||||||
|
|
||||||
|
1. **GEMM count.** `0 ≤ count(op.kind == "gemm" in cmd.ops) ≤ 1`. A
|
||||||
|
composite with 0 GEMMs is MATH-only.
|
||||||
|
2. **softmax_merge has no V operand.** `RECIPE_DESCRIPTORS["softmax_merge"
|
||||||
|
].operands` does not include any HBM-resident handle → no DMA inserted
|
||||||
|
during the prologue → V DMA can only happen during the GEMM head →
|
||||||
|
natural K-before-V DMA priority across #1 / #2 of decode opt2.
|
||||||
|
3. **Strict FIFO across composites.** PE_SCHEDULER tracks in-flight
|
||||||
|
`rw_handles`. A new composite whose `rw_handles` (or any of its op
|
||||||
|
inputs) intersect with an in-flight composite's `rw_handles` waits
|
||||||
|
for all earlier composites to complete — no out-of-order reorder.
|
||||||
|
4. **No backwards-incompatibility for legacy callers.** The user-facing
|
||||||
|
API `tl.composite(op="gemm", a, b, epilogue=[...])` is preserved.
|
||||||
|
TLContext internally lowers it to a flat-ops `CompositeCmd`.
|
||||||
|
5. **PE_SCHEDULER recipe-free.** PE_SCHEDULER does not import
|
||||||
|
RECIPE_DESCRIPTORS, does not know that "softmax_merge" exists, and
|
||||||
|
has no recipe-specific code branches.
|
||||||
|
6. **No implicit operand replacement (auto-bind conflict).** If a
|
||||||
|
prologue recipe declares a `primary_out` AND the head op's auto-bind
|
||||||
|
target operand (e.g., GEMM `a`) is *also* provided by the kernel
|
||||||
|
explicitly, TLContext raises a validation error at emit time. The
|
||||||
|
kernel must either let the recipe bind (omit the operand) **or**
|
||||||
|
specify it explicitly (omit the prologue, or use a recipe without
|
||||||
|
`primary_out`). This prevents the ambiguity of "which value wins".
|
||||||
|
7. **Prologue MATH operands TCM-only.** Restatement of D4: every operand
|
||||||
|
of a *prologue recipe* (non-GEMM) OpSpec must be TCM-resident. The head
|
||||||
|
op (gemm or math) is exempt — it may stream from HBM. Phase 1 enforced
|
||||||
|
at TLContext emit.
|
||||||
|
|
||||||
|
### D7. Boundary summary (compiler vs scheduler vs engine)
|
||||||
|
|
||||||
|
```
|
||||||
|
HOST DEVICE
|
||||||
|
TLContext (compiler analog) PE_SCHEDULER (scheduler/dispatcher)
|
||||||
|
- validate against RECIPE_DESC. - scan flat ops list
|
||||||
|
- derive primary_out shape - identify GEMM (≤1)
|
||||||
|
- allocate scratch - position-based stage placement
|
||||||
|
- expand recipe → flat OpSpecs - auto-insert DMA stages from space
|
||||||
|
- compute rw_handles - strict-FIFO RW hazard tracking
|
||||||
|
- emit CompositeCmd - emit Stage list
|
||||||
|
imports: RECIPE_DESCRIPTORS imports: nothing recipe-related
|
||||||
|
|
||||||
|
ENGINES (PE_MATH / PE_GEMM / PE_DMA)
|
||||||
|
- read Stage.params (op_kind, addresses, n_elements)
|
||||||
|
- existing latency models unchanged
|
||||||
|
imports: nothing recipe-related
|
||||||
|
```
|
||||||
|
|
||||||
|
### D8. Composite output handle + output-space DMA
|
||||||
|
|
||||||
|
`tl.composite(...)` returns the **output `TensorHandle`** (not a bare
|
||||||
|
`CompletionHandle`), so a composite's result can feed a downstream op the
|
||||||
|
same way `tl.dot`'s result does. The handle's `pending` field references
|
||||||
|
the composite's completion; downstream consumers auto-await it
|
||||||
|
(`_await_pending`, ADR-0062 lazy pattern) and `tl.wait(handle)` works.
|
||||||
|
|
||||||
|
**`out` is always a `TensorHandle`** (never a raw address) — its location is
|
||||||
|
the kernel's choice, never guessed:
|
||||||
|
|
||||||
|
- **HBM output** → `out=tl.ref(addr, shape)`. `tl.ref` returns a handle with
|
||||||
|
`space="hbm"`, so the composite writes it back via DMA_WRITE. The
|
||||||
|
STORE + DMA_WRITE stay **inside** the composite pipeline — they are part of
|
||||||
|
the fused op, *not* a separate `tl.store`.
|
||||||
|
- **In-place TCM** → `out=<existing TCM handle>` (e.g. the recipe accumulator
|
||||||
|
`O`). `space="tcm"` → STORE only, result stays in TCM (chainable).
|
||||||
|
- **Omitted** → TLContext **auto-allocates a TCM scratch** (like `tl.dot`)
|
||||||
|
and returns it.
|
||||||
|
|
||||||
|
For ergonomics, `out_ptr: int` is accepted as **shorthand for an HBM
|
||||||
|
output** — equivalent to `out=tl.ref(out_ptr, <output shape>)` (the compiler
|
||||||
|
wraps it into an `space="hbm"` handle). When no output is specified the
|
||||||
|
compiler owns the scratch address, never the kernel. (`tl.ref` returns
|
||||||
|
`space="hbm"` — it references HBM data. The operand-**input** DMA decision
|
||||||
|
stays `pinned`-based per D4, so this label does not affect input streaming.)
|
||||||
|
|
||||||
|
The output handle's **`space` drives the tile loop's write-back** (the
|
||||||
|
output analog of the operand `pinned` rule in D4):
|
||||||
|
|
||||||
|
| `out.space` | tile-loop write-back |
|
||||||
|
| --- | --- |
|
||||||
|
| `"tcm"` | STORE only — result stays in TCM (chainable; **no DMA_WRITE**) |
|
||||||
|
| `"hbm"` | STORE + DMA_WRITE — final HBM output |
|
||||||
|
|
||||||
|
This makes a TCM-resident composite output (e.g. a recipe accumulator) legal
|
||||||
|
**and** keeps a TCM-scratch address out of the DMA engine's PA decoder
|
||||||
|
(which would otherwise reject the high-bit scratch address). Existing benches
|
||||||
|
wrap their HBM output via `tl.ref` → write-back unchanged (byte-equal).
|
||||||
|
|
||||||
|
## Alternatives
|
||||||
|
|
||||||
|
### A1. Three composites per tile (no prologue concept)
|
||||||
|
|
||||||
|
Split #2 further into `softmax_merge` + `gemm(P·V)` + `add(O)` as three
|
||||||
|
separate CompositeCmds. Pros: no flat-ops restructuring (`add` reuses
|
||||||
|
existing epilogue path). Cons: 3 dispatches per tile instead of 2 —
|
||||||
|
diverges from ADR-0060 §8 item 4 sizing ("split into **two**"); loses
|
||||||
|
~32 ns × N_tiles of fixed-cost savings. **Rejected.**
|
||||||
|
|
||||||
|
### A2. Mixed-engine recipe (`flash_pv_merge`)
|
||||||
|
|
||||||
|
One recipe absorbing MATH + GEMM + MATH (ADR-0060 §5.6 original
|
||||||
|
phrasing). Pros: tightest dispatch (1 composite). Cons: PE_SCHEDULER
|
||||||
|
must know engine-boundary recipes, violates the "engines see only
|
||||||
|
Stage.op_kind" boundary, recipe table becomes ISA-like and pollutes
|
||||||
|
HW interface. **Rejected.**
|
||||||
|
|
||||||
|
### A3. Generic op-list DAG (`tl.ex_composite([...])`)
|
||||||
|
|
||||||
|
User-defined sequence of any ops with named slots and dataflow analysis.
|
||||||
|
Pros: maximally flexible. Cons: a single user (softmax_merge) does not
|
||||||
|
justify a generic DAG language; PE_SCHEDULER would need dataflow
|
||||||
|
analysis; reintroduces ADR-0060 §8 item 4's "absorb the inner loop"
|
||||||
|
trap. **Rejected (Simplicity First).**
|
||||||
|
|
||||||
|
### A4. RW-aware scheduler reorder (not strict FIFO)
|
||||||
|
|
||||||
|
PE_SCHEDULER could allow a later CompositeCmd to overtake an in-flight
|
||||||
|
one when their `rw_handles` don't intersect (e.g., next-tile #1 could
|
||||||
|
overlap with this-tile #2 since #1 doesn't touch m/l/O). Pros: more
|
||||||
|
overlap. Cons: more scheduler state, gains are bounded (next #1 waits
|
||||||
|
for K DMA anyway). **Deferred** — strict FIFO ships first; RW-aware
|
||||||
|
reorder may be a future follow-up ADR.
|
||||||
|
|
||||||
|
### A5. Embedding softmax_merge as an epilogue of #1 (Q·Kᵀ)
|
||||||
|
|
||||||
|
Wrong: softmax_merge's output `P` is the input of #2's P·V GEMM, which
|
||||||
|
runs *after* #1's epilogue. Embedding it would force #2 to read its own
|
||||||
|
input from #1's epilogue scratch — circular tile-loop dependency.
|
||||||
|
**Rejected (incorrect).**
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
- ADR-0060 §5.6 / §8 item 4 carve-out implemented exactly: opt2 = 2
|
||||||
|
composites per tile, K-before-V DMA priority natural.
|
||||||
|
- Composite contract becomes uniform: flat ops list with scope-based
|
||||||
|
placement. No structural distinction between "head" and "epilogue"
|
||||||
|
in the HW cmd.
|
||||||
|
- DMAs auto-inferred from operand `space`; kernel surface simpler.
|
||||||
|
- HW interface (`CompositeCmd` shape) stays small even as recipes grow
|
||||||
|
— recipe expansion happens host-side.
|
||||||
|
- New fused patterns (linear+gelu+norm, rmsnorm+linear, …) can be added
|
||||||
|
by inserting one `RECIPE_DESCRIPTORS` entry; no PE_SCHEDULER changes.
|
||||||
|
|
||||||
|
### Negative
|
||||||
|
- CompositeCmd's struct shape changes — a refactor for all current
|
||||||
|
callers. Semantics preserved (D6.4); existing goldens unchanged once
|
||||||
|
ADR-0064 Revision 2 lands.
|
||||||
|
- `OpSpec.operands` changes from positional `tuple[Any, ...]` to
|
||||||
|
`dict[str, TensorHandle]` — touches the existing epilogue lowering.
|
||||||
|
- New TLContext code: recipe expansion + scratch-slot allocation +
|
||||||
|
primary_out auto-bind. ~80–120 LOC.
|
||||||
|
- PE_SCHEDULER's `_generate_plan` gets a position-based scan + DMA
|
||||||
|
auto-insertion + strict-FIFO RW tracker. ~60–100 LOC.
|
||||||
|
- **Strict-FIFO RW hazard tracking is safe but conservative.** When
|
||||||
|
two composites share any `rw_handle`, the new one waits for *all*
|
||||||
|
prior overlapping composites to fully complete — even when their
|
||||||
|
compute could in principle overlap (e.g., next-tile #1 = Q·Kᵀ does
|
||||||
|
not touch m/l/O and could overlap with this-tile's #2, but FIFO
|
||||||
|
still serializes any path that touches the same handles down the
|
||||||
|
line). This **may under-expose composite-level overlap** relative to
|
||||||
|
a smarter scheduler. RW-aware reorder (A4) is the deferred
|
||||||
|
improvement.
|
||||||
|
|
||||||
|
## Open review items
|
||||||
|
|
||||||
|
1. **Recipe shape derivation rule beyond identity.** Future recipes may
|
||||||
|
need non-identity transforms (e.g., transposed primary_out). The
|
||||||
|
`PrimaryOutSpec.transform` field is forward-compat; add new
|
||||||
|
transforms as needed.
|
||||||
|
2. **Recipe scratch allocator** integration with ADR-0063's
|
||||||
|
`scratch_scope`. Initial wiring: TLContext allocates inside the
|
||||||
|
current `scratch_scope` context if active, else in a kernel-scope
|
||||||
|
persistent slot.
|
||||||
|
3. **`tl_recipes.py` location.** New module under `triton_emu/`. Keeps
|
||||||
|
recipe knowledge co-located with the compiler analog (TLContext).
|
||||||
|
4. **GEMM `out=TensorHandle`.** New form alongside legacy `out_ptr:
|
||||||
|
int`. Both accepted; handle form preferred for new code (enables
|
||||||
|
strict-FIFO RW tracking by handle identity).
|
||||||
|
|
||||||
|
## Test Requirements
|
||||||
|
|
||||||
|
1. **CompositeCmd flat-ops refactor — meaning preservation.** Every
|
||||||
|
existing bench's op_log is byte-equal pre/post refactor (legacy API
|
||||||
|
`tl.composite(op="gemm", a, b, epilogue=[...])` lowers to the same
|
||||||
|
Stage sequence).
|
||||||
|
2. **softmax_merge recipe lowering.** TLContext call `tl.composite(
|
||||||
|
prologue=[{"op": "softmax_merge", "s": Sj, "m": m, "l": l, "O": O}],
|
||||||
|
op="gemm", b=V_ref, out=O, epilogue=[{"op": "add", "other": O}])`
|
||||||
|
produces a `CompositeCmd` with exactly 10 ops in the expected order
|
||||||
|
(8 MATH + 1 GEMM + 1 MATH(add)) and `rw_handles == (m, l, O)`.
|
||||||
|
3. **K-before-V DMA priority invariant.** op_log of decode opt2 shows
|
||||||
|
no V-related DMA during #2's MATH prologue (#2's V DMA begins only
|
||||||
|
when the GEMM head starts).
|
||||||
|
4. **Strict-FIFO RW serialization.** Two consecutive composites sharing
|
||||||
|
any `rw_handle` complete in dispatch order; their stages do not
|
||||||
|
interleave.
|
||||||
|
5. **DMA auto-insertion from `pinned`.** A GEMM composite with a
|
||||||
|
not-`pinned` operand (e.g. `b=tl.ref(...)`) emits a DMA_READ Stage; a
|
||||||
|
`pinned` operand (e.g. a recipe's TCM scratch / primary-out, or a
|
||||||
|
`tl.load` result) does not. (As-built D4 note: the DMA decision uses
|
||||||
|
the existing `pinned` flag, not a `space` tag.)
|
||||||
|
6. **opt2 numeric parity with opt3.** In data mode, opt2's final
|
||||||
|
`(m, l, O)` matches opt3's within fp tolerance.
|
||||||
|
7. **opt2 dispatch ratio (after ADR-0064 Rev2) — robust.** opt3 vs
|
||||||
|
opt2 per-tile PE_CPU dispatch cycles satisfies `opt3 > 2 × opt2`
|
||||||
|
(qualitative gate; calibration-independent). The default-calibration
|
||||||
|
model expectation is ≈ 4.0× (FIXED=40 cycles, R=0.0625 cycles/byte)
|
||||||
|
— see DDD-0065 §11 for the model-derived numbers; only the loose
|
||||||
|
`> 2×` bound is the test gate so it survives calibration changes.
|
||||||
|
Ratio is FIXED-dominated, reflecting command-count reduction as the
|
||||||
|
primary signal.
|
||||||
|
8. **GEMM-count invariant.** A composite with two GEMM OpSpecs raises a
|
||||||
|
validation error at TLContext emit.
|
||||||
|
9. **Within composite size cap (ADR-0064 D7).** Decode opt2's `#2`
|
||||||
|
composite (10 ops, ~322 logical bytes — per-op summation of operand
|
||||||
|
handles) fits comfortably under the default
|
||||||
|
`MAX_COMPOSITE_LOGICAL_BYTES=1024` — no segmentation. The recipe
|
||||||
|
lowering test asserts `cmd.logical_bytes < 1024`.
|
||||||
|
10. **Auto-bind conflict.** `tl.composite(op="gemm", a=A_handle,
|
||||||
|
prologue=[{"op": "softmax_merge", ...}], ...)` where softmax_merge
|
||||||
|
declares `primary_out` raises a validation error at emit time
|
||||||
|
(D6.6).
|
||||||
|
11. **MATH operand TCM invariant.** `tl.composite(op="math",
|
||||||
|
a=tl.ref(addr, shape), ...)` (passing an HBM-resident handle as a
|
||||||
|
MATH operand) raises a validation error at emit time (D6.7).
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **ADR-0060** §5.6, §8 item 4 — the carve-out this ADR implements.
|
||||||
|
- **ADR-0064 Revision 2** — dispatch cost model; lands first, makes
|
||||||
|
opt2's fewer-issues win measurable.
|
||||||
|
- **ADR-0063** `scratch_scope` — recipe intermediates allocated inside;
|
||||||
|
`m, l, O` allocated outside as persistent arena (per DDD-0060 §6.2).
|
||||||
|
- **ADR-0046** `tl_context` contract — extended by `prologue=[...]`
|
||||||
|
kwarg, `out=TensorHandle`, RECIPE_DESCRIPTORS lookup.
|
||||||
|
- **ADR-0042** tile plan generators — extended (not redesigned) to
|
||||||
|
process the flat ops list and auto-insert DMAs.
|
||||||
|
- **ADR-0014** PE pipeline — boundary preserved: scheduler stays
|
||||||
|
recipe-free, engines stay op_kind-opaque.
|
||||||
|
|
||||||
|
## Migration
|
||||||
|
|
||||||
|
> **Implementation ordering note.** The ADR-0064-first ordering below was
|
||||||
|
> the original plan. The actual implementation **reversed steps 1 and 2** —
|
||||||
|
> ADR-0065 Phase 1 (flat-ops `CompositeCmd`) landed *before* ADR-0064 Rev2.
|
||||||
|
> Reason: `logical_bytes` (ADR-0064 D2) is naturally defined on the flat-ops
|
||||||
|
> shape, so doing the refactor first avoids a throwaway transitional
|
||||||
|
> `logical_bytes`. Each step is independently safe: P1 is a meaning-
|
||||||
|
> preserving refactor (op_log byte-equal) under the pre-existing cost path;
|
||||||
|
> ADR-0064 Rev2 then swaps in the structural formula on the already-flat
|
||||||
|
> shape. The end state is identical either way.
|
||||||
|
|
||||||
|
Land order (original plan; see note above for the as-built reversal of 1↔2):
|
||||||
|
1. **ADR-0064 Revision 2** (separate PR): structural dispatch cost +
|
||||||
|
`logical_bytes` property + topology config override + one-time
|
||||||
|
goldens regeneration.
|
||||||
|
2. **ADR-0065 Phase 1** (this ADR, PR a): `CompositeCmd` flat-ops
|
||||||
|
restructure + legacy-API lowering. Refactor only; goldens unchanged.
|
||||||
|
3. **ADR-0065 Phase 2** (this ADR, PR b): `tl_recipes.py` +
|
||||||
|
`softmax_merge` recipe + PE_SCHEDULER position-scan + DMA
|
||||||
|
auto-insertion + strict-FIFO RW tracker.
|
||||||
|
4. **ADR-0065 Phase 3** (this ADR, PR c): `_gqa_decode_long.py` opt2
|
||||||
|
variant + numeric parity test + dispatch-ratio measurement against
|
||||||
|
opt3 baseline.
|
||||||
|
|
||||||
|
Detailed implementation plan: see **DDD-0065**.
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
# ADR Index
|
||||||
|
|
||||||
|
Auto-generated by `tools/generate_adr_index.py`. Total ADRs: **47**.
|
||||||
|
|
||||||
|
Classification mirrors the `/report` skill's section assignment. When adding a new ADR, also add an entry to the `CLASSIFICATION` table in `tools/generate_adr_index.py`.
|
||||||
|
|
||||||
|
## Design Principles
|
||||||
|
|
||||||
|
- [ADR-0013](./ADR-0013-ver-verification-strategy.md) — Verification Strategy and Phase 1 Test Plan
|
||||||
|
- [ADR-0033](./ADR-0033-lat-latency-model-assumptions.md) — Latency Model: Assumptions and Known Simplifications
|
||||||
|
|
||||||
|
## High-level Architecture
|
||||||
|
|
||||||
|
- [ADR-0003](./ADR-0003-dev-target-system-hierarchy.md) — Target System Hierarchy & Modeling Scope _(System hierarchy (Tray / SIP / CUBE / PE))_
|
||||||
|
- [ADR-0007](./ADR-0007-api-runtime-api-boundaries.md) — Runtime API and Simulation Engine Boundaries _(Runtime API ↔ sim_engine boundaries)_
|
||||||
|
- [ADR-0016](./ADR-0016-dev-iochiplet-noc-and-memory-path.md) — IOChiplet NOC and Memory Data Path _(IOChiplet NOC and memory data path)_
|
||||||
|
- [ADR-0017](./ADR-0017-dev-cube-noc-and-hbm-connectivity.md) — Cube NOC and HBM Connectivity _(Cube NOC and HBM connectivity)_
|
||||||
|
|
||||||
|
## Detailed Architecture
|
||||||
|
|
||||||
|
One subsection per component file under `src/kernbench/components/builtin/`.
|
||||||
|
|
||||||
|
### forwarding
|
||||||
|
|
||||||
|
- [ADR-0037](./ADR-0037-dev-forwarding-component.md) — Forwarding Component (forwarding_v1)
|
||||||
|
|
||||||
|
### hbm_ctrl
|
||||||
|
|
||||||
|
- [ADR-0034](./ADR-0034-dev-hbm-controller-internal-design.md) — HBM Controller Internal Design
|
||||||
|
|
||||||
|
### io_cpu
|
||||||
|
|
||||||
|
- [ADR-0036](./ADR-0036-dev-io-cpu-component-model.md) — IO_CPU Component Model
|
||||||
|
|
||||||
|
### m_cpu
|
||||||
|
|
||||||
|
- [ADR-0035](./ADR-0035-dev-m-cpu-and-m-cpu-dma-component-model.md) — M_CPU and M_CPU.DMA Component Model
|
||||||
|
|
||||||
|
### pcie_ep
|
||||||
|
|
||||||
|
- [ADR-0038](./ADR-0038-dev-pcie-ep-component-model.md) — PCIE_EP Component Model
|
||||||
|
|
||||||
|
### pe_cpu
|
||||||
|
|
||||||
|
- [ADR-0014](./ADR-0014-dev-pe-pipeline-execution-model.md) — PE Pipeline Execution Model
|
||||||
|
|
||||||
|
### pe_dma
|
||||||
|
|
||||||
|
- [ADR-0014](./ADR-0014-dev-pe-pipeline-execution-model.md) — PE Pipeline Execution Model
|
||||||
|
- [ADR-0023](./ADR-0023-dev-ipcq-pe-collective.md) — PE-level IPCQ — Inter-PE Collective Communication
|
||||||
|
|
||||||
|
### pe_fetch_store
|
||||||
|
|
||||||
|
- [ADR-0014](./ADR-0014-dev-pe-pipeline-execution-model.md) — PE Pipeline Execution Model
|
||||||
|
|
||||||
|
### pe_gemm
|
||||||
|
|
||||||
|
- [ADR-0014](./ADR-0014-dev-pe-pipeline-execution-model.md) — PE Pipeline Execution Model
|
||||||
|
|
||||||
|
### pe_ipcq
|
||||||
|
|
||||||
|
- [ADR-0023](./ADR-0023-dev-ipcq-pe-collective.md) — PE-level IPCQ — Inter-PE Collective Communication
|
||||||
|
|
||||||
|
### pe_math
|
||||||
|
|
||||||
|
- [ADR-0014](./ADR-0014-dev-pe-pipeline-execution-model.md) — PE Pipeline Execution Model
|
||||||
|
|
||||||
|
### pe_mmu
|
||||||
|
|
||||||
|
- [ADR-0039](./ADR-0039-dev-pe-mmu-component-model.md) — PE_MMU Component Model — Component + Utility Dual Role
|
||||||
|
|
||||||
|
### pe_scheduler
|
||||||
|
|
||||||
|
- [ADR-0014](./ADR-0014-dev-pe-pipeline-execution-model.md) — PE Pipeline Execution Model
|
||||||
|
|
||||||
|
### pe_tcm
|
||||||
|
|
||||||
|
- [ADR-0040](./ADR-0040-dev-pe-tcm-component-model.md) — PE_TCM Component Model — Dual-Channel BW Serialization
|
||||||
|
|
||||||
|
### sram
|
||||||
|
|
||||||
|
- [ADR-0041](./ADR-0041-dev-cube-sram-component-model.md) — Cube SRAM Component Model — terminal scratchpad on cube NoC
|
||||||
|
|
||||||
|
### tiling
|
||||||
|
|
||||||
|
- [ADR-0042](./ADR-0042-prog-tile-plan-generators.md) — Tile Plan Generators — GEMM/Math Pipeline Plan Builders
|
||||||
|
|
||||||
|
## Implementation Decisions
|
||||||
|
|
||||||
|
### Address Scheme
|
||||||
|
|
||||||
|
- [ADR-0001](./ADR-0001-mem-physaddr-layout.md) — 51-bit Physical Address Layout & Decoding Contract
|
||||||
|
- [ADR-0011](./ADR-0011-mem-memory-addressing-simplification.md) — Memory Addressing — PA / VA / LA Address Models
|
||||||
|
|
||||||
|
### Routing & Helper API
|
||||||
|
|
||||||
|
- [ADR-0002](./ADR-0002-lat-routing-distance.md) — Routing Distance, Ordering & Bypass Rules
|
||||||
|
- [ADR-0051](./ADR-0051-lat-routing-helper-api.md) — Routing Helper API — `AddressResolver` + `PathRouter`
|
||||||
|
|
||||||
|
### Memory Semantics & Local-HBM Bandwidth
|
||||||
|
|
||||||
|
- [ADR-0004](./ADR-0004-mem-memory-semantics-local-hbm.md) — Memory Semantics & Local-HBM Bandwidth Guarantee
|
||||||
|
|
||||||
|
### Topology Compilation, Diagrams & Builder Algorithms
|
||||||
|
|
||||||
|
- [ADR-0005](./ADR-0005-dev-diagram-views-distance-layout.md) — Diagram Views & Distance-Aware Layout Rules
|
||||||
|
- [ADR-0006](./ADR-0006-dev-topology-compilation-distance-diagram.md) — Topology Compilation, Distance Extraction, and Automatic Diagram Generation
|
||||||
|
- [ADR-0053](./ADR-0053-dev-topology-builder-algorithms.md) — Topology Builder + Visualizer Algorithms
|
||||||
|
|
||||||
|
### Tensor Deployment and Allocation
|
||||||
|
|
||||||
|
- [ADR-0008](./ADR-0008-api-tensor-deploy-and-allocation.md) — Tensor Deployment and Allocation (Host Allocator, PA-first)
|
||||||
|
|
||||||
|
### Kernel Execution and Host-Device Messaging
|
||||||
|
|
||||||
|
- [ADR-0009](./ADR-0009-api-kernel-execution-messaging.md) — Kernel Execution Messaging and Completion Semantics
|
||||||
|
- [ADR-0012](./ADR-0012-api-host-io-message-schema.md) — Host ↔ IO_CPU Message Schema (PA-first, PE-tagged)
|
||||||
|
|
||||||
|
### CLI Surface and Semantics
|
||||||
|
|
||||||
|
- [ADR-0010](./ADR-0010-api-cli-surface-and-semantics.md) — Command Line Interface and Execution Semantics
|
||||||
|
|
||||||
|
### Component Port/Wire Fabric Model
|
||||||
|
|
||||||
|
- [ADR-0015](./ADR-0015-dev-component-port-wire-model.md) — Component Port/Wire Model and Fabric Routing
|
||||||
|
|
||||||
|
### Two-Pass Data Execution
|
||||||
|
|
||||||
|
- [ADR-0020](./ADR-0020-prog-data-execution-two-pass.md) — 2-Pass Data Execution Model (Timing / Data Separation)
|
||||||
|
|
||||||
|
### 2D Grid Program Identity
|
||||||
|
|
||||||
|
- [ADR-0022](./ADR-0022-prog-program-id-2d-grid.md) — 2D Grid program_id Semantics
|
||||||
|
|
||||||
|
### Parallelism (Launcher, DP, TP, AHBM backend, CCL algorithm)
|
||||||
|
|
||||||
|
- [ADR-0024](./ADR-0024-par-sip-tp-launcher.md) — SIP-level Launcher — rank = SIP
|
||||||
|
- [ADR-0026](./ADR-0026-par-dppolicy-intra-device.md) — DPPolicy = Intra-Device Only — remove sip/num_sips fields
|
||||||
|
- [ADR-0027](./ADR-0027-par-megatron-tp.md) — Megatron-style Tensor Parallelism API
|
||||||
|
- [ADR-0047](./ADR-0047-par-ahbm-ccl-backend.md) — AHBM CCL Backend — `torch.distributed`-compat shim
|
||||||
|
- [ADR-0050](./ADR-0050-par-ccl-algorithm-module-contract.md) — CCL Algorithm Module Contract — `ccl/algorithms/*.py`
|
||||||
|
|
||||||
|
### IPCQ Direction Addressing
|
||||||
|
|
||||||
|
- [ADR-0025](./ADR-0025-algo-ipcq-direction-addressing.md) — IPCQ Direction Addressing — address-based matching
|
||||||
|
|
||||||
|
### Intercube All-Reduce
|
||||||
|
|
||||||
|
- [ADR-0032](./ADR-0032-algo-intercube-allreduce.md) — Intercube All-Reduce — pe0 cube-mesh reduce + multi-SIP exchange
|
||||||
|
|
||||||
|
### Evaluation Harnesses
|
||||||
|
|
||||||
|
- [ADR-0043](./ADR-0043-eval-allreduce-harness.md) — Allreduce Evaluation Harness — `tests/sccl/`
|
||||||
|
- [ADR-0044](./ADR-0044-eval-gemm-harness.md) — GEMM Evaluation Harness — `scripts/gemm_sweep.py` + `tests/gemm/`
|
||||||
|
- [ADR-0054](./ADR-0054-eval-milestone-benches.md) — Milestone Eval Benches — self-contained sweep + figure benches
|
||||||
|
|
||||||
|
### Bench Module Contract
|
||||||
|
|
||||||
|
- [ADR-0045](./ADR-0045-prog-bench-module-contract.md) — Bench Module Contract — registration, dispatch, and authoring
|
||||||
|
|
||||||
|
### Kernel-side tl.* API (TLContext)
|
||||||
|
|
||||||
|
- [ADR-0046](./ADR-0046-prog-tl-context-contract.md) — TLContext — Kernel-side `tl.*` API Contract
|
||||||
|
|
||||||
|
### Memory Allocator Algorithms
|
||||||
|
|
||||||
|
- [ADR-0048](./ADR-0048-mem-allocator-algorithms.md) — Memory Allocator Algorithms — VirtualAllocator + PEMemAllocator
|
||||||
|
|
||||||
|
### Probe Subcommand
|
||||||
|
|
||||||
|
- [ADR-0049](./ADR-0049-ver-probe-subcommand.md) — `kernbench probe` Subcommand — Traffic-Pattern Verification Harness
|
||||||
|
|
||||||
|
### Sim-engine Op Log and Memory Store Schemas
|
||||||
|
|
||||||
|
- [ADR-0052](./ADR-0052-dev-oplog-memory-store-schemas.md) — OpLog + MemoryStore Schemas — sim_engine internals
|
||||||
@@ -0,0 +1,699 @@
|
|||||||
|
# Detailed Design Document — Flat-ops `CompositeCmd` + `softmax_merge` recipe (ADR-0065)
|
||||||
|
|
||||||
|
**Status:** Accepted (companion to ADR-0065, Accepted) — reconciled to
|
||||||
|
the as-built implementation.
|
||||||
|
**Scope:** the `CompositeCmd` flat-ops restructure, the `softmax_merge`
|
||||||
|
recipe, the decode opt2 variant in `_gqa_attention_decode_opt2.py`, and the
|
||||||
|
data-mode numeric path that makes opt2's recipe compute.
|
||||||
|
**Audience:** reviewer / implementer. Read **ADR-0065** and **ADR-0064
|
||||||
|
Revision 2** first.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. As-built reconciliation (read first)
|
||||||
|
|
||||||
|
This DDD was authored in the design round; the implementation diverged in
|
||||||
|
several places. The sections below have been corrected, but the headline
|
||||||
|
deltas are collected here. (ADR-0065 itself was kept in sync; see commits
|
||||||
|
`47e2c78`,`184f654`,`55f025c`,`17fb940`,`35453cc`,`4a55ae5`,`e8d6c28`,
|
||||||
|
`4089e18`,`3d4a3d4`,`5bbe293`,`e9d62b9`,`f0c5b9c`.)
|
||||||
|
|
||||||
|
| Topic | Original DDD | As-built |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **Phase order** | P0 (ADR-0064 Rev2) lands first | **P1 (flat-ops) first**, then P0 — `logical_bytes` is naturally defined on the flat shape, so the refactor goes first |
|
||||||
|
| **Operand-input DMA** | from `handle.space` (`space=="hbm"`→DMA_READ) | **from the `pinned` flag** (already-in-TCM→skip). The `space` flip was avoided (it would break byte-equality); `tl.ref`→hbm, but the *decision* stays pinned-based |
|
||||||
|
| **Output DMA / handle** | (not addressed; `out=O`) | **ADR-0065 D8** — `tl.composite` returns the output `TensorHandle`; `out`=handle (HBM via `tl.ref`) \| `out_ptr` (HBM shorthand) \| omitted→auto-TCM; **DMA_WRITE gated on `out.space=="hbm"`** (TCM output stays on-chip, chainable) |
|
||||||
|
| **Size cap (D7)** | deterministic segmentation | **hard cap → `ValueError`** at emit (no segmentation) |
|
||||||
|
| **Prologue execution** | feeder yields `prologue_stages` first | feeder feeds each prologue stage as a **standalone 1-stage tile** (folding broke PE_MATH→PE_DMA routing) |
|
||||||
|
| **D6.7 (MATH TCM-only)** | every non-GEMM op | **prologue recipe ops only**; the head op (gemm *or* math) keeps DMA-staged-from-HBM |
|
||||||
|
| **Numeric parity** | "just works" via the existing data executor | composites did **not** compute in data mode at all — built in **N1–N4** (below): composite-as-gemm record, recipe MATH compute, GEMM accumulate, opt2 ordering |
|
||||||
|
| **Dispatch ratio** | model ≈ 4.0× | **measured 5.22×** per-tile (gate `>2×`); R-sweep 4.20×/5.22×/5.45× |
|
||||||
|
| **opt2↔opt3 parity** | exact in data mode | exact recipe math verified by N2/N3 (isolated, non-trivial); e2e opt2↔opt3 holds in the **near-uniform regime** (kernbench K reshape-as-transpose caveat, shared by both kernels) |
|
||||||
|
|
||||||
|
**Data-mode numerics (N1–N4), absent from the original DDD — see §15.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Goal and success criteria
|
||||||
|
|
||||||
|
**Goal:** make decode opt2 *runnable*, *numerically equivalent to opt3*,
|
||||||
|
and *measurably cheaper in PE_CPU dispatch* under the ADR-0064 Rev2 cost
|
||||||
|
model. Existing benches keep their (post-ADR-0064 Rev2) goldens
|
||||||
|
unchanged.
|
||||||
|
|
||||||
|
**Success criteria:**
|
||||||
|
1. **Refactor safety.** `CompositeCmd` flat-ops restructure preserves the
|
||||||
|
op_log byte-for-byte for every existing bench
|
||||||
|
(`tests/<existing>` all green, no goldens diff).
|
||||||
|
2. **Recipe correctness.** opt2 final `(m, l, O)` matches opt3 within fp
|
||||||
|
tolerance in data mode (`enable_data=True`).
|
||||||
|
3. **Boundary preservation.** PE_SCHEDULER imports nothing recipe-related;
|
||||||
|
PE_MATH/PE_GEMM/PE_DMA see only `Stage.params["op_kind"]` (no new
|
||||||
|
engine code).
|
||||||
|
4. **Dispatch ratio (robust).** Per-tile PE_CPU dispatch cycles satisfy
|
||||||
|
`opt3 > 2 × opt2` (qualitative invariant, calibration-independent).
|
||||||
|
Default ADR-0064 Rev2 calibration (FIXED=40 cycles, R=0.0625
|
||||||
|
cycles/byte) gives model-expected ≈ 4.0× — informative only, not the
|
||||||
|
test gate.
|
||||||
|
5. **K-before-V invariant.** op_log of decode opt2 shows zero V-related
|
||||||
|
DMA during #2's MATH prologue.
|
||||||
|
|
||||||
|
**Non-goals:** prefill (causal `if` in kernel control flow, separate
|
||||||
|
work); RW-aware scheduler reorder (strict FIFO ships); user-defined
|
||||||
|
recipes; new engine micro-ops in PE_MATH (it stays op_kind-opaque).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Where the design sits in the current code
|
||||||
|
|
||||||
|
```
|
||||||
|
runtime_api (host)
|
||||||
|
RuntimeContext.zeros/empty/launch context.py
|
||||||
|
│ KernelLaunchMsg
|
||||||
|
▼
|
||||||
|
sim_engine
|
||||||
|
GraphEngine(enable_data=…) engine.py
|
||||||
|
MemoryStore memory_store.py
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
components / PE pipeline (per PE)
|
||||||
|
PE_CPU → PE_SCHEDULER → PE_{DMA,GEMM,MATH,IPCQ,TCM}
|
||||||
|
greenlet kernel ↔ SimPy kernel_runner.py, pe_cpu.py
|
||||||
|
CompositeCmd → tile plan → engines pe_scheduler.py (ADR-0014 D6)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
tl programming model tl_context.py
|
||||||
|
composite / dot / load / store / scratch_scope
|
||||||
|
── ADR-0065 NEW ──────────────────────────
|
||||||
|
tl_recipes.py RECIPE_DESCRIPTORS, EngineOp,
|
||||||
|
RecipeDescriptor, PrimaryOutSpec
|
||||||
|
```
|
||||||
|
|
||||||
|
The new module `tl_recipes.py` sits beside `tl_context.py`. PE_SCHEDULER
|
||||||
|
(`pe_scheduler.py`) and `tiling.py` extend to consume the flat ops list
|
||||||
|
without importing `tl_recipes`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. File plan
|
||||||
|
|
||||||
|
### 3.1 Modified production files
|
||||||
|
|
||||||
|
| File | Change | ADR ref |
|
||||||
|
|---|---|---|
|
||||||
|
| `src/kernbench/common/pe_commands.py` | `CompositeCmd` flat-ops dataclass; `OpSpec.operands: dict`; new `Scope` value semantics (KERNEL position-based); `logical_bytes` property on every `PeCommand`. | 0065 D1/D2, 0064 D2 |
|
||||||
|
| `src/kernbench/triton_emu/tl_context.py` | `composite(prologue=[...], out=TensorHandle, …)`; recipe lowering pass (RECIPE_DESCRIPTORS lookup, scratch alloc, OpSpec expansion, primary_out auto-bind); legacy API preserved (`op="gemm", a, b, epilogue=[...]` still works, internally lowers to flat ops). | 0065 D1/D5 |
|
||||||
|
| `src/kernbench/triton_emu/tl_recipes.py` (NEW) | `RecipeDescriptor`, `PrimaryOutSpec`, `EngineOp` dataclasses; `RECIPE_DESCRIPTORS["softmax_merge"]` entry; expansion helper. | 0065 D5 |
|
||||||
|
| `src/kernbench/components/builtin/pe_scheduler.py` | `_generate_plan` rewritten for flat ops: scan for GEMM (≤1), position-based KERNEL placement, OUTPUT_TILE/K_TILE epilogue, strict-FIFO RW tracker keyed by handle id. | 0065 D3/D6 |
|
||||||
|
| `src/kernbench/components/builtin/tiling.py` | `generate_plan_from_ops(ops, tile_*, bpe, pe_prefix)` wraps `generate_gemm_plan`/`generate_math_plan`. Input DMA_READ from the **`pinned`** flag; **output DMA_WRITE gated on `out.space=="hbm"`** (D8). `_math_stage` carries operand+output addrs on recipe prologue stages (N2). | 0065 D3/D4/D8 |
|
||||||
|
| `src/kernbench/common/pe_cost_model.py` (NEW) | `PeCostModel` (FIXED/R/cap) + `from_node_attrs`; replaces the removed Rev1 `cpu_issue_cost.py`. | 0064 D1/D4 |
|
||||||
|
| `src/kernbench/components/builtin/pe_cpu.py` | reads `pe_cost_model:` + `clock_freq_ghz` from node attrs, threads to TLContext (legacy + greenlet KernelRunner). | 0064 D1/D4 |
|
||||||
|
| `src/kernbench/sim_engine/op_log.py` | `_extract_op_info(CompositeCmd)` scans for the GEMM op + `accumulate` flag (N1/N3); `record_end` promotes an address-carrying MATH stage to op_kind="math" (N2). | 0065 N1-N3 |
|
||||||
|
| `src/kernbench/sim_engine/data_executor.py` | `_compute_math` +5 recipe ops; `_execute_gemm` best-effort skip + accumulate (N1-N3). | 0065 N1-N3 |
|
||||||
|
| `src/kernbench/benches/_gqa_attention_decode_opt2.py` (NEW) | opt2 variant — tile 0 establishes (m,l,O) with primitives; the merge tile uses `#1 scores = tl.composite(op="gemm", a=Q, b=K_T1)` + `#2 tl.composite(prologue=[softmax_merge], op="gemm", b=V_ref, out=O, epilogue=[add])`. | 0065 §Migration P3 |
|
||||||
|
|
||||||
|
### 3.2 New tests
|
||||||
|
|
||||||
|
| File | Covers |
|
||||||
|
|---|---|
|
||||||
|
| `tests/test_composite_flat_ops_refactor.py` | P1 flat-ops lowering shape (legacy + math + epilogue) |
|
||||||
|
| `tests/test_pe_cost_model.py` | ADR-0064 Rev2 — `logical_bytes`/formula, override, `PeCpuOverheadCmd` bypass, D7 cap→error, live wiring/path-parity |
|
||||||
|
| `tests/test_tl_recipe_softmax_merge.py` | recipe lowering — 10 ops in order + `rw_handles == (m, l, O)` + size cap |
|
||||||
|
| `tests/test_tl_composite_validation.py` | auto-bind conflict (D6.6); prologue MATH operand TCM-only (D6.7) |
|
||||||
|
| `tests/test_pe_scheduler_flat_ops_plan.py` | position-scan; pinned-based DMA; prologue stages; MATH-only |
|
||||||
|
| `tests/test_pe_scheduler_strict_fifo.py` | `_RwHazardTracker` admit/retire (RAW/WAW/disjoint/legacy) |
|
||||||
|
| `tests/test_composite_output_handle.py` | D8 — composite returns handle (auto-TCM); chaining; output-space DMA; `tl.ref`→hbm |
|
||||||
|
| `tests/test_recipe_data_mode.py` | **N2/N3** — recipe MATH compute m/l/O-rescale + full accumulate (O = O·corr + P@V) vs numpy |
|
||||||
|
| `tests/test_e2e_pipeline.py` | **N1** — composite GEMM computes a@b in data mode |
|
||||||
|
| `tests/attention/test_gqa_decode_opt2.py` | dispatch ratio (>2×) + R-sweep; K-before-V; **N4** opt2↔opt3 parity |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Data model — `CompositeCmd` flat-ops
|
||||||
|
|
||||||
|
### 4.1 New shape
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class CompositeCmd:
|
||||||
|
completion: CompletionHandle
|
||||||
|
ops: tuple[OpSpec, ...] # ordered MATH/GEMM ops
|
||||||
|
rw_handles: tuple[TensorHandle, ...] = () # cross-composite hazard
|
||||||
|
data_op: bool = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def logical_bytes(self) -> int:
|
||||||
|
return (
|
||||||
|
4 # framing
|
||||||
|
+ 1 + sum(op.logical_bytes for op in self.ops)
|
||||||
|
+ 1 + 8 * len(self.rw_handles)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
The previous `op`, `a`, `b`, `out_addr`, `out_nbytes`, `math_op` fields
|
||||||
|
are **removed**. All info lives in `ops[i]`.
|
||||||
|
|
||||||
|
### 4.2 `OpSpec` extended
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class OpSpec:
|
||||||
|
kind: str # "gemm" | "rmax" | "exp_diff" | ...
|
||||||
|
scope: Scope # KERNEL | K_TILE | OUTPUT_TILE
|
||||||
|
operands: dict[str, TensorHandle]
|
||||||
|
extra: dict[str, Any] = field(default_factory=dict)
|
||||||
|
out: TensorHandle | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def logical_bytes(self) -> int:
|
||||||
|
return (
|
||||||
|
1 + 1 # opcode + scope enum
|
||||||
|
+ 1 + 8 * len(self.operands) # named handles
|
||||||
|
+ (8 if self.out is not None else 0)
|
||||||
|
+ 1 + 4 * len(self.extra) # scalars
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Backward compat: existing `operands: tuple[Any, ...]` use sites
|
||||||
|
(epilogue `bias`, `scale`, `add`, etc.) migrate to `operands={"other":
|
||||||
|
h}` / `operands={"src": h}` per a small lookup in
|
||||||
|
`pe_commands.EPILOGUE_OPS`.
|
||||||
|
|
||||||
|
### 4.3 `Scope` semantics — phase vs repetition (ADR-0065 D3)
|
||||||
|
|
||||||
|
`Scope` keeps its three values (`KERNEL`, `K_TILE`, `OUTPUT_TILE`).
|
||||||
|
What changes is the **semantics split**:
|
||||||
|
- **Position** determines the *phase* (where in the tile-loop lifecycle
|
||||||
|
the op fires — pre-loop / in-loop / post-loop).
|
||||||
|
- **Scope** determines the *repetition* within the phase
|
||||||
|
(KERNEL = once per `CompositeCmd` invocation; K_TILE = per K-tile
|
||||||
|
iteration; OUTPUT_TILE = per output tile).
|
||||||
|
|
||||||
|
`KERNEL` does *not* mean "once per kernel launch" or "once anywhere".
|
||||||
|
It means "once per `CompositeCmd` invocation, at this op's position".
|
||||||
|
A composite with multiple KERNEL-scope ops (e.g., softmax_merge's 8
|
||||||
|
prologue ops) executes each exactly once, in sequence order, before
|
||||||
|
the GEMM tile loop begins.
|
||||||
|
|
||||||
|
Concretely:
|
||||||
|
- KERNEL ops *before* GEMM → pre-tile-loop (single shot, in order)
|
||||||
|
- KERNEL ops *after* GEMM → post-tile-loop (single shot, in order)
|
||||||
|
- K_TILE / OUTPUT_TILE ops *after* GEMM → inside tile loop as today
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Recipe: `softmax_merge`
|
||||||
|
|
||||||
|
### 5.1 `tl_recipes.py` definitions
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PrimaryOutSpec:
|
||||||
|
from_shape: str # operand name to copy shape from
|
||||||
|
from_dtype: str # operand name to copy dtype from
|
||||||
|
transform: str # "identity" | "trans" (forward-compat)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class EngineOp:
|
||||||
|
engine: Literal["MATH", "GEMM", "DMA"]
|
||||||
|
op_kind: str # PE_MATH op_kind label (opaque to engine)
|
||||||
|
# Slot names: any of "src", "src_a", "src_b", "src_c", "dst" — these
|
||||||
|
# are RecipeDescriptor.operands names or internal scratch names.
|
||||||
|
src: str | None = None
|
||||||
|
src_a: str | None = None
|
||||||
|
src_b: str | None = None
|
||||||
|
src_c: str | None = None
|
||||||
|
dst: str | None = None
|
||||||
|
reduce_axis: int | None = None
|
||||||
|
bcast_axis: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class RecipeDescriptor:
|
||||||
|
operands: dict[str, str] # name → "R" | "RW"
|
||||||
|
primary_out: PrimaryOutSpec | None # implicit output spec
|
||||||
|
tile_alignment: Literal["single_shot", "tile_aligned"]
|
||||||
|
internal_scratch_bytes_fn: Callable[..., int]
|
||||||
|
engine_seq: tuple[EngineOp, ...]
|
||||||
|
|
||||||
|
|
||||||
|
RECIPE_DESCRIPTORS: dict[str, RecipeDescriptor] = { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 Engine sequence (8 MATH ops)
|
||||||
|
|
||||||
|
| # | engine | op_kind | reads (slots) | writes (slots) | extras |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 1 | MATH | `rmax` | s | m_loc | `reduce_axis=-1` |
|
||||||
|
| 2 | MATH | `max_elem` | m, m_loc | m_new | — |
|
||||||
|
| 3 | MATH | `exp_diff` | m, m_new | corr | — |
|
||||||
|
| 4 | MATH | `exp_diff` | s, m_new | P (primary_out)| `bcast_axis=0` |
|
||||||
|
| 5 | MATH | `rsum` | P | l_loc | `reduce_axis=-1` |
|
||||||
|
| 6 | MATH | `fma` | l, corr, l_loc | l | — |
|
||||||
|
| 7 | MATH | `mul_bcast` | O, corr | O | `bcast_axis=1` |
|
||||||
|
| 8 | MATH | `copy` | m_new | m | — |
|
||||||
|
|
||||||
|
Intermediates `m_loc, m_new, corr, P, l_loc` are scratch slots allocated
|
||||||
|
by TLContext before lowering. `P` is the recipe's primary output; auto-
|
||||||
|
bound to the head GEMM's `operands["a"]`.
|
||||||
|
|
||||||
|
### 5.3 PE_MATH op_kind additions
|
||||||
|
|
||||||
|
PE_MATH is op_kind-opaque ([pe_math.py:90-99](src/kernbench/components/builtin/pe_math.py#L90-L99)).
|
||||||
|
Latency = `ceil(num_elements / vector_width) / clock`. Adding new
|
||||||
|
op_kind labels (rmax, max_elem, exp_diff, rsum, fma, mul_bcast, copy)
|
||||||
|
requires **no PE_MATH code changes** — they pass through as string
|
||||||
|
labels in `Stage.params["op_kind"]`. Latency stays consistent for the
|
||||||
|
same `num_elements`.
|
||||||
|
|
||||||
|
(Reductions like `rmax` / `rsum` have log-depth final stages in real HW
|
||||||
|
not modelled by `ceil(num_elements / vector_width)`; this is an existing
|
||||||
|
approximation, not introduced by ADR-0065.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. TLContext lowering pipeline
|
||||||
|
|
||||||
|
Input (user-facing API, preserved):
|
||||||
|
```python
|
||||||
|
tl.composite(
|
||||||
|
op="gemm", b=tl.ref(v_tile(j), (TILE, d)), out=O,
|
||||||
|
prologue=[{"op": "softmax_merge", "s": Sj, "m": m, "l": l, "O": O}],
|
||||||
|
epilogue=[{"op": "add", "other": O}],
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Lowering steps in `TLContext.composite()`:
|
||||||
|
|
||||||
|
1. **Validate prologue items.** For each, lookup
|
||||||
|
`RECIPE_DESCRIPTORS[item["op"]]`. Check all operands present, type
|
||||||
|
tags (R / RW) match handle states (e.g., RW must not be `tl.ref`).
|
||||||
|
2. **Allocate internal scratch.** Call
|
||||||
|
`recipe.internal_scratch_bytes_fn(G, TILE, d, bpe)`; allocate via
|
||||||
|
`scratch_scope` helper (ADR-0063). Get slot addresses for `m_loc,
|
||||||
|
m_new, corr, P, l_loc`.
|
||||||
|
3. **Derive primary_out handle.** From `recipe.primary_out`: `P` handle =
|
||||||
|
`TensorHandle(shape=Sj.shape, dtype=Sj.dtype, addr=<P slot>,
|
||||||
|
space="tcm")`.
|
||||||
|
4. **Expand `engine_seq` → flat OpSpecs.** For each `EngineOp`:
|
||||||
|
- Resolve slot names (operands or scratch) to `TensorHandle`s.
|
||||||
|
- Build `OpSpec(kind=op_kind, scope=Scope.KERNEL, operands={...},
|
||||||
|
extra={...}, out=...)`.
|
||||||
|
- `out` is the handle for `dst` slot; `operands` is `{src*: handle}`.
|
||||||
|
5. **Auto-bind head GEMM (with conflict check, ADR-0065 D6.6).** GEMM
|
||||||
|
OpSpec built from `op="gemm"`, `operands={"a": P_handle, "b":
|
||||||
|
V_handle}`, `out=<output handle>`, `extra={"m": G, "k": TILE, "n":
|
||||||
|
d}`. **Validation:** if the kernel passed `a=<some handle>`
|
||||||
|
explicitly AND there is a prologue recipe with `primary_out` that
|
||||||
|
would auto-bind into `a`, raise a validation error (auto-bind
|
||||||
|
conflict). The kernel must omit `a` (let recipe bind) or omit the
|
||||||
|
prologue / use a `primary_out=None` recipe (no auto-bind).
|
||||||
|
6. **Build epilogue OpSpecs.** Each `epilogue=[{...}]` entry maps to
|
||||||
|
`OpSpec(kind=..., scope=Scope.OUTPUT_TILE, operands={...}, ...)`.
|
||||||
|
The `add` epilogue specifically gets `operands={"other": O_handle}`
|
||||||
|
so the Stage later carries `other_handle` (GAP 9 fix).
|
||||||
|
7. **Compute `rw_handles`.** Collect all handles tagged RW in prologue
|
||||||
|
recipes + the head GEMM's `out` (if accumulating) + any RW epilogue
|
||||||
|
targets. For softmax_merge: `rw_handles = (m, l, O)`.
|
||||||
|
8. **Operand validation (ADR-0065 D6.7, as-built scope).** Checked on the
|
||||||
|
**prologue recipe ops only**: every operand handle must be
|
||||||
|
`space == "tcm"` (HBM recipe operand → `ValueError`). The head op (gemm
|
||||||
|
*or* math) is **exempt** — it may stream from HBM. softmax_merge's 8 MATH
|
||||||
|
ops only reference TCM-resident handles → the invariant naturally holds.
|
||||||
|
Also: GEMM-count ≤ 1 (D6.1).
|
||||||
|
9. **Check composite size cap (ADR-0064 D7, as-built).** If
|
||||||
|
`cmd.logical_bytes > max_composite_logical_bytes` (default 1024), raise
|
||||||
|
`ValueError` at emit — **hard cap, no auto-segmentation**. softmax_merge's
|
||||||
|
10-op composite (~322 bytes) is well under the cap.
|
||||||
|
10. **Resolve the output handle (ADR-0065 D8) + emit.** `out`=handle (HBM
|
||||||
|
`tl.ref` \| in-place TCM) \| `out_ptr` (HBM shorthand) \| omitted→auto-TCM
|
||||||
|
scratch. Emit `CompositeCmd(ops=(8 MATH + 1 GEMM + 1 MATH(add)),
|
||||||
|
rw_handles=(m,l,O))`, attach a `CompositeFuture` (carrying the
|
||||||
|
completion) to the output handle, and **return that handle** so the
|
||||||
|
result chains like `tl.dot`'s (downstream auto-awaits via
|
||||||
|
`_await_pending`).
|
||||||
|
|
||||||
|
**Legacy lowering.** Pre-existing
|
||||||
|
`tl.composite(op="gemm", a=A, b=B, epilogue=[{"op":"bias","bias":h}])`
|
||||||
|
goes through the same emit path: no prologue, head OpSpec from `op="gemm"`,
|
||||||
|
epilogue OpSpecs built from `epilogue=[...]`. Output `CompositeCmd` has
|
||||||
|
flat ops list; semantics preserved.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. PE_SCHEDULER tile plan generation (flat-ops)
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _generate_plan(cmd: CompositeCmd) -> PipelinePlan:
|
||||||
|
pp = self._pe_prefix
|
||||||
|
bpe = 2
|
||||||
|
|
||||||
|
# 1. Identify GEMM op (≤1).
|
||||||
|
gemm_idx = next(
|
||||||
|
(i for i, o in enumerate(cmd.ops) if o.kind == "gemm"),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. MATH-only composite (no GEMM): single tile of all ops in order.
|
||||||
|
if gemm_idx is None:
|
||||||
|
return _math_only_plan(cmd.ops, pp)
|
||||||
|
|
||||||
|
gemm = cmd.ops[gemm_idx]
|
||||||
|
M, K, N = gemm.extra["m"], gemm.extra["k"], gemm.extra["n"]
|
||||||
|
|
||||||
|
# 3. Partition ops by position.
|
||||||
|
pre_loop_ops = cmd.ops[:gemm_idx] # KERNEL scope
|
||||||
|
after_ops = cmd.ops[gemm_idx+1:] # mixed scopes
|
||||||
|
|
||||||
|
# 4. Pre-loop MATH Stages (single-shot, before tile loop).
|
||||||
|
pre_stages = [_math_stage_from_op(op, pp) for op in pre_loop_ops]
|
||||||
|
|
||||||
|
# 5. Tile loop (existing generate_gemm_plan pattern, but:
|
||||||
|
# - DMA_READ inserted only when operand.space == "hbm"
|
||||||
|
# - K_TILE epilogue from after_ops with scope == K_TILE
|
||||||
|
# - OUTPUT_TILE epilogue from after_ops with scope == OUTPUT_TILE
|
||||||
|
# - DMA_WRITE inserted only when out.space == "hbm")
|
||||||
|
tile_plans = _generate_tile_loop(
|
||||||
|
gemm, M, K, N, after_ops, pp, bpe,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 6. Post-loop KERNEL Stages (after tile loop completes).
|
||||||
|
post_loop_ops = [op for op in after_ops if op.scope == Scope.KERNEL]
|
||||||
|
post_stages = [_math_stage_from_op(op, pp) for op in post_loop_ops]
|
||||||
|
|
||||||
|
return PipelinePlan(
|
||||||
|
prologue_stages=tuple(pre_stages),
|
||||||
|
tiles=tile_plans,
|
||||||
|
epilogue_stages=tuple(post_stages),
|
||||||
|
m_tiles=..., k_tiles=..., n_tiles=...,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
`PipelinePlan` gets two new fields:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PipelinePlan:
|
||||||
|
tiles: list[TilePlan]
|
||||||
|
m_tiles: int
|
||||||
|
k_tiles: int
|
||||||
|
n_tiles: int
|
||||||
|
prologue_stages: tuple[Stage, ...] = ()
|
||||||
|
epilogue_stages: tuple[Stage, ...] = ()
|
||||||
|
```
|
||||||
|
|
||||||
|
Feeder change (`_feed_loop`): yield `prologue_stages` Stages first, then
|
||||||
|
tile loop as today, then `epilogue_stages`. For the single-Stage case
|
||||||
|
(no prologue/epilogue), behaviour identical to today.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Strict-FIFO RW hazard tracker
|
||||||
|
|
||||||
|
```python
|
||||||
|
class _RwHazardTracker:
|
||||||
|
def __init__(self):
|
||||||
|
self._inflight: list[tuple[CompletionHandle, set[str]]] = []
|
||||||
|
# (completion, set of handle ids written or RW)
|
||||||
|
|
||||||
|
def admit(self, env, cmd: CompositeCmd) -> Generator:
|
||||||
|
"""Block until no in-flight composite has overlapping RW."""
|
||||||
|
rw_ids = {h.id for h in cmd.rw_handles}
|
||||||
|
# Strict FIFO: wait for ALL prior composites whose rw_set
|
||||||
|
# intersects, in order.
|
||||||
|
while True:
|
||||||
|
blocking = [c for c, s in self._inflight if s & rw_ids]
|
||||||
|
if not blocking:
|
||||||
|
break
|
||||||
|
# Wait on the *first* blocking composite's completion.
|
||||||
|
yield blocking[0].done_event
|
||||||
|
self._inflight.append((cmd.completion, rw_ids))
|
||||||
|
|
||||||
|
def retire(self, completion: CompletionHandle) -> None:
|
||||||
|
self._inflight = [
|
||||||
|
(c, s) for c, s in self._inflight if c != completion
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
`PE_SCHEDULER` invokes `admit()` before queueing a CompositeCmd's tile
|
||||||
|
plan and `retire()` upon `done_event.succeed()`. Strict FIFO: even when
|
||||||
|
overlap is partial, the new composite waits for all prior overlapping
|
||||||
|
composites to *fully* drain — simple, easy to reason about, easy to
|
||||||
|
test. (RW-aware reorder deferred per ADR-0065 A4.)
|
||||||
|
|
||||||
|
**Conservatism trade-off (ADR-0065 Consequences/Negative).** This rule
|
||||||
|
is safe but may under-expose overlap. Concretely for decode opt2:
|
||||||
|
next-tile's #1 (Q·Kᵀ) does not touch `(m, l, O)` and *could* in
|
||||||
|
principle run while this-tile's #2 is still computing on those handles.
|
||||||
|
Under strict FIFO, next-tile's #1 still gets dispatched (no rw overlap
|
||||||
|
with prior in-flight) — so #1 cross-tile pipelining is preserved.
|
||||||
|
However, if a future bench composed multiple #2-like recipes that share
|
||||||
|
handles, FIFO would serialize them even when their compute could
|
||||||
|
overlap. Acceptable for Phase 1; revisit when a real workload shows
|
||||||
|
the gap.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Phased implementation plan (test-first per CLAUDE.md)
|
||||||
|
|
||||||
|
Each phase is an independent Phase-1 → Phase-2 cycle per CLAUDE.md Part 1.
|
||||||
|
|
||||||
|
| Phase | Deliverable | Gate |
|
||||||
|
|---|---|---|
|
||||||
|
| **P0 (= ADR-0064 Rev2)** | `logical_bytes` property + dispatch formula + topology override + size cap; one-time goldens regeneration | typical 1-OpSpec composite ≈ 43 ns; dispatch ratio test infrastructure in place |
|
||||||
|
| **P1** | `CompositeCmd` flat-ops dataclass refactor + `OpSpec.operands: dict` + legacy `tl.composite` lowering preserved | every existing bench's op_log byte-equal pre/post |
|
||||||
|
| **P2** | `tl_recipes.py` + `RECIPE_DESCRIPTORS["softmax_merge"]` + TLContext lowering for `prologue=[...]` | recipe lowering test (10 ops, rw_handles, addresses) passes |
|
||||||
|
| **P3** | PE_SCHEDULER `_generate_plan` flat-ops + `PipelinePlan.prologue_stages` + DMA auto-insertion + `_feed_loop` extension | all existing bench Stage sequences unchanged; new MATH chain visible in op_log for opt2 mock |
|
||||||
|
| **P4** | Strict-FIFO RW hazard tracker + `_RwHazardTracker` integration | two-composite RW conflict test serializes correctly |
|
||||||
|
| **P5** | `_gqa_decode_long.py` opt2 variant; data-mode numeric parity check; K-before-V invariant check | opt2 matches opt3 within fp tolerance; no V DMA during prologue |
|
||||||
|
| **P6** | Dispatch-ratio measurement: opt3 vs opt2 per-tile PE_CPU cycles + R sensitivity sweep | `opt3 > 2 × opt2` at default calibration (gate); model-expected ≈ 4.0× (informative); `opt2 < opt3` at all `R ∈ {0.25, 0.0625, 0.03125}` (ADR-0064 Test #9) |
|
||||||
|
|
||||||
|
**As-built ordering:** the implementation reversed P0↔P1 — **P1 (flat-ops
|
||||||
|
refactor) landed first**, then P0 (ADR-0064 Rev2 cost model), because
|
||||||
|
`logical_bytes` is naturally defined on the flat shape (doing P1 first avoids
|
||||||
|
a throwaway transitional definition). P1 is a pure, byte-equal refactor; P0
|
||||||
|
then swaps in the structural formula and regenerates goldens (in practice no
|
||||||
|
golden churn occurred). P5 was split: **P5(B)** shipped the opt2 kernel +
|
||||||
|
op_log dispatch measurement first (numeric parity deferred), then **D8 +
|
||||||
|
N1–N4** (§15) built the data-mode numeric path. P6 closed the measurement.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Verification plan (concrete)
|
||||||
|
|
||||||
|
Mirrors ADR-0065 §Test Requirements; grounded in SPEC R2/R5, ADR-0023/
|
||||||
|
0025, ADR-0046, ADR-0054.
|
||||||
|
|
||||||
|
**Refactor safety (P1):**
|
||||||
|
- Parametrize over `tests/<existing bench>.py` baseline op_logs.
|
||||||
|
- Pre/post refactor diff must be empty for *every* bench.
|
||||||
|
- Failure → revert P1 atomically; do not land partial.
|
||||||
|
|
||||||
|
**Recipe lowering (P2):**
|
||||||
|
- Given `Sj=(8,64), m=(8,), l=(8,), O=(8,128)`, lower
|
||||||
|
`tl.composite(prologue=[{"op":"softmax_merge", "s":Sj,...}], op="gemm",
|
||||||
|
b=V_ref, out=O, epilogue=[{"op":"add", "other":O}])`.
|
||||||
|
- Assert `len(cmd.ops) == 10`, `cmd.ops[0..7].kind == [rmax, max_elem,
|
||||||
|
exp_diff, exp_diff, rsum, fma, mul_bcast, copy]`,
|
||||||
|
`cmd.ops[8].kind == "gemm"`, `cmd.ops[9].kind == "add"`.
|
||||||
|
- Assert `cmd.rw_handles == (m, l, O)`.
|
||||||
|
- Assert all OpSpec addresses are concrete (no None).
|
||||||
|
|
||||||
|
**Scheduler plan (P3):**
|
||||||
|
- For the above CompositeCmd, assert PipelinePlan has 8 prologue_stages
|
||||||
|
(MATH), tile loop with FETCH/GEMM/MATH(add), no epilogue_stages.
|
||||||
|
- Assert no DMA_READ for prologue MATH stages (operands TCM-resident).
|
||||||
|
- Assert one DMA_READ for V (`b.space == "hbm"`).
|
||||||
|
- Assert no DMA_WRITE (`out.space == "tcm"`).
|
||||||
|
|
||||||
|
**RW hazard (P4):**
|
||||||
|
- Submit composite A (rw_handles=(O,)) then composite B (operands
|
||||||
|
reading O). Assert B's Stages start only after A's done_event.
|
||||||
|
|
||||||
|
**Numeric parity (P5):**
|
||||||
|
- In data mode, run opt2 and opt3 for `(G=8, T_q=1, S_kv=64, C=1, P=1)`.
|
||||||
|
- Assert `O_opt2 ≈ O_opt3` within `fp16` tolerance.
|
||||||
|
|
||||||
|
**K-before-V invariant (P5):**
|
||||||
|
- op_log inspection: for each tile's #2 CompositeCmd, no
|
||||||
|
`memory/dma_read` events between #2 start and the GEMM compute begin
|
||||||
|
that are not the V tile DMA itself.
|
||||||
|
|
||||||
|
**Dispatch ratio (P6):**
|
||||||
|
- For `S_kv=64, n_tiles=16`, measure total PE_CPU dispatch cycles for
|
||||||
|
opt3 vs opt2 paths.
|
||||||
|
- Assert `opt3 > 2 × opt2` (robust gate — calibration-independent).
|
||||||
|
- Record observed ratio; the default-calibration model expects ≈ 4.0×
|
||||||
|
(DDD §11). The recorded number is informative for performance
|
||||||
|
tracking, not a test gate.
|
||||||
|
- **Sensitivity sweep.** Repeat with `R ∈ {0.25, 0.0625, 0.03125}`
|
||||||
|
cycles/byte; assert opt2 < opt3 in all three. (ADR-0064 Test #9.)
|
||||||
|
|
||||||
|
**Invariant guards (P2):**
|
||||||
|
- Auto-bind conflict: `tl.composite(op="gemm", a=A_handle,
|
||||||
|
prologue=[{"op": "softmax_merge", "s": Sj, "m": m, "l": l, "O": O}],
|
||||||
|
...)` → `pytest.raises(ValidationError, match="auto-bind conflict")`.
|
||||||
|
- MATH operand TCM-only: `tl.composite(op="math", a=tl.ref(addr,
|
||||||
|
shape), ...)` → `pytest.raises(ValidationError, match="MATH operand
|
||||||
|
must be TCM")`.
|
||||||
|
- Per-op logical_bytes summation: a synthetic composite with the same
|
||||||
|
handle `O` in 3 OpSpecs + in `rw_handles` reports
|
||||||
|
`logical_bytes` higher than a deduplicated equivalent by exactly
|
||||||
|
`3 × 8` (operand references) + `8` (rw_handles entry) = 32 bytes.
|
||||||
|
|
||||||
|
**Determinism (all phases):**
|
||||||
|
- Identical inputs → identical op_log + latency (SPEC §0.1).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Performance model (expected)
|
||||||
|
|
||||||
|
Per-tile PE_CPU dispatch cycles, ADR-0064 Rev2 default calibration
|
||||||
|
(FIXED=40 cycles, R=0.0625 cycles/byte, 1 cycle = 1 ns at 1 GHz):
|
||||||
|
|
||||||
|
```
|
||||||
|
opt3: 10 cmds × 40 + total_bytes(≈232) × 0.0625 = 400 + 14.5 ≈ 414.5 ns
|
||||||
|
opt2: 2 cmds × 40 + total_bytes(≈380) × 0.0625 = 80 + 23.75 ≈ 103.75 ns
|
||||||
|
ratio = 414.5 / 103.75 ≈ 4.0×
|
||||||
|
```
|
||||||
|
|
||||||
|
The FIXED term dominates (~96% of the differential), reflecting
|
||||||
|
ADR-0064's framing: the primary signal is command-count reduction.
|
||||||
|
The byte term is a secondary refinement (visible: opt2 carries more
|
||||||
|
total bytes than opt3, so its byte cost is higher; this is more than
|
||||||
|
offset by 8× fewer FIXED payments).
|
||||||
|
|
||||||
|
Per-tile engine timing dominates total latency; dispatch is one
|
||||||
|
component. The ratio above is the *dispatch-only* relative cost. Total
|
||||||
|
latency improvement depends on whether dispatch was on the critical
|
||||||
|
path — measured per workload in P6.
|
||||||
|
|
||||||
|
**R sensitivity** (ADR-0064 Test #9 — sanity at neighbouring R):
|
||||||
|
|
||||||
|
| R (cycles/byte) | opt3 ns | opt2 ns | ratio |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 0.25 (4 B/cy) | 458 | 175 | 2.6× |
|
||||||
|
| 0.0625 (16 B/cy) ✓| 414.5 | 103.75 | 4.0× |
|
||||||
|
| 0.03125 (32 B/cy) | 407.25 | 91.875 | 4.4× |
|
||||||
|
|
||||||
|
`opt2 < opt3` at all three; ratio is monotonic in `1/R` (more
|
||||||
|
FIXED-dominated as R decreases).
|
||||||
|
|
||||||
|
**As-built (measured, `test_gqa_decode_opt2.py`).** The per-tile emitters
|
||||||
|
give opt3=960 ns, opt2=184 ns → **5.22×** at the default calibration (higher
|
||||||
|
than the model's ≈4.0× because the opt3 emitter includes the full online
|
||||||
|
merge); R-sweep ratios **4.20× / 5.22× / 5.45×** for R∈{0.25,0.0625,0.03125},
|
||||||
|
monotonically increasing as R decreases. The gate is the calibration-robust
|
||||||
|
`opt3 > 2 × opt2`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Open items (status)
|
||||||
|
|
||||||
|
Most ADR-0065 §Open review items map to specific implementation choices
|
||||||
|
already documented in §§5–8. Live remaining items:
|
||||||
|
|
||||||
|
| Item | Resolution venue |
|
||||||
|
|---|---|
|
||||||
|
| Non-identity `PrimaryOutSpec.transform` (e.g., `"trans"`) | Future recipe addition; current code uses string dispatch in step 3 of §6 |
|
||||||
|
| `_gqa_decode_long.py` opt2 — TILE size sweep | Open (existing §B-3, §9 of ADR-0060) |
|
||||||
|
| Larger composite descriptors approaching ADR-0064 D7 cap (1024 bytes) | Hard cap → `ValueError` at emit (no segmentation); kernel must split the recipe |
|
||||||
|
| `scratch_scope` budget vs softmax_merge intermediates (G·TILE for P) | ADR-0063 sizing check at S_kv = 256K |
|
||||||
|
| Scheduler plan-gen cost for large composites (ADR-0064 Open Item 2) | If P6 measurements show scheduler-bound behaviour near the cap, expose per-op-count `overhead_ns` on PE_SCHEDULER |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Risks
|
||||||
|
|
||||||
|
| Risk | Likelihood | Mitigation |
|
||||||
|
|---|---|---|
|
||||||
|
| `CompositeCmd` refactor (P1) changes op_log for some bench | med | Parametrized regression over all benches; revert atomically |
|
||||||
|
| `OpSpec.operands` migration (positional → dict) breaks epilogue site | med | Per-EPILOGUE_OPS migration table; existing epilogue tests cover |
|
||||||
|
| `_RwHazardTracker` false-positive serializes too aggressively | low | Strict FIFO is the safe direction; RW-aware reorder is a future ADR |
|
||||||
|
| ADR-0064 Rev2 and ADR-0065 P1 land in wrong order → stale goldens | med | PR ordering enforced; CI gate requires both to be green |
|
||||||
|
| `softmax_merge` scratch budget exceeds `scratch_scope` at long context | med | ADR-0063 sizing check; if needed, tile_alignment promotion (future) |
|
||||||
|
| PE_MATH op_kind label collision with new strings | low | Labels are opaque strings — no enum conflict; just add to allowed set if validated |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Glossary & references
|
||||||
|
|
||||||
|
- **flat ops list** — `CompositeCmd.ops: tuple[OpSpec, ...]` with no
|
||||||
|
head/epilogue field; placement determined by op position + scope.
|
||||||
|
- **prologue (user API)** — `tl.composite(prologue=[...])` kwarg for
|
||||||
|
recipe ops that lower into pre-GEMM KERNEL-scope OpSpecs. *HW command
|
||||||
|
does not carry this concept.*
|
||||||
|
- **recipe** — a named macro op (e.g., `softmax_merge`) that TLContext
|
||||||
|
expands to multiple flat OpSpecs at emit time. Registry =
|
||||||
|
`RECIPE_DESCRIPTORS`.
|
||||||
|
- **primary_out** — recipe's implicit output (e.g., `P` for
|
||||||
|
softmax_merge). Auto-bound to head GEMM's first matrix operand.
|
||||||
|
- **handle-identity strict FIFO** — cross-composite serialization rule:
|
||||||
|
a composite waits for all prior composites whose `rw_handles`
|
||||||
|
intersect with its own to complete.
|
||||||
|
- **logical_bytes** — HW-logical byte size of a PE command, derived
|
||||||
|
from a structural rule (ADR-0064 D2), not from Python's
|
||||||
|
`sys.getsizeof`.
|
||||||
|
|
||||||
|
Source anchors: `tl_context.py` (tl API), `pe_scheduler.py:104-143`
|
||||||
|
(composite tile plan), `pe_dma.py:45,89` (read channel), `pe_math.py`
|
||||||
|
(op_kind opaque latency), `pe_cpu.py` (dispatch hook),
|
||||||
|
`pe_commands.py` (CompositeCmd, OpSpec, EPILOGUE_OPS).
|
||||||
|
|
||||||
|
ADRs: **0065** (this design), **0064 Revision 2** (dispatch cost),
|
||||||
|
**0060** (GQA fused attention, §5.6 / §8 item 4 carve-out), **0063**
|
||||||
|
(scratch_scope), **0046** (tl_context contract), **0042** (tile plan
|
||||||
|
generators), **0014** (PE pipeline execution model).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. Data-mode numerics (D8 + N1–N4) — as-built
|
||||||
|
|
||||||
|
The original DDD assumed opt2's numeric parity "just works" via the existing
|
||||||
|
DataExecutor. It does not: a composite in the builtin tiled path is recorded
|
||||||
|
**only as latency TileTokens** (`op_kind="unknown"`, no addresses) — its
|
||||||
|
matmul/MATH is never computed; the output address is never written. Making
|
||||||
|
opt2's recipe compute required building the composite/recipe data-mode path.
|
||||||
|
|
||||||
|
**D8 — output handle + output-space DMA (commit `e8d6c28`).** Prerequisite:
|
||||||
|
a composite's output must be a real, addressable, chainable handle, and a
|
||||||
|
TCM-resident output must NOT be DMA_WRITTEN (its high-bit scratch address
|
||||||
|
crashes the DMA PA decoder). `tl.composite` returns the output `TensorHandle`
|
||||||
|
(auto-TCM if `out` omitted); the tile loop's DMA_WRITE is gated on
|
||||||
|
`out.space=="hbm"`. This is what lets opt2 *run* in data mode at all.
|
||||||
|
|
||||||
|
**N1 — composite GEMM record (commit `4089e18`).** `op_log._extract_op_info
|
||||||
|
(CompositeCmd)` scans `ops` for the GEMM (not `ops[0]` — a recipe's GEMM sits
|
||||||
|
after the prologue) and emits one `composite_gemm` record (a/b/out
|
||||||
|
addrs+spaces). `PE_SCHEDULER` records the composite's numeric op (no-op
|
||||||
|
without an op_logger). `DataExecutor._execute_gemm` is best-effort: skips on
|
||||||
|
(KeyError, ValueError) reading inputs (torch.empty / sharded operands).
|
||||||
|
|
||||||
|
**N2 — recipe MATH ops (commit `3d4a3d4`).** `_compute_math` gains
|
||||||
|
`rmax`/`rsum` (keepdims) + `max_elem`/`exp_diff`/`mul_bcast` (binary; numpy
|
||||||
|
broadcasting covers `bcast_axis`). `tiling._math_stage` carries operand+output
|
||||||
|
addrs/shapes/spaces+axis on the **recipe prologue** stages only;
|
||||||
|
`op_log.record_end` promotes a MATH stage to `op_kind="math"` *only* when it
|
||||||
|
carries `input_addrs` (legacy epilogue stages stay opaque → byte-equal). A
|
||||||
|
latent P2 bug was fixed: reductions keep the reduced axis as size 1 (keepdims)
|
||||||
|
so they broadcast against `m=(G,1)`. Verified: the 8 MATH ops produce m, l
|
||||||
|
(full online-softmax update) and O (rescaled by corr) vs a numpy reference.
|
||||||
|
|
||||||
|
**N3 — GEMM-epilogue accumulate (commit `5bbe293`).** The composite GEMM
|
||||||
|
record is marked `accumulate=True` when an `add` epilogue targets the GEMM's
|
||||||
|
own output; `_execute_gemm` then does `O = O_existing + a@b`. The composite's
|
||||||
|
numeric op is recorded at **completion** (`_retire_on_done`), not dispatch, so
|
||||||
|
its `t_start` sorts AFTER its prologue MATH ops → the GEMM reads the computed
|
||||||
|
P and the rescaled O. Verified: full recipe → `O = O_old·corr + P@V` vs numpy.
|
||||||
|
|
||||||
|
**N4 — opt2 parity + ordering (commit `e9d62b9`).** `composite()` awaits its
|
||||||
|
prologue recipe operands (the cross-composite RAW: #2's `s` is #1's score
|
||||||
|
tile) so #2 waits for #1. With N1–N3, opt2 computes a correct attention output
|
||||||
|
e2e in data mode and matches opt3 within fp tolerance. **Scope:** the e2e
|
||||||
|
opt2↔opt3 parity holds in the near-uniform-attention regime — kernbench's K
|
||||||
|
reshape-as-transpose (opt3 docstring: correct for zero/symmetric inputs)
|
||||||
|
makes opt2's per-sub-tile K interpretation differ from opt3's single-tile one
|
||||||
|
for high-contrast inputs (a pre-existing kernbench limitation shared by both
|
||||||
|
kernels, not the recipe). The **exact** recipe math is verified non-trivially
|
||||||
|
by N2/N3 (`test_recipe_data_mode.py`).
|
||||||
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
@@ -1,13 +1,13 @@
|
|||||||
buffer_kind,sip_topology,n_sips,n_elem,bytes_per_pe,latency_ns
|
buffer_kind,sip_topology,n_sips,n_elem,bytes_per_pe,latency_ns
|
||||||
hbm,torus_2d,6,128,256,2120.040000000012
|
hbm,torus_2d,6,128,256,2345.040000000012
|
||||||
hbm,torus_2d,6,1024,2048,2717.2783333333473
|
hbm,torus_2d,6,1024,2048,2942.2783333333473
|
||||||
hbm,torus_2d,6,8192,16384,7315.184999999989
|
hbm,torus_2d,6,8192,16384,7540.184999999989
|
||||||
hbm,torus_2d,6,32768,65536,23081.26500000037
|
hbm,torus_2d,6,32768,65536,23306.26500000037
|
||||||
sram,torus_2d,6,128,256,2060.040000000012
|
sram,torus_2d,6,128,256,2285.040000000012
|
||||||
sram,torus_2d,6,1024,2048,2909.2783333333473
|
sram,torus_2d,6,1024,2048,3134.2783333333527
|
||||||
sram,torus_2d,6,8192,16384,9523.184999999869
|
sram,torus_2d,6,8192,16384,9748.184999999869
|
||||||
sram,torus_2d,6,32768,65536,32201.265000000385
|
sram,torus_2d,6,32768,65536,32426.265000000385
|
||||||
tcm,torus_2d,6,128,256,1964.040000000012
|
tcm,torus_2d,6,128,256,2189.040000000012
|
||||||
tcm,torus_2d,6,1024,2048,2477.2783333333473
|
tcm,torus_2d,6,1024,2048,2702.2783333333473
|
||||||
tcm,torus_2d,6,8192,16384,6403.185000000109
|
tcm,torus_2d,6,8192,16384,6628.18500000005
|
||||||
tcm,torus_2d,6,32768,65536,19865.265000000378
|
tcm,torus_2d,6,32768,65536,20090.265000000378
|
||||||
|
|||||||
|
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 92 KiB |
@@ -1,37 +1,37 @@
|
|||||||
algorithm,sip_topology,n_sips,n_elem,bytes_per_pe,bytes_per_sip,latency_ns
|
algorithm,sip_topology,n_sips,n_elem,bytes_per_pe,bytes_per_sip,latency_ns
|
||||||
lrab_hierarchical_allreduce,mesh_2d_no_wrap,6,8,16,256,2666.552500000015
|
lrab_hierarchical_allreduce,mesh_2d_no_wrap,6,8,16,256,2918.5525000000157
|
||||||
lrab_hierarchical_allreduce,mesh_2d_no_wrap,6,32,64,1024,2747.7400000000152
|
lrab_hierarchical_allreduce,mesh_2d_no_wrap,6,32,64,1024,2999.740000000016
|
||||||
lrab_hierarchical_allreduce,mesh_2d_no_wrap,6,64,128,2048,2855.990000000018
|
lrab_hierarchical_allreduce,mesh_2d_no_wrap,6,64,128,2048,3107.990000000019
|
||||||
lrab_hierarchical_allreduce,mesh_2d_no_wrap,6,128,256,4096,3072.490000000019
|
lrab_hierarchical_allreduce,mesh_2d_no_wrap,6,128,256,4096,3324.4900000000207
|
||||||
lrab_hierarchical_allreduce,mesh_2d_no_wrap,6,512,1024,16384,3337.1133333333582
|
lrab_hierarchical_allreduce,mesh_2d_no_wrap,6,512,1024,16384,3589.1133333333582
|
||||||
lrab_hierarchical_allreduce,mesh_2d_no_wrap,6,1024,2048,32768,3708.0333333333692
|
lrab_hierarchical_allreduce,mesh_2d_no_wrap,6,1024,2048,32768,3960.0333333333692
|
||||||
lrab_hierarchical_allreduce,mesh_2d_no_wrap,6,2048,4096,65536,4449.873333333393
|
lrab_hierarchical_allreduce,mesh_2d_no_wrap,6,2048,4096,65536,4701.873333333393
|
||||||
lrab_hierarchical_allreduce,mesh_2d_no_wrap,6,4096,8192,131072,5933.020000000124
|
lrab_hierarchical_allreduce,mesh_2d_no_wrap,6,4096,8192,131072,6185.020000000124
|
||||||
lrab_hierarchical_allreduce,mesh_2d_no_wrap,6,8192,16384,262144,8900.379999999863
|
lrab_hierarchical_allreduce,mesh_2d_no_wrap,6,8192,16384,262144,9152.379999999861
|
||||||
lrab_hierarchical_allreduce,mesh_2d_no_wrap,6,16384,32768,524288,14835.099999999224
|
lrab_hierarchical_allreduce,mesh_2d_no_wrap,6,16384,32768,524288,15087.099999999224
|
||||||
lrab_hierarchical_allreduce,mesh_2d_no_wrap,6,32768,65536,1048576,26704.540000000765
|
lrab_hierarchical_allreduce,mesh_2d_no_wrap,6,32768,65536,1048576,26956.540000000765
|
||||||
lrab_hierarchical_allreduce,mesh_2d_no_wrap,6,49152,98304,1572864,38573.97999999701
|
lrab_hierarchical_allreduce,mesh_2d_no_wrap,6,49152,98304,1572864,38825.97999999701
|
||||||
lrab_hierarchical_allreduce,ring_1d,6,8,16,256,2365.255833333347
|
lrab_hierarchical_allreduce,ring_1d,6,8,16,256,2628.2558333333477
|
||||||
lrab_hierarchical_allreduce,ring_1d,6,32,64,1024,2436.9433333333473
|
lrab_hierarchical_allreduce,ring_1d,6,32,64,1024,2699.943333333348
|
||||||
lrab_hierarchical_allreduce,ring_1d,6,64,128,2048,2532.526666666683
|
lrab_hierarchical_allreduce,ring_1d,6,64,128,2048,2795.526666666683
|
||||||
lrab_hierarchical_allreduce,ring_1d,6,128,256,4096,2723.693333333349
|
lrab_hierarchical_allreduce,ring_1d,6,128,256,4096,2986.693333333351
|
||||||
lrab_hierarchical_allreduce,ring_1d,6,512,1024,16384,3048.635000000021
|
lrab_hierarchical_allreduce,ring_1d,6,512,1024,16384,3311.635000000021
|
||||||
lrab_hierarchical_allreduce,ring_1d,6,1024,2048,32768,3393.4016666666957
|
lrab_hierarchical_allreduce,ring_1d,6,1024,2048,32768,3656.4016666666957
|
||||||
lrab_hierarchical_allreduce,ring_1d,6,2048,4096,65536,4082.401666666714
|
lrab_hierarchical_allreduce,ring_1d,6,2048,4096,65536,4345.401666666714
|
||||||
lrab_hierarchical_allreduce,ring_1d,6,4096,8192,131072,5458.80166666677
|
lrab_hierarchical_allreduce,ring_1d,6,4096,8192,131072,5721.801666666768
|
||||||
lrab_hierarchical_allreduce,ring_1d,6,8192,16384,262144,8216.934999999943
|
lrab_hierarchical_allreduce,ring_1d,6,8192,16384,262144,8479.934999999887
|
||||||
lrab_hierarchical_allreduce,ring_1d,6,16384,32768,524288,13733.201666665835
|
lrab_hierarchical_allreduce,ring_1d,6,16384,32768,524288,13996.201666665835
|
||||||
lrab_hierarchical_allreduce,ring_1d,6,32768,65536,1048576,24765.73500000064
|
lrab_hierarchical_allreduce,ring_1d,6,32768,65536,1048576,25028.73500000064
|
||||||
lrab_hierarchical_allreduce,ring_1d,6,49152,98304,1572864,35798.268333331536
|
lrab_hierarchical_allreduce,ring_1d,6,49152,98304,1572864,36061.26833333154
|
||||||
lrab_hierarchical_allreduce,torus_2d,6,8,16,256,1700.6025000000095
|
lrab_hierarchical_allreduce,torus_2d,6,8,16,256,1925.6025000000104
|
||||||
lrab_hierarchical_allreduce,torus_2d,6,32,64,1024,1753.2900000000102
|
lrab_hierarchical_allreduce,torus_2d,6,32,64,1024,1978.290000000011
|
||||||
lrab_hierarchical_allreduce,torus_2d,6,64,128,2048,1823.540000000012
|
lrab_hierarchical_allreduce,torus_2d,6,64,128,2048,2048.540000000012
|
||||||
lrab_hierarchical_allreduce,torus_2d,6,128,256,4096,1964.040000000012
|
lrab_hierarchical_allreduce,torus_2d,6,128,256,4096,2189.040000000012
|
||||||
lrab_hierarchical_allreduce,torus_2d,6,512,1024,16384,2196.8183333333463
|
lrab_hierarchical_allreduce,torus_2d,6,512,1024,16384,2421.8183333333463
|
||||||
lrab_hierarchical_allreduce,torus_2d,6,1024,2048,32768,2477.2783333333473
|
lrab_hierarchical_allreduce,torus_2d,6,1024,2048,32768,2702.2783333333473
|
||||||
lrab_hierarchical_allreduce,torus_2d,6,2048,4096,65536,3038.1983333333583
|
lrab_hierarchical_allreduce,torus_2d,6,2048,4096,65536,3263.1983333333583
|
||||||
lrab_hierarchical_allreduce,torus_2d,6,4096,8192,131072,4159.5050000000665
|
lrab_hierarchical_allreduce,torus_2d,6,4096,8192,131072,4384.5050000000665
|
||||||
lrab_hierarchical_allreduce,torus_2d,6,8192,16384,262144,6403.185000000109
|
lrab_hierarchical_allreduce,torus_2d,6,8192,16384,262144,6628.18500000005
|
||||||
lrab_hierarchical_allreduce,torus_2d,6,16384,32768,524288,10890.5449999995
|
lrab_hierarchical_allreduce,torus_2d,6,16384,32768,524288,11115.5449999995
|
||||||
lrab_hierarchical_allreduce,torus_2d,6,32768,65536,1048576,19865.265000000378
|
lrab_hierarchical_allreduce,torus_2d,6,32768,65536,1048576,20090.265000000378
|
||||||
lrab_hierarchical_allreduce,torus_2d,6,49152,98304,1572864,28839.98500000059
|
lrab_hierarchical_allreduce,torus_2d,6,49152,98304,1572864,29064.985000000597
|
||||||
|
|||||||
|
|
Before Width: | Height: | Size: 194 KiB After Width: | Height: | Size: 194 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 137 KiB |
@@ -1,81 +1,81 @@
|
|||||||
hop,label,size_bytes,path,total_ns
|
hop,label,size_bytes,path,total_ns
|
||||||
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),128,ipcq,24.88749999999891
|
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),128,ipcq,42.88749999999891
|
||||||
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),128,raw,33.57999999999811
|
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),128,raw,51.57999999999811
|
||||||
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),256,ipcq,28.13749999999891
|
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),256,ipcq,46.13749999999891
|
||||||
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),256,raw,36.07999999999811
|
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),256,raw,54.07999999999811
|
||||||
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),384,ipcq,29.88749999999891
|
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),384,ipcq,47.88749999999891
|
||||||
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),384,raw,37.07999999999811
|
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),384,raw,55.07999999999811
|
||||||
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),512,ipcq,31.63749999999891
|
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),512,ipcq,49.63749999999891
|
||||||
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),512,raw,38.07999999999811
|
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),512,raw,56.07999999999811
|
||||||
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),768,ipcq,35.13749999999891
|
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),768,ipcq,53.13749999999891
|
||||||
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),768,raw,40.07999999999811
|
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),768,raw,58.07999999999811
|
||||||
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),1024,ipcq,38.63749999999891
|
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),1024,ipcq,56.63749999999891
|
||||||
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),1024,raw,42.07999999999811
|
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),1024,raw,60.07999999999811
|
||||||
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),2048,ipcq,52.63749999999891
|
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),2048,ipcq,70.63749999999891
|
||||||
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),2048,raw,50.07999999999811
|
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),2048,raw,68.07999999999811
|
||||||
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),4096,ipcq,80.63750000000073
|
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),4096,ipcq,98.63750000000073
|
||||||
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),4096,raw,66.08000000000175
|
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),4096,raw,84.08000000000175
|
||||||
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),8192,ipcq,136.63750000000073
|
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),8192,ipcq,154.63750000000073
|
||||||
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),8192,raw,98.08000000000175
|
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),8192,raw,116.08000000000175
|
||||||
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),10240,ipcq,164.63750000000073
|
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),10240,ipcq,182.63750000000073
|
||||||
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),10240,raw,114.08000000000175
|
latency_intracube_PE0_to_PE1_horizontal,Intra-cube PE-to-PE latency: PE0 → PE1 (horizontal),10240,raw,132.08000000000175
|
||||||
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),128,ipcq,38.49749999999585
|
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),128,ipcq,56.49749999999585
|
||||||
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),128,raw,47.18999999999505
|
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),128,raw,65.18999999999505
|
||||||
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),256,ipcq,43.24749999999585
|
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),256,ipcq,61.24749999999585
|
||||||
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),256,raw,51.18999999999505
|
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),256,raw,69.18999999999505
|
||||||
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),384,ipcq,44.99749999999585
|
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),384,ipcq,62.99749999999585
|
||||||
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),384,raw,52.18999999999505
|
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),384,raw,70.18999999999505
|
||||||
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),512,ipcq,46.74749999999585
|
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),512,ipcq,64.74749999999585
|
||||||
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),512,raw,53.18999999999505
|
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),512,raw,71.18999999999505
|
||||||
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),768,ipcq,50.24749999999585
|
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),768,ipcq,68.24749999999585
|
||||||
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),768,raw,55.18999999999505
|
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),768,raw,73.18999999999505
|
||||||
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),1024,ipcq,53.74749999999585
|
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),1024,ipcq,71.74749999999585
|
||||||
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),1024,raw,57.18999999999505
|
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),1024,raw,75.18999999999505
|
||||||
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),2048,ipcq,67.74749999999585
|
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),2048,ipcq,85.74749999999585
|
||||||
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),2048,raw,65.18999999999505
|
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),2048,raw,83.18999999999505
|
||||||
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),4096,ipcq,95.74750000000131
|
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),4096,ipcq,113.74750000000131
|
||||||
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),4096,raw,81.19000000000233
|
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),4096,raw,99.19000000000233
|
||||||
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),8192,ipcq,151.7475000000013
|
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),8192,ipcq,169.7475000000013
|
||||||
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),8192,raw,113.19000000000233
|
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),8192,raw,131.19000000000233
|
||||||
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),10240,ipcq,179.7475000000013
|
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),10240,ipcq,197.7475000000013
|
||||||
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),10240,raw,129.19000000000233
|
latency_intracube_PE0_to_PE4_vertical,Intra-cube PE-to-PE latency: PE0 → PE4 (vertical),10240,raw,147.19000000000233
|
||||||
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),128,ipcq,81.15999999999804
|
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),128,ipcq,99.15999999999804
|
||||||
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),128,raw,89.28999999999724
|
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),128,raw,107.28999999999724
|
||||||
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),256,ipcq,88.65999999999804
|
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),256,ipcq,106.65999999999804
|
||||||
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),256,raw,95.53999999999724
|
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),256,raw,113.53999999999724
|
||||||
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),384,ipcq,90.90999999999804
|
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),384,ipcq,108.90999999999804
|
||||||
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),384,raw,96.53999999999724
|
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),384,raw,114.53999999999724
|
||||||
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),512,ipcq,93.15999999999804
|
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),512,ipcq,111.15999999999804
|
||||||
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),512,raw,97.53999999999724
|
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),512,raw,115.53999999999724
|
||||||
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),768,ipcq,97.65999999999804
|
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),768,ipcq,115.65999999999804
|
||||||
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),768,raw,99.53999999999724
|
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),768,raw,117.53999999999724
|
||||||
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),1024,ipcq,103.15999999999804
|
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),1024,ipcq,121.15999999999804
|
||||||
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),1024,raw,102.53999999999724
|
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),1024,raw,120.53999999999724
|
||||||
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),2048,ipcq,125.15999999999804
|
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),2048,ipcq,143.15999999999804
|
||||||
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),2048,raw,114.53999999999724
|
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),2048,raw,132.53999999999724
|
||||||
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),4096,ipcq,169.15999999999985
|
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),4096,ipcq,187.15999999999985
|
||||||
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),4096,raw,138.54000000000087
|
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),4096,raw,156.54000000000087
|
||||||
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),8192,ipcq,257.15999999999985
|
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),8192,ipcq,275.15999999999985
|
||||||
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),8192,raw,186.54000000000087
|
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),8192,raw,204.54000000000087
|
||||||
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),10240,ipcq,301.15999999999985
|
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),10240,ipcq,319.15999999999985
|
||||||
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),10240,raw,210.54000000000087
|
latency_intercube_C0PE0_to_C1PE0_horizontal,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube1.PE0 (horizontal),10240,raw,228.54000000000087
|
||||||
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),128,ipcq,103.15999999999804
|
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),128,ipcq,121.15999999999804
|
||||||
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),128,raw,111.28999999999724
|
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),128,raw,129.28999999999724
|
||||||
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),256,ipcq,112.65999999999804
|
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),256,ipcq,130.65999999999804
|
||||||
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),256,raw,119.53999999999724
|
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),256,raw,137.53999999999724
|
||||||
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),384,ipcq,114.90999999999804
|
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),384,ipcq,132.90999999999804
|
||||||
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),384,raw,120.53999999999724
|
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),384,raw,138.53999999999724
|
||||||
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),512,ipcq,117.15999999999804
|
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),512,ipcq,135.15999999999804
|
||||||
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),512,raw,121.53999999999724
|
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),512,raw,139.53999999999724
|
||||||
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),768,ipcq,121.65999999999804
|
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),768,ipcq,139.65999999999804
|
||||||
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),768,raw,123.53999999999724
|
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),768,raw,141.53999999999724
|
||||||
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),1024,ipcq,127.15999999999804
|
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),1024,ipcq,145.15999999999804
|
||||||
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),1024,raw,126.53999999999724
|
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),1024,raw,144.53999999999724
|
||||||
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),2048,ipcq,149.15999999999804
|
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),2048,ipcq,167.15999999999804
|
||||||
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),2048,raw,138.53999999999724
|
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),2048,raw,156.53999999999724
|
||||||
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),4096,ipcq,193.15999999999985
|
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),4096,ipcq,211.15999999999985
|
||||||
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),4096,raw,162.54000000000087
|
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),4096,raw,180.54000000000087
|
||||||
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),8192,ipcq,281.15999999999985
|
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),8192,ipcq,299.15999999999985
|
||||||
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),8192,raw,210.54000000000087
|
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),8192,raw,228.54000000000087
|
||||||
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),10240,ipcq,325.15999999999985
|
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),10240,ipcq,343.15999999999985
|
||||||
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),10240,raw,234.54000000000087
|
latency_intercube_C0PE0_to_C4PE0_vertical,Inter-cube PE-to-PE latency: Cube0.PE0 → Cube4.PE0 (vertical),10240,raw,252.54000000000087
|
||||||
|
|||||||
|
@@ -0,0 +1,2 @@
|
|||||||
|
# Downloaded Tectonic toolchain binary (large, machine-local; re-fetched by /paper)
|
||||||
|
build/.tools/
|
||||||
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 194 KiB |
@@ -0,0 +1,312 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="860" height="798" viewBox="55 2 860 798">
|
||||||
|
<title>cube</title>
|
||||||
|
<rect width="970" height="900" fill="#ffffff"/>
|
||||||
|
<text x="485" y="22" text-anchor="middle" font-family="monospace" font-size="18" font-weight="bold" fill="#1f2937">CUBE TOPOLOGY — 17.0×14.0mm | 6×6 Router Mesh | n_to_one mode | 64 pseudo-ch</text>
|
||||||
|
<text x="485" y="40" text-anchor="middle" font-family="monospace" font-size="15" fill="#ffffff">Per-PE: 8 ch × 32.0 GB/s = 256.0 GB/s | Cube total: 64 × 32.0 = 2048.0 GB/s</text>
|
||||||
|
<rect x="60" y="60" width="850.0" height="700.0" rx="6" fill="none" stroke="#94a3b8" stroke-width="2" stroke-dasharray="8,4"/>
|
||||||
|
<rect x="260" y="285" width="450" height="250" rx="6" fill="#ecfdf5" stroke="#047857" stroke-width="2" opacity="0.6"/>
|
||||||
|
<text x="485" y="395" text-anchor="middle" font-family="monospace" font-size="16" font-weight="bold" fill="#047857">HBM_CTRL | 64 pseudo channels</text>
|
||||||
|
<text x="485" y="412" text-anchor="middle" font-family="monospace" font-size="14" fill="#059669">Total BW: 2048 GB/s</text>
|
||||||
|
<rect x="270.0" y="289" width="12.9" height="8" rx="1" fill="#3b82f6" opacity="0.8"/>
|
||||||
|
<rect x="283.4" y="289" width="12.9" height="8" rx="1" fill="#3b82f6" opacity="0.8"/>
|
||||||
|
<rect x="296.9" y="289" width="12.9" height="8" rx="1" fill="#3b82f6" opacity="0.8"/>
|
||||||
|
<rect x="310.3" y="289" width="12.9" height="8" rx="1" fill="#3b82f6" opacity="0.8"/>
|
||||||
|
<rect x="323.8" y="289" width="12.9" height="8" rx="1" fill="#3b82f6" opacity="0.8"/>
|
||||||
|
<rect x="337.2" y="289" width="12.9" height="8" rx="1" fill="#3b82f6" opacity="0.8"/>
|
||||||
|
<rect x="350.6" y="289" width="12.9" height="8" rx="1" fill="#3b82f6" opacity="0.8"/>
|
||||||
|
<rect x="364.1" y="289" width="12.9" height="8" rx="1" fill="#3b82f6" opacity="0.8"/>
|
||||||
|
<rect x="377.5" y="289" width="12.9" height="8" rx="1" fill="#60a5fa" opacity="0.8"/>
|
||||||
|
<rect x="390.9" y="289" width="12.9" height="8" rx="1" fill="#60a5fa" opacity="0.8"/>
|
||||||
|
<rect x="404.4" y="289" width="12.9" height="8" rx="1" fill="#60a5fa" opacity="0.8"/>
|
||||||
|
<rect x="417.8" y="289" width="12.9" height="8" rx="1" fill="#60a5fa" opacity="0.8"/>
|
||||||
|
<rect x="431.2" y="289" width="12.9" height="8" rx="1" fill="#60a5fa" opacity="0.8"/>
|
||||||
|
<rect x="444.7" y="289" width="12.9" height="8" rx="1" fill="#60a5fa" opacity="0.8"/>
|
||||||
|
<rect x="458.1" y="289" width="12.9" height="8" rx="1" fill="#60a5fa" opacity="0.8"/>
|
||||||
|
<rect x="471.6" y="289" width="12.9" height="8" rx="1" fill="#60a5fa" opacity="0.8"/>
|
||||||
|
<rect x="485.0" y="289" width="12.9" height="8" rx="1" fill="#475569" opacity="0.8"/>
|
||||||
|
<rect x="498.4" y="289" width="12.9" height="8" rx="1" fill="#475569" opacity="0.8"/>
|
||||||
|
<rect x="511.9" y="289" width="12.9" height="8" rx="1" fill="#475569" opacity="0.8"/>
|
||||||
|
<rect x="525.3" y="289" width="12.9" height="8" rx="1" fill="#475569" opacity="0.8"/>
|
||||||
|
<rect x="538.8" y="289" width="12.9" height="8" rx="1" fill="#475569" opacity="0.8"/>
|
||||||
|
<rect x="552.2" y="289" width="12.9" height="8" rx="1" fill="#475569" opacity="0.8"/>
|
||||||
|
<rect x="565.6" y="289" width="12.9" height="8" rx="1" fill="#475569" opacity="0.8"/>
|
||||||
|
<rect x="579.1" y="289" width="12.9" height="8" rx="1" fill="#475569" opacity="0.8"/>
|
||||||
|
<rect x="592.5" y="289" width="12.9" height="8" rx="1" fill="#94a3b8" opacity="0.8"/>
|
||||||
|
<rect x="605.9" y="289" width="12.9" height="8" rx="1" fill="#94a3b8" opacity="0.8"/>
|
||||||
|
<rect x="619.4" y="289" width="12.9" height="8" rx="1" fill="#94a3b8" opacity="0.8"/>
|
||||||
|
<rect x="632.8" y="289" width="12.9" height="8" rx="1" fill="#94a3b8" opacity="0.8"/>
|
||||||
|
<rect x="646.2" y="289" width="12.9" height="8" rx="1" fill="#94a3b8" opacity="0.8"/>
|
||||||
|
<rect x="659.7" y="289" width="12.9" height="8" rx="1" fill="#94a3b8" opacity="0.8"/>
|
||||||
|
<rect x="673.1" y="289" width="12.9" height="8" rx="1" fill="#94a3b8" opacity="0.8"/>
|
||||||
|
<rect x="686.6" y="289" width="12.9" height="8" rx="1" fill="#94a3b8" opacity="0.8"/>
|
||||||
|
<text x="324" y="286" text-anchor="middle" font-family="monospace" font-size="12" fill="#3b82f6">PE0×8ch</text>
|
||||||
|
<text x="431" y="286" text-anchor="middle" font-family="monospace" font-size="12" fill="#60a5fa">PE1×8ch</text>
|
||||||
|
<text x="539" y="286" text-anchor="middle" font-family="monospace" font-size="12" fill="#475569">PE2×8ch</text>
|
||||||
|
<text x="646" y="286" text-anchor="middle" font-family="monospace" font-size="12" fill="#94a3b8">PE3×8ch</text>
|
||||||
|
<rect x="270.0" y="523" width="12.9" height="8" rx="1" fill="#f59e0b" opacity="0.8"/>
|
||||||
|
<rect x="283.4" y="523" width="12.9" height="8" rx="1" fill="#f59e0b" opacity="0.8"/>
|
||||||
|
<rect x="296.9" y="523" width="12.9" height="8" rx="1" fill="#f59e0b" opacity="0.8"/>
|
||||||
|
<rect x="310.3" y="523" width="12.9" height="8" rx="1" fill="#f59e0b" opacity="0.8"/>
|
||||||
|
<rect x="323.8" y="523" width="12.9" height="8" rx="1" fill="#f59e0b" opacity="0.8"/>
|
||||||
|
<rect x="337.2" y="523" width="12.9" height="8" rx="1" fill="#f59e0b" opacity="0.8"/>
|
||||||
|
<rect x="350.6" y="523" width="12.9" height="8" rx="1" fill="#f59e0b" opacity="0.8"/>
|
||||||
|
<rect x="364.1" y="523" width="12.9" height="8" rx="1" fill="#f59e0b" opacity="0.8"/>
|
||||||
|
<rect x="377.5" y="523" width="12.9" height="8" rx="1" fill="#fbbf24" opacity="0.8"/>
|
||||||
|
<rect x="390.9" y="523" width="12.9" height="8" rx="1" fill="#fbbf24" opacity="0.8"/>
|
||||||
|
<rect x="404.4" y="523" width="12.9" height="8" rx="1" fill="#fbbf24" opacity="0.8"/>
|
||||||
|
<rect x="417.8" y="523" width="12.9" height="8" rx="1" fill="#fbbf24" opacity="0.8"/>
|
||||||
|
<rect x="431.2" y="523" width="12.9" height="8" rx="1" fill="#fbbf24" opacity="0.8"/>
|
||||||
|
<rect x="444.7" y="523" width="12.9" height="8" rx="1" fill="#fbbf24" opacity="0.8"/>
|
||||||
|
<rect x="458.1" y="523" width="12.9" height="8" rx="1" fill="#fbbf24" opacity="0.8"/>
|
||||||
|
<rect x="471.6" y="523" width="12.9" height="8" rx="1" fill="#fbbf24" opacity="0.8"/>
|
||||||
|
<rect x="485.0" y="523" width="12.9" height="8" rx="1" fill="#ef4444" opacity="0.8"/>
|
||||||
|
<rect x="498.4" y="523" width="12.9" height="8" rx="1" fill="#ef4444" opacity="0.8"/>
|
||||||
|
<rect x="511.9" y="523" width="12.9" height="8" rx="1" fill="#ef4444" opacity="0.8"/>
|
||||||
|
<rect x="525.3" y="523" width="12.9" height="8" rx="1" fill="#ef4444" opacity="0.8"/>
|
||||||
|
<rect x="538.8" y="523" width="12.9" height="8" rx="1" fill="#ef4444" opacity="0.8"/>
|
||||||
|
<rect x="552.2" y="523" width="12.9" height="8" rx="1" fill="#ef4444" opacity="0.8"/>
|
||||||
|
<rect x="565.6" y="523" width="12.9" height="8" rx="1" fill="#ef4444" opacity="0.8"/>
|
||||||
|
<rect x="579.1" y="523" width="12.9" height="8" rx="1" fill="#ef4444" opacity="0.8"/>
|
||||||
|
<rect x="592.5" y="523" width="12.9" height="8" rx="1" fill="#f87171" opacity="0.8"/>
|
||||||
|
<rect x="605.9" y="523" width="12.9" height="8" rx="1" fill="#f87171" opacity="0.8"/>
|
||||||
|
<rect x="619.4" y="523" width="12.9" height="8" rx="1" fill="#f87171" opacity="0.8"/>
|
||||||
|
<rect x="632.8" y="523" width="12.9" height="8" rx="1" fill="#f87171" opacity="0.8"/>
|
||||||
|
<rect x="646.2" y="523" width="12.9" height="8" rx="1" fill="#f87171" opacity="0.8"/>
|
||||||
|
<rect x="659.7" y="523" width="12.9" height="8" rx="1" fill="#f87171" opacity="0.8"/>
|
||||||
|
<rect x="673.1" y="523" width="12.9" height="8" rx="1" fill="#f87171" opacity="0.8"/>
|
||||||
|
<rect x="686.6" y="523" width="12.9" height="8" rx="1" fill="#f87171" opacity="0.8"/>
|
||||||
|
<text x="324" y="539" text-anchor="middle" font-family="monospace" font-size="12" fill="#f59e0b">PE4×8ch</text>
|
||||||
|
<text x="431" y="539" text-anchor="middle" font-family="monospace" font-size="12" fill="#fbbf24">PE5×8ch</text>
|
||||||
|
<text x="539" y="539" text-anchor="middle" font-family="monospace" font-size="12" fill="#ef4444">PE6×8ch</text>
|
||||||
|
<text x="646" y="539" text-anchor="middle" font-family="monospace" font-size="12" fill="#f87171">PE7×8ch</text>
|
||||||
|
<line x1="135" y1="135" x2="285" y2="135" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="135" y1="135" x2="135" y2="260" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="285" y1="135" x2="435" y2="135" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="285" y1="135" x2="285" y2="260" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="435" y1="135" x2="585" y2="135" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="435" y1="135" x2="435" y2="260" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="585" y1="135" x2="685" y2="135" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="585" y1="135" x2="585" y2="260" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="685" y1="135" x2="835" y2="135" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="685" y1="135" x2="685" y2="260" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="835" y1="135" x2="835" y2="260" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="135" y1="260" x2="285" y2="260" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="135" y1="260" x2="135" y2="335" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="285" y1="260" x2="435" y2="260" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="285" y1="260" x2="285" y2="335" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="435" y1="260" x2="585" y2="260" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="435" y1="260" x2="435" y2="560" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="585" y1="260" x2="685" y2="260" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="585" y1="260" x2="585" y2="560" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="685" y1="260" x2="835" y2="260" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="685" y1="260" x2="685" y2="335" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="835" y1="260" x2="835" y2="335" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="135" y1="335" x2="285" y2="335" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="135" y1="335" x2="135" y2="485" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="285" y1="335" x2="685" y2="335" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="285" y1="335" x2="285" y2="485" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="685" y1="335" x2="835" y2="335" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="685" y1="335" x2="685" y2="485" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="835" y1="335" x2="835" y2="485" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="135" y1="485" x2="285" y2="485" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="135" y1="485" x2="135" y2="560" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="285" y1="485" x2="685" y2="485" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="285" y1="485" x2="285" y2="560" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="685" y1="485" x2="835" y2="485" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="685" y1="485" x2="685" y2="560" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="835" y1="485" x2="835" y2="560" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="135" y1="560" x2="285" y2="560" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="135" y1="560" x2="135" y2="685" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="285" y1="560" x2="435" y2="560" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="285" y1="560" x2="285" y2="685" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="435" y1="560" x2="585" y2="560" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="435" y1="560" x2="435" y2="685" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="585" y1="560" x2="685" y2="560" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="585" y1="560" x2="585" y2="685" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="685" y1="560" x2="835" y2="560" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="685" y1="560" x2="685" y2="685" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="835" y1="560" x2="835" y2="685" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="135" y1="685" x2="285" y2="685" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="285" y1="685" x2="435" y2="685" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="435" y1="685" x2="585" y2="685" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="585" y1="685" x2="685" y2="685" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<line x1="685" y1="685" x2="835" y2="685" stroke="#94a3b8" stroke-width="1" opacity="0.4"/>
|
||||||
|
<circle cx="135" cy="135" r="17" fill="#ffffff" stroke="#94a3b8" stroke-width="1"/>
|
||||||
|
<text x="135" y="138" text-anchor="middle" font-family="monospace" font-size="12" fill="#1f2937">r0c0</text>
|
||||||
|
<rect x="119" y="81" width="32" height="16" rx="3" fill="#ffffff" stroke="#a855f7" stroke-width="1"/>
|
||||||
|
<text x="135" y="92" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#a855f7">PE0</text>
|
||||||
|
<line x1="135" y1="127" x2="149" y2="97" stroke="#a855f7" stroke-width="1" opacity="0.6"/>
|
||||||
|
<circle cx="285" cy="135" r="17" fill="#ffffff" stroke="#94a3b8" stroke-width="1"/>
|
||||||
|
<text x="285" y="138" text-anchor="middle" font-family="monospace" font-size="12" fill="#1f2937">r0c1</text>
|
||||||
|
<rect x="269" y="81" width="32" height="16" rx="3" fill="#ffffff" stroke="#a855f7" stroke-width="1"/>
|
||||||
|
<text x="285" y="92" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#a855f7">PE1</text>
|
||||||
|
<line x1="285" y1="127" x2="299" y2="97" stroke="#a855f7" stroke-width="1" opacity="0.6"/>
|
||||||
|
<circle cx="435" cy="135" r="17" fill="#ffffff" stroke="#94a3b8" stroke-width="1"/>
|
||||||
|
<text x="435" y="138" text-anchor="middle" font-family="monospace" font-size="12" fill="#1f2937">r0c2</text>
|
||||||
|
<circle cx="585" cy="135" r="17" fill="#ffffff" stroke="#94a3b8" stroke-width="1"/>
|
||||||
|
<text x="585" y="138" text-anchor="middle" font-family="monospace" font-size="12" fill="#1f2937">r0c3</text>
|
||||||
|
<circle cx="685" cy="135" r="17" fill="#ffffff" stroke="#94a3b8" stroke-width="1"/>
|
||||||
|
<text x="685" y="138" text-anchor="middle" font-family="monospace" font-size="12" fill="#1f2937">r0c4</text>
|
||||||
|
<circle cx="835" cy="135" r="17" fill="#ffffff" stroke="#94a3b8" stroke-width="1"/>
|
||||||
|
<text x="835" y="138" text-anchor="middle" font-family="monospace" font-size="12" fill="#1f2937">r0c5</text>
|
||||||
|
<circle cx="135" cy="260" r="17" fill="#ffffff" stroke="#94a3b8" stroke-width="1"/>
|
||||||
|
<text x="135" y="263" text-anchor="middle" font-family="monospace" font-size="12" fill="#1f2937">r1c0</text>
|
||||||
|
<circle cx="285" cy="260" r="17" fill="#ffffff" stroke="#94a3b8" stroke-width="1"/>
|
||||||
|
<text x="285" y="263" text-anchor="middle" font-family="monospace" font-size="12" fill="#1f2937">r1c1</text>
|
||||||
|
<circle cx="435" cy="260" r="17" fill="#ffffff" stroke="#94a3b8" stroke-width="1"/>
|
||||||
|
<text x="435" y="263" text-anchor="middle" font-family="monospace" font-size="12" fill="#1f2937">r1c2</text>
|
||||||
|
<rect x="419" y="206" width="32" height="16" rx="3" fill="#ffffff" stroke="#f59e0b" stroke-width="1"/>
|
||||||
|
<text x="435" y="217" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#f59e0b">M_CPU</text>
|
||||||
|
<line x1="435" y1="252" x2="449" y2="222" stroke="#f59e0b" stroke-width="1" opacity="0.6"/>
|
||||||
|
<circle cx="585" cy="260" r="17" fill="#ffffff" stroke="#94a3b8" stroke-width="1"/>
|
||||||
|
<text x="585" y="263" text-anchor="middle" font-family="monospace" font-size="12" fill="#1f2937">r1c3</text>
|
||||||
|
<circle cx="685" cy="260" r="17" fill="#ffffff" stroke="#94a3b8" stroke-width="1"/>
|
||||||
|
<text x="685" y="263" text-anchor="middle" font-family="monospace" font-size="12" fill="#1f2937">r1c4</text>
|
||||||
|
<rect x="669" y="206" width="32" height="16" rx="3" fill="#ffffff" stroke="#a855f7" stroke-width="1"/>
|
||||||
|
<text x="685" y="217" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#a855f7">PE2</text>
|
||||||
|
<line x1="685" y1="252" x2="699" y2="222" stroke="#a855f7" stroke-width="1" opacity="0.6"/>
|
||||||
|
<circle cx="835" cy="260" r="17" fill="#ffffff" stroke="#94a3b8" stroke-width="1"/>
|
||||||
|
<text x="835" y="263" text-anchor="middle" font-family="monospace" font-size="12" fill="#1f2937">r1c5</text>
|
||||||
|
<rect x="819" y="206" width="32" height="16" rx="3" fill="#ffffff" stroke="#a855f7" stroke-width="1"/>
|
||||||
|
<text x="835" y="217" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#a855f7">PE3</text>
|
||||||
|
<line x1="835" y1="252" x2="849" y2="222" stroke="#a855f7" stroke-width="1" opacity="0.6"/>
|
||||||
|
<circle cx="135" cy="335" r="17" fill="#ffffff" stroke="#94a3b8" stroke-width="1"/>
|
||||||
|
<text x="135" y="338" text-anchor="middle" font-family="monospace" font-size="12" fill="#1f2937">r2c0</text>
|
||||||
|
<circle cx="285" cy="335" r="17" fill="#ffffff" stroke="#94a3b8" stroke-width="1"/>
|
||||||
|
<text x="285" y="338" text-anchor="middle" font-family="monospace" font-size="12" fill="#1f2937">r2c1</text>
|
||||||
|
<circle cx="685" cy="335" r="17" fill="#ffffff" stroke="#94a3b8" stroke-width="1"/>
|
||||||
|
<text x="685" y="338" text-anchor="middle" font-family="monospace" font-size="12" fill="#1f2937">r2c4</text>
|
||||||
|
<circle cx="835" cy="335" r="17" fill="#ffffff" stroke="#94a3b8" stroke-width="1"/>
|
||||||
|
<text x="835" y="338" text-anchor="middle" font-family="monospace" font-size="12" fill="#1f2937">r2c5</text>
|
||||||
|
<circle cx="135" cy="485" r="17" fill="#ffffff" stroke="#94a3b8" stroke-width="1"/>
|
||||||
|
<text x="135" y="488" text-anchor="middle" font-family="monospace" font-size="12" fill="#1f2937">r3c0</text>
|
||||||
|
<rect x="119" y="523" width="32" height="16" rx="3" fill="#ffffff" stroke="#d97706" stroke-width="1"/>
|
||||||
|
<text x="135" y="534" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#d97706">SRAM</text>
|
||||||
|
<line x1="135" y1="493" x2="149" y2="523" stroke="#d97706" stroke-width="1" opacity="0.6"/>
|
||||||
|
<circle cx="285" cy="485" r="17" fill="#ffffff" stroke="#94a3b8" stroke-width="1"/>
|
||||||
|
<text x="285" y="488" text-anchor="middle" font-family="monospace" font-size="12" fill="#1f2937">r3c1</text>
|
||||||
|
<circle cx="685" cy="485" r="17" fill="#ffffff" stroke="#94a3b8" stroke-width="1"/>
|
||||||
|
<text x="685" y="488" text-anchor="middle" font-family="monospace" font-size="12" fill="#1f2937">r3c4</text>
|
||||||
|
<circle cx="835" cy="485" r="17" fill="#ffffff" stroke="#94a3b8" stroke-width="1"/>
|
||||||
|
<text x="835" y="488" text-anchor="middle" font-family="monospace" font-size="12" fill="#1f2937">r3c5</text>
|
||||||
|
<circle cx="135" cy="560" r="17" fill="#ffffff" stroke="#94a3b8" stroke-width="1"/>
|
||||||
|
<text x="135" y="563" text-anchor="middle" font-family="monospace" font-size="12" fill="#1f2937">r4c0</text>
|
||||||
|
<rect x="119" y="598" width="32" height="16" rx="3" fill="#ffffff" stroke="#a855f7" stroke-width="1"/>
|
||||||
|
<text x="135" y="609" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#a855f7">PE4</text>
|
||||||
|
<line x1="135" y1="568" x2="149" y2="598" stroke="#a855f7" stroke-width="1" opacity="0.6"/>
|
||||||
|
<circle cx="285" cy="560" r="17" fill="#ffffff" stroke="#94a3b8" stroke-width="1"/>
|
||||||
|
<text x="285" y="563" text-anchor="middle" font-family="monospace" font-size="12" fill="#1f2937">r4c1</text>
|
||||||
|
<rect x="269" y="598" width="32" height="16" rx="3" fill="#ffffff" stroke="#a855f7" stroke-width="1"/>
|
||||||
|
<text x="285" y="609" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#a855f7">PE5</text>
|
||||||
|
<line x1="285" y1="568" x2="299" y2="598" stroke="#a855f7" stroke-width="1" opacity="0.6"/>
|
||||||
|
<circle cx="435" cy="560" r="17" fill="#ffffff" stroke="#94a3b8" stroke-width="1"/>
|
||||||
|
<text x="435" y="563" text-anchor="middle" font-family="monospace" font-size="12" fill="#1f2937">r4c2</text>
|
||||||
|
<circle cx="585" cy="560" r="17" fill="#ffffff" stroke="#94a3b8" stroke-width="1"/>
|
||||||
|
<text x="585" y="563" text-anchor="middle" font-family="monospace" font-size="12" fill="#1f2937">r4c3</text>
|
||||||
|
<circle cx="685" cy="560" r="17" fill="#ffffff" stroke="#94a3b8" stroke-width="1"/>
|
||||||
|
<text x="685" y="563" text-anchor="middle" font-family="monospace" font-size="12" fill="#1f2937">r4c4</text>
|
||||||
|
<circle cx="835" cy="560" r="17" fill="#ffffff" stroke="#94a3b8" stroke-width="1"/>
|
||||||
|
<text x="835" y="563" text-anchor="middle" font-family="monospace" font-size="12" fill="#1f2937">r4c5</text>
|
||||||
|
<circle cx="135" cy="685" r="17" fill="#ffffff" stroke="#94a3b8" stroke-width="1"/>
|
||||||
|
<text x="135" y="688" text-anchor="middle" font-family="monospace" font-size="12" fill="#1f2937">r5c0</text>
|
||||||
|
<circle cx="285" cy="685" r="17" fill="#ffffff" stroke="#94a3b8" stroke-width="1"/>
|
||||||
|
<text x="285" y="688" text-anchor="middle" font-family="monospace" font-size="12" fill="#1f2937">r5c1</text>
|
||||||
|
<circle cx="435" cy="685" r="17" fill="#ffffff" stroke="#94a3b8" stroke-width="1"/>
|
||||||
|
<text x="435" y="688" text-anchor="middle" font-family="monospace" font-size="12" fill="#1f2937">r5c2</text>
|
||||||
|
<circle cx="585" cy="685" r="17" fill="#ffffff" stroke="#94a3b8" stroke-width="1"/>
|
||||||
|
<text x="585" y="688" text-anchor="middle" font-family="monospace" font-size="12" fill="#1f2937">r5c3</text>
|
||||||
|
<circle cx="685" cy="685" r="17" fill="#ffffff" stroke="#94a3b8" stroke-width="1"/>
|
||||||
|
<text x="685" y="688" text-anchor="middle" font-family="monospace" font-size="12" fill="#1f2937">r5c4</text>
|
||||||
|
<rect x="669" y="723" width="32" height="16" rx="3" fill="#ffffff" stroke="#a855f7" stroke-width="1"/>
|
||||||
|
<text x="685" y="734" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#a855f7">PE6</text>
|
||||||
|
<line x1="685" y1="693" x2="699" y2="723" stroke="#a855f7" stroke-width="1" opacity="0.6"/>
|
||||||
|
<circle cx="835" cy="685" r="17" fill="#ffffff" stroke="#94a3b8" stroke-width="1"/>
|
||||||
|
<text x="835" y="688" text-anchor="middle" font-family="monospace" font-size="12" fill="#1f2937">r5c5</text>
|
||||||
|
<rect x="819" y="723" width="32" height="16" rx="3" fill="#ffffff" stroke="#a855f7" stroke-width="1"/>
|
||||||
|
<text x="835" y="734" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#a855f7">PE7</text>
|
||||||
|
<line x1="835" y1="693" x2="849" y2="723" stroke="#a855f7" stroke-width="1" opacity="0.6"/>
|
||||||
|
<polyline points="135,143 208,216 251,216 324,289" fill="none" stroke="#10b981" stroke-width="1.5" opacity="0.6" stroke-dasharray="4,3"/>
|
||||||
|
<text x="239" y="216" font-family="monospace" font-size="12" fill="#047857">256GB/s</text>
|
||||||
|
<polyline points="285,143 358,216 358,216 431,289" fill="none" stroke="#10b981" stroke-width="1.5" opacity="0.6" stroke-dasharray="4,3"/>
|
||||||
|
<text x="368" y="216" font-family="monospace" font-size="12" fill="#047857">256GB/s</text>
|
||||||
|
<polyline points="685,268 674,278 549,278 539,289" fill="none" stroke="#10b981" stroke-width="1.5" opacity="0.6" stroke-dasharray="4,3"/>
|
||||||
|
<text x="622" y="278" font-family="monospace" font-size="12" fill="#047857">256GB/s</text>
|
||||||
|
<polyline points="835,268 824,278 657,278 646,289" fill="none" stroke="#10b981" stroke-width="1.5" opacity="0.6" stroke-dasharray="4,3"/>
|
||||||
|
<text x="751" y="278" font-family="monospace" font-size="12" fill="#047857">256GB/s</text>
|
||||||
|
<polyline points="135,552 146,542 313,542 324,531" fill="none" stroke="#10b981" stroke-width="1.5" opacity="0.6" stroke-dasharray="4,3"/>
|
||||||
|
<text x="239" y="542" font-family="monospace" font-size="12" fill="#047857">256GB/s</text>
|
||||||
|
<polyline points="285,552 296,542 421,542 431,531" fill="none" stroke="#10b981" stroke-width="1.5" opacity="0.6" stroke-dasharray="4,3"/>
|
||||||
|
<text x="368" y="542" font-family="monospace" font-size="12" fill="#047857">256GB/s</text>
|
||||||
|
<polyline points="685,677 612,604 612,604 539,531" fill="none" stroke="#10b981" stroke-width="1.5" opacity="0.6" stroke-dasharray="4,3"/>
|
||||||
|
<text x="622" y="604" font-family="monospace" font-size="12" fill="#047857">256GB/s</text>
|
||||||
|
<polyline points="835,677 762,604 719,604 646,531" fill="none" stroke="#10b981" stroke-width="1.5" opacity="0.6" stroke-dasharray="4,3"/>
|
||||||
|
<text x="751" y="604" font-family="monospace" font-size="12" fill="#047857">256GB/s</text>
|
||||||
|
<rect x="65" y="360" width="50" height="100" rx="3" fill="#ffffff" stroke="#475569" stroke-width="1.5" opacity="0.9"/>
|
||||||
|
<text x="90" y="357" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#475569">UCIe-W</text>
|
||||||
|
<rect x="67" y="362" width="46" height="23" rx="2" fill="#cbd5e1" opacity="0.7"/>
|
||||||
|
<text x="90" y="376" text-anchor="middle" font-family="monospace" font-size="10" fill="#1f2937">c0</text>
|
||||||
|
<polyline points="127,135 120,142 120,366 113,374" fill="none" stroke="#cbd5e1" stroke-width="1" opacity="0.5"/>
|
||||||
|
<rect x="67" y="386" width="46" height="23" rx="2" fill="#94a3b8" opacity="0.7"/>
|
||||||
|
<text x="90" y="400" text-anchor="middle" font-family="monospace" font-size="10" fill="#1f2937">c1</text>
|
||||||
|
<polyline points="127,260 120,267 120,390 113,398" fill="none" stroke="#94a3b8" stroke-width="1" opacity="0.5"/>
|
||||||
|
<rect x="67" y="410" width="46" height="23" rx="2" fill="#64748b" opacity="0.7"/>
|
||||||
|
<text x="90" y="424" text-anchor="middle" font-family="monospace" font-size="10" fill="#1f2937">c2</text>
|
||||||
|
<polyline points="127,560 120,553 120,428 113,422" fill="none" stroke="#64748b" stroke-width="1" opacity="0.5"/>
|
||||||
|
<rect x="67" y="434" width="46" height="23" rx="2" fill="#374151" opacity="0.7"/>
|
||||||
|
<text x="90" y="448" text-anchor="middle" font-family="monospace" font-size="10" fill="#1f2937">c3</text>
|
||||||
|
<polyline points="127,685 120,678 120,452 113,446" fill="none" stroke="#374151" stroke-width="1" opacity="0.5"/>
|
||||||
|
<rect x="435" y="65" width="100" height="50" rx="3" fill="#ffffff" stroke="#475569" stroke-width="1.5" opacity="0.9"/>
|
||||||
|
<text x="485" y="62" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#475569">UCIe-N</text>
|
||||||
|
<rect x="437" y="67" width="23" height="46" rx="2" fill="#cbd5e1" opacity="0.7"/>
|
||||||
|
<text x="448" y="93" text-anchor="middle" font-family="monospace" font-size="10" fill="#1f2937">c0</text>
|
||||||
|
<polyline points="135,127 142,120 442,120 448,113" fill="none" stroke="#cbd5e1" stroke-width="1" opacity="0.5"/>
|
||||||
|
<rect x="461" y="67" width="23" height="46" rx="2" fill="#94a3b8" opacity="0.7"/>
|
||||||
|
<text x="472" y="93" text-anchor="middle" font-family="monospace" font-size="10" fill="#1f2937">c1</text>
|
||||||
|
<polyline points="285,127 292,120 466,120 472,113" fill="none" stroke="#94a3b8" stroke-width="1" opacity="0.5"/>
|
||||||
|
<rect x="485" y="67" width="23" height="46" rx="2" fill="#64748b" opacity="0.7"/>
|
||||||
|
<text x="496" y="93" text-anchor="middle" font-family="monospace" font-size="10" fill="#1f2937">c2</text>
|
||||||
|
<polyline points="685,127 678,120 504,120 496,113" fill="none" stroke="#64748b" stroke-width="1" opacity="0.5"/>
|
||||||
|
<rect x="509" y="67" width="23" height="46" rx="2" fill="#374151" opacity="0.7"/>
|
||||||
|
<text x="520" y="93" text-anchor="middle" font-family="monospace" font-size="10" fill="#1f2937">c3</text>
|
||||||
|
<polyline points="835,127 828,120 528,120 520,113" fill="none" stroke="#374151" stroke-width="1" opacity="0.5"/>
|
||||||
|
<rect x="855" y="360" width="50" height="100" rx="3" fill="#ffffff" stroke="#475569" stroke-width="1.5" opacity="0.9"/>
|
||||||
|
<text x="880" y="357" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#475569">UCIe-E</text>
|
||||||
|
<rect x="857" y="362" width="46" height="23" rx="2" fill="#cbd5e1" opacity="0.7"/>
|
||||||
|
<text x="880" y="376" text-anchor="middle" font-family="monospace" font-size="10" fill="#1f2937">c0</text>
|
||||||
|
<polyline points="843,135 850,142 850,367 857,374" fill="none" stroke="#cbd5e1" stroke-width="1" opacity="0.5"/>
|
||||||
|
<rect x="857" y="386" width="46" height="23" rx="2" fill="#94a3b8" opacity="0.7"/>
|
||||||
|
<text x="880" y="400" text-anchor="middle" font-family="monospace" font-size="10" fill="#1f2937">c1</text>
|
||||||
|
<polyline points="843,260 850,267 850,391 857,398" fill="none" stroke="#94a3b8" stroke-width="1" opacity="0.5"/>
|
||||||
|
<rect x="857" y="410" width="46" height="23" rx="2" fill="#64748b" opacity="0.7"/>
|
||||||
|
<text x="880" y="424" text-anchor="middle" font-family="monospace" font-size="10" fill="#1f2937">c2</text>
|
||||||
|
<polyline points="843,560 850,553 850,428 857,422" fill="none" stroke="#64748b" stroke-width="1" opacity="0.5"/>
|
||||||
|
<rect x="857" y="434" width="46" height="23" rx="2" fill="#374151" opacity="0.7"/>
|
||||||
|
<text x="880" y="448" text-anchor="middle" font-family="monospace" font-size="10" fill="#1f2937">c3</text>
|
||||||
|
<polyline points="843,685 850,678 850,452 857,446" fill="none" stroke="#374151" stroke-width="1" opacity="0.5"/>
|
||||||
|
<rect x="435" y="705" width="100" height="50" rx="3" fill="#ffffff" stroke="#475569" stroke-width="1.5" opacity="0.9"/>
|
||||||
|
<text x="485" y="702" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#475569">UCIe-S</text>
|
||||||
|
<rect x="437" y="707" width="23" height="46" rx="2" fill="#cbd5e1" opacity="0.7"/>
|
||||||
|
<text x="448" y="733" text-anchor="middle" font-family="monospace" font-size="10" fill="#1f2937">c0</text>
|
||||||
|
<polyline points="135,693 142,700 442,700 448,707" fill="none" stroke="#cbd5e1" stroke-width="1" opacity="0.5"/>
|
||||||
|
<rect x="461" y="707" width="23" height="46" rx="2" fill="#94a3b8" opacity="0.7"/>
|
||||||
|
<text x="472" y="733" text-anchor="middle" font-family="monospace" font-size="10" fill="#1f2937">c1</text>
|
||||||
|
<polyline points="285,693 292,700 466,700 472,707" fill="none" stroke="#94a3b8" stroke-width="1" opacity="0.5"/>
|
||||||
|
<rect x="485" y="707" width="23" height="46" rx="2" fill="#64748b" opacity="0.7"/>
|
||||||
|
<text x="496" y="733" text-anchor="middle" font-family="monospace" font-size="10" fill="#1f2937">c2</text>
|
||||||
|
<polyline points="685,693 678,700 504,700 496,707" fill="none" stroke="#64748b" stroke-width="1" opacity="0.5"/>
|
||||||
|
<rect x="509" y="707" width="23" height="46" rx="2" fill="#374151" opacity="0.7"/>
|
||||||
|
<text x="520" y="733" text-anchor="middle" font-family="monospace" font-size="10" fill="#1f2937">c3</text>
|
||||||
|
<polyline points="835,693 828,700 528,700 520,707" fill="none" stroke="#374151" stroke-width="1" opacity="0.5"/>
|
||||||
|
<rect x="60" y="775" width="10" height="10" rx="2" fill="#3b82f6" stroke="#94a3b8" stroke-width="0.5"/>
|
||||||
|
<text x="74" y="784" font-family="monospace" font-size="13" fill="#1f2937">PE Router</text>
|
||||||
|
<rect x="147" y="775" width="10" height="10" rx="2" fill="#f59e0b" stroke="#94a3b8" stroke-width="0.5"/>
|
||||||
|
<text x="161" y="784" font-family="monospace" font-size="13" fill="#1f2937">M_CPU / SRAM</text>
|
||||||
|
<rect x="255" y="775" width="10" height="10" rx="2" fill="#475569" stroke="#94a3b8" stroke-width="0.5"/>
|
||||||
|
<text x="269" y="784" font-family="monospace" font-size="13" fill="#1f2937">UCIe</text>
|
||||||
|
<rect x="307" y="775" width="10" height="10" rx="2" fill="#1f2937" stroke="#94a3b8" stroke-width="0.5"/>
|
||||||
|
<text x="321" y="784" font-family="monospace" font-size="13" fill="#1f2937">Relay</text>
|
||||||
|
<rect x="366" y="775" width="10" height="10" rx="2" fill="#10b981" stroke="#94a3b8" stroke-width="0.5"/>
|
||||||
|
<text x="380" y="784" font-family="monospace" font-size="13" fill="#1f2937">HBM Link</text>
|
||||||
|
<rect x="446" y="775" width="10" height="10" rx="2" fill="#1f2937" stroke="#94a3b8" stroke-width="0.5"/>
|
||||||
|
<text x="460" y="784" font-family="monospace" font-size="13" fill="#1f2937">Mesh Link</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 136 KiB |
@@ -0,0 +1,97 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"panels": [
|
||||||
|
"single_user_prefill_gqa",
|
||||||
|
"multi_user_prefill_gqa",
|
||||||
|
"single_user_decode_gqa",
|
||||||
|
"multi_user_decode_gqa"
|
||||||
|
],
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"panel": "single_user_prefill_gqa",
|
||||||
|
"kind": "prefill",
|
||||||
|
"C": 1,
|
||||||
|
"S_kv": 16,
|
||||||
|
"latency_ns": 445.1180000000004,
|
||||||
|
"op_log_summary": {
|
||||||
|
"gemm_count": 2,
|
||||||
|
"ipcq_copy_count": 0,
|
||||||
|
"dma_read_count": 3,
|
||||||
|
"dma_write_count": 1
|
||||||
|
},
|
||||||
|
"engine_occupancy_ns": {
|
||||||
|
"pe_gemm": 2.048000000000002,
|
||||||
|
"pe_math": 5.0,
|
||||||
|
"pe_dma": 72.0,
|
||||||
|
"pe_fetch_store": 0,
|
||||||
|
"pe_ipcq": 0,
|
||||||
|
"pe_cpu": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"panel": "multi_user_prefill_gqa",
|
||||||
|
"kind": "prefill",
|
||||||
|
"C": 4,
|
||||||
|
"S_kv": 16,
|
||||||
|
"latency_ns": 4630.408000000019,
|
||||||
|
"op_log_summary": {
|
||||||
|
"gemm_count": 32,
|
||||||
|
"ipcq_copy_count": 24,
|
||||||
|
"dma_read_count": 12,
|
||||||
|
"dma_write_count": 4
|
||||||
|
},
|
||||||
|
"engine_occupancy_ns": {
|
||||||
|
"pe_gemm": 8.192000000000917,
|
||||||
|
"pe_math": 236.0,
|
||||||
|
"pe_dma": 1446.945000000003,
|
||||||
|
"pe_fetch_store": 0,
|
||||||
|
"pe_ipcq": 0,
|
||||||
|
"pe_cpu": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"panel": "single_user_decode_gqa",
|
||||||
|
"kind": "decode",
|
||||||
|
"C": 1,
|
||||||
|
"P": 8,
|
||||||
|
"S_kv": 64,
|
||||||
|
"latency_ns": 3631.730500000015,
|
||||||
|
"op_log_summary": {
|
||||||
|
"gemm_count": 16,
|
||||||
|
"ipcq_copy_count": 21,
|
||||||
|
"dma_read_count": 24,
|
||||||
|
"dma_write_count": 1
|
||||||
|
},
|
||||||
|
"engine_occupancy_ns": {
|
||||||
|
"pe_gemm": 16.383999999998196,
|
||||||
|
"pe_math": 160.0,
|
||||||
|
"pe_dma": 1450.1150000000016,
|
||||||
|
"pe_fetch_store": 0,
|
||||||
|
"pe_ipcq": 0,
|
||||||
|
"pe_cpu": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"panel": "multi_user_decode_gqa",
|
||||||
|
"kind": "decode",
|
||||||
|
"C": 4,
|
||||||
|
"P": 8,
|
||||||
|
"S_kv": 128,
|
||||||
|
"latency_ns": 6692.578999999866,
|
||||||
|
"op_log_summary": {
|
||||||
|
"gemm_count": 64,
|
||||||
|
"ipcq_copy_count": 93,
|
||||||
|
"dma_read_count": 96,
|
||||||
|
"dma_write_count": 1
|
||||||
|
},
|
||||||
|
"engine_occupancy_ns": {
|
||||||
|
"pe_gemm": 32.76799999998184,
|
||||||
|
"pe_math": 688.0000000000009,
|
||||||
|
"pe_dma": 15919.751500000013,
|
||||||
|
"pe_fetch_store": 0,
|
||||||
|
"pe_ipcq": 0,
|
||||||
|
"pe_cpu": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 182 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 253 KiB |
|
After Width: | Height: | Size: 305 KiB |
|
After Width: | Height: | Size: 381 KiB |
|
After Width: | Height: | Size: 189 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 1014 KiB |
@@ -0,0 +1,57 @@
|
|||||||
|
|
||||||
|
=== H2D Write Latency (IO->HBM, data=32768B) ===
|
||||||
|
Case Target Hops Actual Ovhd Drain Wire Ovhd% Drain% Eff.BW BN.BW Util%
|
||||||
|
-------------------------------------------------------------------------------------------------------------------
|
||||||
|
h2d-1hop cube0.pe0 1 289.53 50.0 256.0 0.18 17.3% 88.4% 113.17 128.0 88.4%
|
||||||
|
h2d-2hop cube4.pe0 2 326.04 78.0 256.0 0.20 23.9% 78.5% 100.50 128.0 78.5%
|
||||||
|
h2d-3hop cube8.pe0 3 362.56 106.0 256.0 0.20 29.2% 70.6% 90.38 128.0 70.6%
|
||||||
|
h2d-4hop cube12.pe0 4 399.06 134.0 256.0 0.21 33.6% 64.1% 82.11 128.0 64.1%
|
||||||
|
[v] Monotonic increase: PASS
|
||||||
|
|
||||||
|
BW Saturation (Util% by data size):
|
||||||
|
Case 4KB 16KB 64KB 256KB 1MB
|
||||||
|
------------------------------------------------------------------
|
||||||
|
h2d-1hop 38.9% 71.8% 91.1% 97.6% 99.4%
|
||||||
|
h2d-2hop 29.0% 62.1% 86.8% 96.3% 99.1%
|
||||||
|
h2d-3hop 23.2% 54.7% 82.8% 95.1% 98.7%
|
||||||
|
h2d-4hop 19.3% 48.8% 79.2% 93.8% 98.4%
|
||||||
|
|
||||||
|
=== D2H Read Latency (HBM->IO, data=32768B) ===
|
||||||
|
Case Source Hops Actual Ovhd Drain Wire Ovhd% Drain% Eff.BW BN.BW Util%
|
||||||
|
-------------------------------------------------------------------------------------------------------------------
|
||||||
|
d2h-1hop cube0.pe0 1 571.20 23.0 256.0 0.04 4.0% 44.8% 57.37 128.0 44.8%
|
||||||
|
d2h-2hop cube4.pe0 2 635.72 51.0 256.0 0.05 8.0% 40.3% 51.54 128.0 40.3%
|
||||||
|
d2h-3hop cube8.pe0 3 700.24 79.0 256.0 0.06 11.3% 36.6% 46.80 128.0 36.6%
|
||||||
|
d2h-4hop cube12.pe0 4 764.76 107.0 256.0 0.07 14.0% 33.5% 42.85 128.0 33.5%
|
||||||
|
[v] Monotonic increase: PASS
|
||||||
|
|
||||||
|
BW Saturation (Util% by data size):
|
||||||
|
Case 4KB 16KB 64KB 256KB 1MB
|
||||||
|
------------------------------------------------------------------
|
||||||
|
d2h-1hop 9.2% 28.9% 61.9% 86.7% 96.3%
|
||||||
|
d2h-2hop 7.8% 25.2% 57.4% 84.4% 95.6%
|
||||||
|
d2h-3hop 6.7% 22.4% 53.5% 82.2% 94.9%
|
||||||
|
d2h-4hop 5.9% 20.1% 50.2% 80.1% 94.2%
|
||||||
|
[v] D2H >= H2D (reverse data path): PASS
|
||||||
|
|
||||||
|
=== PE DMA Latency (pe_dma -> router -> HBM, data=32768B) ===
|
||||||
|
Case Target Actual Ovhd Drain Wire Ovhd% Drain% Eff.BW BN.BW Util%
|
||||||
|
----------------------------------------------------------------------------------------------------------------------------
|
||||||
|
pe-local-hbm c0.pe0->c0.slice0 141.00 2.0 128.0 0.00 1.4% 90.8% 232.40 256.0 90.8%
|
||||||
|
pe-same-half-hbm c0.pe0->c0.slice1 147.87 4.0 128.0 0.03 2.7% 86.6% 221.60 256.0 86.6%
|
||||||
|
pe-cross-half-hbm c0.pe0->c0.slice4 161.17 10.0 128.0 0.09 6.2% 79.4% 203.31 256.0 79.4%
|
||||||
|
pe-cross-cube-hbm-best c0.pe0->c1.slice0 330.52 30.0 256.0 0.01 9.1% 77.5% 99.14 128.0 77.5%
|
||||||
|
pe-cross-cube-hbm-worst c0.pe0->c15.slice0 677.12 180.0 256.0 0.06 26.6% 37.8% 48.39 128.0 37.8%
|
||||||
|
* Local BN: 256.0 GB/s, Remote BN: 256.0 GB/s
|
||||||
|
[v] Cross-cube best < worst: PASS (330.52ns < 677.12ns)
|
||||||
|
|
||||||
|
BW Saturation (Util% by data size):
|
||||||
|
Case 4KB 16KB 64KB 256KB 1MB
|
||||||
|
------------------------------------------------------------------
|
||||||
|
pe-local-hbm 88.9% 97.0% 99.2% 99.8% 100.0%
|
||||||
|
pe-same-half-hbm 79.9% 94.1% 98.5% 99.6% 99.9%
|
||||||
|
pe-cross-half-hbm 61.3% 86.4% 96.2% 99.0% 99.8%
|
||||||
|
pe-cross-cube-hbm-best 51.6% 81.0% 94.5% 98.6% 99.6%
|
||||||
|
pe-cross-cube-hbm-worst 15.1% 41.6% 74.0% 91.9% 97.8%
|
||||||
|
|
||||||
|
============================================================
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="508" height="495" viewBox="70 40 508 495">
|
||||||
|
<title>sip</title>
|
||||||
|
<rect width="648" height="648" fill="#ffffff"/>
|
||||||
|
<line x1="108.0" y1="144.0" x2="252.0" y2="144.0" stroke="#000000" stroke-width="1" opacity="0.8"/>
|
||||||
|
<text x="180.0" y="140.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#000000">1.0mm 512GB/s</text>
|
||||||
|
<line x1="108.0" y1="144.0" x2="108.0" y2="264.0" stroke="#000000" stroke-width="1" opacity="0.8"/>
|
||||||
|
<text x="108.0" y="200.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#000000">1.0mm 512GB/s</text>
|
||||||
|
<line x1="252.0" y1="144.0" x2="396.0" y2="144.0" stroke="#000000" stroke-width="1" opacity="0.8"/>
|
||||||
|
<text x="324.0" y="140.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#000000">1.0mm 512GB/s</text>
|
||||||
|
<line x1="252.0" y1="144.0" x2="252.0" y2="264.0" stroke="#000000" stroke-width="1" opacity="0.8"/>
|
||||||
|
<text x="252.0" y="200.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#000000">1.0mm 512GB/s</text>
|
||||||
|
<line x1="396.0" y1="144.0" x2="540.0" y2="144.0" stroke="#000000" stroke-width="1" opacity="0.8"/>
|
||||||
|
<text x="468.0" y="140.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#000000">1.0mm 512GB/s</text>
|
||||||
|
<line x1="396.0" y1="144.0" x2="396.0" y2="264.0" stroke="#000000" stroke-width="1" opacity="0.8"/>
|
||||||
|
<text x="396.0" y="200.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#000000">1.0mm 512GB/s</text>
|
||||||
|
<line x1="540.0" y1="144.0" x2="540.0" y2="264.0" stroke="#000000" stroke-width="1" opacity="0.8"/>
|
||||||
|
<text x="540.0" y="200.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#000000">1.0mm 512GB/s</text>
|
||||||
|
<line x1="108.0" y1="264.0" x2="252.0" y2="264.0" stroke="#000000" stroke-width="1" opacity="0.8"/>
|
||||||
|
<text x="180.0" y="260.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#000000">1.0mm 512GB/s</text>
|
||||||
|
<line x1="108.0" y1="264.0" x2="108.0" y2="384.0" stroke="#000000" stroke-width="1" opacity="0.8"/>
|
||||||
|
<text x="108.0" y="320.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#000000">1.0mm 512GB/s</text>
|
||||||
|
<line x1="252.0" y1="264.0" x2="396.0" y2="264.0" stroke="#000000" stroke-width="1" opacity="0.8"/>
|
||||||
|
<text x="324.0" y="260.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#000000">1.0mm 512GB/s</text>
|
||||||
|
<line x1="252.0" y1="264.0" x2="252.0" y2="384.0" stroke="#000000" stroke-width="1" opacity="0.8"/>
|
||||||
|
<text x="252.0" y="320.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#000000">1.0mm 512GB/s</text>
|
||||||
|
<line x1="396.0" y1="264.0" x2="540.0" y2="264.0" stroke="#000000" stroke-width="1" opacity="0.8"/>
|
||||||
|
<text x="468.0" y="260.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#000000">1.0mm 512GB/s</text>
|
||||||
|
<line x1="396.0" y1="264.0" x2="396.0" y2="384.0" stroke="#000000" stroke-width="1" opacity="0.8"/>
|
||||||
|
<text x="396.0" y="320.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#000000">1.0mm 512GB/s</text>
|
||||||
|
<line x1="540.0" y1="264.0" x2="540.0" y2="384.0" stroke="#000000" stroke-width="1" opacity="0.8"/>
|
||||||
|
<text x="540.0" y="320.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#000000">1.0mm 512GB/s</text>
|
||||||
|
<line x1="108.0" y1="384.0" x2="252.0" y2="384.0" stroke="#000000" stroke-width="1" opacity="0.8"/>
|
||||||
|
<text x="180.0" y="380.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#000000">1.0mm 512GB/s</text>
|
||||||
|
<line x1="108.0" y1="384.0" x2="108.0" y2="504.0" stroke="#000000" stroke-width="1" opacity="0.8"/>
|
||||||
|
<text x="108.0" y="440.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#000000">1.0mm 512GB/s</text>
|
||||||
|
<line x1="252.0" y1="384.0" x2="396.0" y2="384.0" stroke="#000000" stroke-width="1" opacity="0.8"/>
|
||||||
|
<text x="324.0" y="380.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#000000">1.0mm 512GB/s</text>
|
||||||
|
<line x1="252.0" y1="384.0" x2="252.0" y2="504.0" stroke="#000000" stroke-width="1" opacity="0.8"/>
|
||||||
|
<text x="252.0" y="440.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#000000">1.0mm 512GB/s</text>
|
||||||
|
<line x1="396.0" y1="384.0" x2="540.0" y2="384.0" stroke="#000000" stroke-width="1" opacity="0.8"/>
|
||||||
|
<text x="468.0" y="380.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#000000">1.0mm 512GB/s</text>
|
||||||
|
<line x1="396.0" y1="384.0" x2="396.0" y2="504.0" stroke="#000000" stroke-width="1" opacity="0.8"/>
|
||||||
|
<text x="396.0" y="440.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#000000">1.0mm 512GB/s</text>
|
||||||
|
<line x1="540.0" y1="384.0" x2="540.0" y2="504.0" stroke="#000000" stroke-width="1" opacity="0.8"/>
|
||||||
|
<text x="540.0" y="440.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#000000">1.0mm 512GB/s</text>
|
||||||
|
<line x1="108.0" y1="504.0" x2="252.0" y2="504.0" stroke="#000000" stroke-width="1" opacity="0.8"/>
|
||||||
|
<text x="180.0" y="500.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#000000">1.0mm 512GB/s</text>
|
||||||
|
<line x1="252.0" y1="504.0" x2="396.0" y2="504.0" stroke="#000000" stroke-width="1" opacity="0.8"/>
|
||||||
|
<text x="324.0" y="500.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#000000">1.0mm 512GB/s</text>
|
||||||
|
<line x1="396.0" y1="504.0" x2="540.0" y2="504.0" stroke="#000000" stroke-width="1" opacity="0.8"/>
|
||||||
|
<text x="468.0" y="500.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#000000">1.0mm 512GB/s</text>
|
||||||
|
<polyline points="324.0,56.0 108.0,56.0 108.0,144.0" fill="none" stroke="#000000" stroke-width="1" opacity="0.8"/>
|
||||||
|
<text x="216.0" y="96.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#000000">2.5mm 512GB/s</text>
|
||||||
|
<polyline points="324.0,56.0 252.0,56.0 252.0,144.0" fill="none" stroke="#000000" stroke-width="1" opacity="0.8"/>
|
||||||
|
<text x="288.0" y="96.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#000000">2.5mm 512GB/s</text>
|
||||||
|
<polyline points="324.0,56.0 396.0,56.0 396.0,144.0" fill="none" stroke="#000000" stroke-width="1" opacity="0.8"/>
|
||||||
|
<text x="360.0" y="96.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#000000">2.5mm 512GB/s</text>
|
||||||
|
<polyline points="324.0,56.0 540.0,56.0 540.0,144.0" fill="none" stroke="#000000" stroke-width="1" opacity="0.8"/>
|
||||||
|
<text x="432.0" y="96.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#000000">2.5mm 512GB/s</text>
|
||||||
|
<rect x="84.0" y="128.0" width="48.0" height="32.0" rx="4" fill="#cbd5e1" stroke="#000000" stroke-width="1"/>
|
||||||
|
<text x="108.0" y="148.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#1e293b">CUBE (0,0)</text>
|
||||||
|
<rect x="228.0" y="128.0" width="48.0" height="32.0" rx="4" fill="#cbd5e1" stroke="#000000" stroke-width="1"/>
|
||||||
|
<text x="252.0" y="148.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#1e293b">CUBE (1,0)</text>
|
||||||
|
<rect x="372.0" y="128.0" width="48.0" height="32.0" rx="4" fill="#cbd5e1" stroke="#000000" stroke-width="1"/>
|
||||||
|
<text x="396.0" y="148.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#1e293b">CUBE (2,0)</text>
|
||||||
|
<rect x="516.0" y="128.0" width="48.0" height="32.0" rx="4" fill="#cbd5e1" stroke="#000000" stroke-width="1"/>
|
||||||
|
<text x="540.0" y="148.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#1e293b">CUBE (3,0)</text>
|
||||||
|
<rect x="84.0" y="248.0" width="48.0" height="32.0" rx="4" fill="#cbd5e1" stroke="#000000" stroke-width="1"/>
|
||||||
|
<text x="108.0" y="268.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#1e293b">CUBE (0,1)</text>
|
||||||
|
<rect x="228.0" y="248.0" width="48.0" height="32.0" rx="4" fill="#cbd5e1" stroke="#000000" stroke-width="1"/>
|
||||||
|
<text x="252.0" y="268.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#1e293b">CUBE (1,1)</text>
|
||||||
|
<rect x="372.0" y="248.0" width="48.0" height="32.0" rx="4" fill="#cbd5e1" stroke="#000000" stroke-width="1"/>
|
||||||
|
<text x="396.0" y="268.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#1e293b">CUBE (2,1)</text>
|
||||||
|
<rect x="516.0" y="248.0" width="48.0" height="32.0" rx="4" fill="#cbd5e1" stroke="#000000" stroke-width="1"/>
|
||||||
|
<text x="540.0" y="268.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#1e293b">CUBE (3,1)</text>
|
||||||
|
<rect x="84.0" y="368.0" width="48.0" height="32.0" rx="4" fill="#cbd5e1" stroke="#000000" stroke-width="1"/>
|
||||||
|
<text x="108.0" y="388.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#1e293b">CUBE (0,2)</text>
|
||||||
|
<rect x="228.0" y="368.0" width="48.0" height="32.0" rx="4" fill="#cbd5e1" stroke="#000000" stroke-width="1"/>
|
||||||
|
<text x="252.0" y="388.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#1e293b">CUBE (1,2)</text>
|
||||||
|
<rect x="372.0" y="368.0" width="48.0" height="32.0" rx="4" fill="#cbd5e1" stroke="#000000" stroke-width="1"/>
|
||||||
|
<text x="396.0" y="388.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#1e293b">CUBE (2,2)</text>
|
||||||
|
<rect x="516.0" y="368.0" width="48.0" height="32.0" rx="4" fill="#cbd5e1" stroke="#000000" stroke-width="1"/>
|
||||||
|
<text x="540.0" y="388.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#1e293b">CUBE (3,2)</text>
|
||||||
|
<rect x="84.0" y="488.0" width="48.0" height="32.0" rx="4" fill="#cbd5e1" stroke="#000000" stroke-width="1"/>
|
||||||
|
<text x="108.0" y="508.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#1e293b">CUBE (0,3)</text>
|
||||||
|
<rect x="228.0" y="488.0" width="48.0" height="32.0" rx="4" fill="#cbd5e1" stroke="#000000" stroke-width="1"/>
|
||||||
|
<text x="252.0" y="508.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#1e293b">CUBE (1,3)</text>
|
||||||
|
<rect x="372.0" y="488.0" width="48.0" height="32.0" rx="4" fill="#cbd5e1" stroke="#000000" stroke-width="1"/>
|
||||||
|
<text x="396.0" y="508.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#1e293b">CUBE (2,3)</text>
|
||||||
|
<rect x="516.0" y="488.0" width="48.0" height="32.0" rx="4" fill="#cbd5e1" stroke="#000000" stroke-width="1"/>
|
||||||
|
<text x="540.0" y="508.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#1e293b">CUBE (3,3)</text>
|
||||||
|
<rect x="308.0" y="50.0" width="32.0" height="12.0" rx="4" fill="#0ea5e9" stroke="#000000" stroke-width="1"/>
|
||||||
|
<text x="324.0" y="60.0" text-anchor="middle" font-family="monospace" font-size="10" fill="#ffffff">IO io0</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,47 @@
|
|||||||
|
\documentclass[10pt,twocolumn]{article}
|
||||||
|
|
||||||
|
\usepackage[margin=0.75in]{geometry}
|
||||||
|
\usepackage{graphicx}
|
||||||
|
\usepackage{booktabs}
|
||||||
|
\usepackage{amsmath}
|
||||||
|
\usepackage{amssymb}
|
||||||
|
\usepackage{siunitx}
|
||||||
|
\DeclareSIUnit\flop{FLOP}
|
||||||
|
\DeclareSIUnit\cycle{cycle}
|
||||||
|
\DeclareSIUnit\byte{B}
|
||||||
|
\usepackage{xcolor}
|
||||||
|
\usepackage{hyperref}
|
||||||
|
\hypersetup{colorlinks=true,linkcolor=blue!50!black,citecolor=blue!50!black,urlcolor=blue!50!black}
|
||||||
|
\usepackage{caption}
|
||||||
|
\usepackage{subcaption}
|
||||||
|
\captionsetup{font=small,labelfont=bf}
|
||||||
|
\usepackage{microtype}
|
||||||
|
\usepackage{tikz}
|
||||||
|
\usetikzlibrary{arrows.meta,positioning,calc,fit}
|
||||||
|
|
||||||
|
\graphicspath{{figures/}}
|
||||||
|
|
||||||
|
\title{\textbf{Hardware--Software Co-Design for Grouped-Query Attention on AHBM}}
|
||||||
|
|
||||||
|
\author{Mukesh Garg \and Jiyoon Lee \and Yangwook Kang}
|
||||||
|
|
||||||
|
\date{
|
||||||
|
\small
|
||||||
|
AGI Computing Lab, System Technology Group\\
|
||||||
|
2026 H1 Report
|
||||||
|
}
|
||||||
|
|
||||||
|
\begin{document}
|
||||||
|
\maketitle
|
||||||
|
|
||||||
|
\input{sections/00-exec-summary}
|
||||||
|
\input{sections/01-introduction}
|
||||||
|
\input{sections/02-platform}
|
||||||
|
\input{sections/03-gemm}
|
||||||
|
\input{sections/04-allreduce}
|
||||||
|
\input{sections/05-gqa}
|
||||||
|
\input{sections/06-discussion}
|
||||||
|
\input{sections/07-conclusion}
|
||||||
|
\input{sections/08-future-work}
|
||||||
|
|
||||||
|
\end{document}
|
||||||