diff --git a/.github/workflows/aten-matrix-full.yml b/.github/workflows/aten-matrix-full.yml new file mode 100644 index 00000000..8ea0e20a --- /dev/null +++ b/.github/workflows/aten-matrix-full.yml @@ -0,0 +1,175 @@ +name: ATen Tests (full matrix, manual) + +# MANUAL-ONLY (workflow_dispatch) full ATen sweep. The per-PR gate is the fast +# `aten-tests.yml` ("ATen Tests (quick)"); this heavy matrix is run on demand +# (Actions tab -> "ATen Tests (full matrix, manual)" -> Run workflow) before +# merges / releases or when touching the DMA / tile-size paths. It takes ~45-50 +# min (the mlen=256 jobs dominate). +# +# Full unified [batch_size, seq_len, hidden] sweep (rows = batch_size*seq_len): +# tests = {softmax, linear, rms-norm, layer-norm, ffn, +# flash-attention, embedding-add, rope} (8 ops) +# mlen = {16, 64, 256} +# blen = {4, 8, 16, 32} +# batch_size = {1, 2} +# seq_len = {4, 16, 64, 128, 256} +# +# 8*3*4*2*5 = 960 logical cells. GitHub caps a matrix at 256 jobs, so the JOBS +# are matrixed only over (mlen, batch_size) = 3*2 = 6 jobs; each builds the +# emulator once (nix) and LOOPS over (test x blen x seq_len) in bash, pruning +# invalid cells. +# +# --------------------------------------------------------------------------- +# PRUNING RULES (a cell is SKIPPED, not FAILED, when any holds): +# 1. blen > mlen -- block length cannot exceed tile width. +# 2. mlen % blen != 0 -- MLEN must be a whole number of blocks. +# 3. (batch_size*seq_len) % blen != 0 +# -- the token-row count must be BLEN-aligned. +# 4. flash-attention with seq_len > mlen +# -- packed-GQA supports one sequence tile. +# 5. softmax with rows (=batch_size*seq_len) > mlen +# -- single [rows, mlen] score-tile accumulator. +# +# Flash-attention (GQA) validates with genuine per-element agreement across the +# full matrix after the test harness was fixed to pass explicit tensor_layouts + +# hbm_addrs for K/V. There are NO tracked flash-attention skips; only the +# structural rules above apply. +# --------------------------------------------------------------------------- +# +# A job FAILS if any non-skipped cell fails. Each job prints a per-cell +# PASS / SKIP / FAIL summary table at the end. + +on: + workflow_dispatch: + +jobs: + aten-matrix: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + mlen: [16, 64, 256] + batch_size: [1, 2] + name: aten mlen=${{ matrix.mlen }} batch=${{ matrix.batch_size }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Free disk space + run: | + echo "Before:"; df -h / + sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android /opt/hostedtoolcache/CodeQL /usr/local/share/boost "$AGENT_TOOLSDIRECTORY" || true + sudo docker image prune --all --force || true + echo "After:"; df -h / + + - name: Install Nix + uses: cachix/install-nix-action@v30 + with: + nix_path: nixpkgs=channel:nixos-25.05 + + - name: Cache cargo registry and build + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + transactional_emulator/target + key: cargo-${{ runner.os }}-${{ hashFiles('transactional_emulator/Cargo.lock') }} + restore-keys: | + cargo-${{ runner.os }}- + + - name: Build Rust emulator + run: nix develop --command bash -c "cd transactional_emulator && cargo build --release" + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Set up Python and install dependencies + run: | + uv python install 3.12 + uv sync + + - name: Run ATen matrix for mlen=${{ matrix.mlen }} batch=${{ matrix.batch_size }} + env: + MLEN: ${{ matrix.mlen }} + BATCH: ${{ matrix.batch_size }} + run: | + set -u + tests="softmax linear rms-norm layer-norm ffn flash-attention embedding-add rope" + blens="4 8 16 32" + seqs="4 16 64 128 256" + + fail=0 + summary="" + + for t in $tests; do + for blen in $blens; do + # --- Pruning rule 1: blen must not exceed mlen. + if [ "$blen" -gt "$MLEN" ]; then + for s in $seqs; do + summary="${summary}\nSKIP ${t} mlen=${MLEN} blen=${blen} batch=${BATCH} seq=${s} (blen>mlen)" + done + continue + fi + # --- Pruning rule 2: mlen must be divisible by blen. + if [ $((MLEN % blen)) -ne 0 ]; then + for s in $seqs; do + summary="${summary}\nSKIP ${t} mlen=${MLEN} blen=${blen} batch=${BATCH} seq=${s} (mlen%blen!=0)" + done + continue + fi + + for s in $seqs; do + rows=$((BATCH * s)) + cell="${t} mlen=${MLEN} blen=${blen} batch=${BATCH} seq=${s} rows=${rows}" + + # --- Pruning rule 3: rows (=batch*seq) must be BLEN-aligned. + if [ $((rows % blen)) -ne 0 ]; then + summary="${summary}\nSKIP ${cell} (rows%blen!=0)" + continue + fi + + # --- Pruning rule 4: flash-attention packed-GQA supports exactly + # one sequence tile, so seq_len (== kv_seq_len) must be <= mlen. + if [ "$t" = "flash-attention" ] && [ "$s" -gt "$MLEN" ]; then + summary="${summary}\nSKIP ${cell} (attention: seq_len>mlen unsupported)" + continue + fi + + # --- Pruning rule 5: softmax runs on a single [rows, mlen] + # score tile, so rows (=batch*seq) must be <= mlen. + if [ "$t" = "softmax" ] && [ "$rows" -gt "$MLEN" ]; then + summary="${summary}\nSKIP ${cell} (softmax: rows>mlen one-tile limit)" + continue + fi + + echo "============================================================" + echo ":: RUN ${cell}" + echo "============================================================" + if nix develop --command bash -c \ + "just test-aten-$t --mlen $MLEN --blen $blen --batch-size $BATCH --seq-len $s"; then + echo ":: PASS ${cell}" + summary="${summary}\nPASS ${cell}" + else + echo ":: FAIL ${cell}" + summary="${summary}\nFAIL ${cell}" + fail=1 + fi + done + done + done + + echo "" + echo "============================================================" + echo " Per-cell summary (mlen=${MLEN} batch=${BATCH})" + echo "============================================================" + printf '%b\n' "$summary" + echo "------------------------------------------------------------" + printf 'PASS=%s SKIP=%s FAIL=%s\n' \ + "$(printf '%b' "$summary" | grep -c '^PASS')" \ + "$(printf '%b' "$summary" | grep -c '^SKIP')" \ + "$(printf '%b' "$summary" | grep -c '^FAIL')" + echo "============================================================" + + exit $fail diff --git a/.github/workflows/aten-tests.yml b/.github/workflows/aten-tests.yml new file mode 100644 index 00000000..244c3c81 --- /dev/null +++ b/.github/workflows/aten-tests.yml @@ -0,0 +1,134 @@ +name: ATen Tests (quick) + +# Fast per-PR gate for the standalone testbench/aten suite. Runs a small set of +# CRUCIAL cells (all 8 ops + a sub-64 / RTL-tile sanity + a batch>1 sanity), all +# at mlen<=64 so the whole job stays well under ~10 min. Every cell here passes +# with real per-element agreement. +# +# The EXHAUSTIVE sweep (mlen{16,64,256} x blen{4,8,16,32} x batch{1,2} x +# seq{4,16,64,128,256}, incl. the slow mlen=256 jobs) is NOT run per-PR -- it +# lives in `aten-matrix-full.yml` ("ATen Tests (full matrix, manual)") and is +# triggered manually from the Actions tab before merges / DMA / tile-size work. +# +# Crucial cells (format: "op mlen blen batch seq"): +# - all 8 ops @ mlen64 blen8 b1 seq64 (canonical one-tile coverage) +# - linear + flash-attention @ mlen16 blen4 b1 seq16 (sub-64 / RTL tile path) +# - linear + flash-attention @ mlen64 blen16 b2 seq32 (batch>1 path) + +on: + push: + branches: [main] + paths: + - 'transactional_emulator/**' + - 'PLENA_Compiler/**' + - 'PLENA_Tools/**' + - 'flake.nix' + - 'flake.lock' + - 'pyproject.toml' + - 'uv.lock' + - '.github/workflows/aten-tests.yml' + pull_request: + branches: [main] + paths: + - 'transactional_emulator/**' + - 'PLENA_Compiler/**' + - 'PLENA_Tools/**' + - 'flake.nix' + - 'flake.lock' + - 'pyproject.toml' + - 'uv.lock' + - '.github/workflows/aten-tests.yml' + +jobs: + aten-quick: + runs-on: ubuntu-latest + name: aten quick (mlen<=64 crucial cells) + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Free disk space + run: | + sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android /opt/hostedtoolcache/CodeQL /usr/local/share/boost "$AGENT_TOOLSDIRECTORY" || true + sudo docker image prune --all --force || true + + - name: Install Nix + uses: cachix/install-nix-action@v30 + with: + nix_path: nixpkgs=channel:nixos-25.05 + + - name: Cache cargo registry and build + # Same cache key as aten-matrix-full.yml so the two share a warm build. + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + transactional_emulator/target + key: cargo-${{ runner.os }}-${{ hashFiles('transactional_emulator/Cargo.lock') }} + restore-keys: | + cargo-${{ runner.os }}- + + - name: Build Rust emulator + run: nix develop --command bash -c "cd transactional_emulator && cargo build --release" + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Set up Python and install dependencies + run: | + uv python install 3.12 + uv sync + + - name: Run crucial ATen cells + run: | + set -u + # "op mlen blen batch seq" + cells=( + "softmax 64 8 1 64" + "linear 64 8 1 64" + "rms-norm 64 8 1 64" + "layer-norm 64 8 1 64" + "ffn 64 8 1 64" + "flash-attention 64 8 1 64" + "embedding-add 64 8 1 64" + "rope 64 8 1 64" + "linear 16 4 1 16" + "flash-attention 16 4 1 16" + "linear 64 16 2 32" + "flash-attention 64 16 2 32" + ) + + fail=0 + summary="" + for cell in "${cells[@]}"; do + set -- $cell + op=$1; mlen=$2; blen=$3; batch=$4; seq=$5 + desc="${op} mlen=${mlen} blen=${blen} batch=${batch} seq=${seq}" + echo "============================================================" + echo ":: RUN ${desc}" + echo "============================================================" + if nix develop --command bash -c \ + "just test-aten-$op --mlen $mlen --blen $blen --batch-size $batch --seq-len $seq"; then + echo ":: PASS ${desc}" + summary="${summary}\nPASS ${desc}" + else + echo ":: FAIL ${desc}" + summary="${summary}\nFAIL ${desc}" + fail=1 + fi + done + + echo "" + echo "============================================================" + echo " Quick ATen summary" + echo "============================================================" + printf '%b\n' "$summary" + echo "------------------------------------------------------------" + printf 'PASS=%s FAIL=%s\n' \ + "$(printf '%b' "$summary" | grep -c '^PASS')" \ + "$(printf '%b' "$summary" | grep -c '^FAIL')" + echo "============================================================" + + exit $fail diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b6a9902..8438ea72 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + submodules: recursive - name: Install uv uses: astral-sh/setup-uv@v4 diff --git a/PLENA_Compiler b/PLENA_Compiler index 287f6fbe..42f1e9d2 160000 --- a/PLENA_Compiler +++ b/PLENA_Compiler @@ -1 +1 @@ -Subproject commit 287f6fbea3b4d48c2bf425a727b7914e23b9c501 +Subproject commit 42f1e9d23ced662f57552b8eab69eb989bdad1e1 diff --git a/PLENA_Tools b/PLENA_Tools index 9055bda0..94d3508a 160000 --- a/PLENA_Tools +++ b/PLENA_Tools @@ -1 +1 @@ -Subproject commit 9055bda069bf959a07127a066ca21f614ca1fa03 +Subproject commit 94d3508a8c9439e4a4bc4652fcb1e1769311cda2 diff --git a/doc/HANDOFF_2026-05-28.md b/doc/HANDOFF_2026-05-28.md new file mode 100644 index 00000000..1e483a3a --- /dev/null +++ b/doc/HANDOFF_2026-05-28.md @@ -0,0 +1,148 @@ +# Handoff Document — 2026-05-28 + +## Session Summary + +Two-day session (2026-05-27/28) covering testbench overhaul, ISA profiling infrastructure, weight streaming memory models, and critical bug discovery. + +## PRs + +| PR | Branch | Status | Description | +|----|--------|--------|-------------| +| #55 | exp/roll-attention-head-batch | **Merged** | Unified model runner, testbench reorg, per-build TOML, cleanup | +| #58 | kev/profile-and-opt | **Draft** | ASM profiler, board configs, weight streaming models. Parked for report. | +| #60 | fix/aten-test-configurable-mlen | **Open** | Configurable ATen tests + config chain fix + golden module. 11 commits. | + +## Critical Bugs Found + +### 1. Config Chain Was Broken (FIXED in PR #60) + +**What:** The Rust emulator hardcoded `../plena_settings.toml` and ignored `PLENA_SETTINGS_TOML` env var. Per-build TOMLs from `HardwareConfig.write_toml()` were invisible to the emulator. + +**Impact:** ALL previous test runs at MLEN ≠ 64 were actually running the emulator at MLEN=64. The per-build TOML was only used by Python (for `create_mem_for_sim` precision settings), never by the Rust emulator. + +**Fix:** `load_config.rs` now checks `PLENA_SETTINGS_TOML` env var first. `cli.rs` adds `--settings` flag. `emulator_runner.py` passes `--settings` and verifies config match post-run. + +**Verification:** Emulator now logs `Topology mlen=128 ...` at warn level, visible in test output. + +### 2. FFN Down-Projection Multi-Tile Bug (FIXED 2026-05-28) + +**What:** `_ffn_asm_with_loops` and `_ffn_asm_fused_up_gate` in `PLENA_Compiler/asm_templates/ffn_asm.py` used `_load_large_int(a_actual_register, mlen * hidden_size)` for the down weight HBM offset advance per K-tile, overwriting the register instead of adding to it. + +**Symptom:** When down had `K_tiles ≥ 3`, K-tiles 2..N-1 all prefetched from the same wrong HBM address. When `out_size > mlen` (multi outer iter) with `K_tiles = 2`, K-tile 1 of later outer iters loaded from the wrong absolute address. This is the actual root cause behind what we previously called the "K-split" bug. + +**Fix:** Changed `_load_large_int(a_actual, mlen*hidden_size)` to `_addi_large_int(a_actual, a_actual, mlen*hidden_size, w_temp)` at all three down-projection prefetch sites (lines 1333, 1699, 1777). Matches the up-projection's `_addi` pattern that was already correct. + +**Verification (after fix):** +| Config | Before | After | +|--------|--------|-------| +| hidden=64, inter=128 (down K=2, out=1) | PASS | PASS | +| hidden=64, inter=256 (down K=4, out=1) | FAIL | PASS | +| hidden=128, inter=128 (down K=2, out=2) | FAIL | PASS | +| hidden=128, inter=256 (down K=2, out=4) | FAIL | PASS | +| hidden=128, inter=64 (down K=1) | PASS | PASS | + +**Previous results invalidated:** The "99% allclose" native results in `doc/EMULATOR_MODEL_CONFIG_MATRIX.md` were from the emulator running at MLEN=64 (config bug #1). They need re-verification at correct MLEN. + +### 3. `sim_env_utils.py` TOML Mode Bug (FIXED in PR #60) + +**What:** `load_toml_config()` defaulted to `mode="BEHAVIOR"` instead of `mode="TRANSACTIONAL"`. Per-build TOMLs don't have a BEHAVIOR section → KeyError on `HBM_M_WEIGHT_TYPE`. + +**Fix:** Changed to `mode="TRANSACTIONAL"` in `sim_env_utils.py`. + +## What Works Now + +### ATen Tests at Varying MLEN (PR #60) + +All tests accept `--mlen --blen --hlen --hidden-size --batch-size --seed`. + +| Test | MLEN=64 | MLEN=128 | Notes | +|------|---------|----------|-------| +| linear | **PASS** | **PASS** | | +| rms_norm | **PASS** | **PASS** | | +| layer_norm | **PASS** | **PASS** | | +| ffn | **PASS** (all hidden/inter) | **PASS** | After down K-split fix | +| softmax | **PASS** | **PASS** | | +| embedding_add | **PASS** | **PASS** | | +| rope | **PASS** | **PASS** | Required HBM MXFP8 in golden + compact tensor_layouts + compiler-derived hbm_addrs | +| norm (rms+layer) | **PASS** | **PASS** | Required fp_preload of eps + 1/hidden at the right offsets per norm type | +| flash_attn_gqa | **PASS** | **PASS** | Required `args.hlen = h_qkv` and BEHAVIOR.CONFIG in per-build TOML so PlenaCompiler sees correct HLEN | + +### ASM Profiler (PR #58, draft) + +```bash +python3 transactional_emulator/testbench/aten/compare/asm_profiler.py --board nexys_a7 --mlen 256 +``` + +- Per-opcode cycle breakdown with category summaries +- Board configs: `nexys_a7.yaml` (DDR3-1600 x16, validated vs JEDEC), `v80.yaml` (HBM2e) +- Validated to 1.4% accuracy against Rust emulator + +### Weight Streaming Memory Models (PR #58, draft) + +```bash +./transactional_emulator --memory-model host-stream --weight-manifest manifest.json --host-bandwidth 38000000 +``` + +- HostStream: direct host-to-SRAM at configurable bandwidth +- LayerSwapping: DDR3 with capacity limit and swap penalties +- Validated end-to-end: identical VRAM output, different timing + +## Remaining Work + +### Done in this update (2026-05-28 evening) +1. ~~Fix K-split bug in `ffn_asm.py`~~ — fixed: `_load_large_int` → `_addi_large_int` for down-projection HBM advance at all three call sites +2. ~~Fix rope_test.py~~ — fixed: HBM MXFP8 in `golden_rope`, compact tensor_layouts + compiler-derived hbm_addrs +3. ~~Fix norm_test.py~~ — fixed: HBM MXFP8 in `golden_rms_norm`/`golden_layer_norm`, fp_preload with eps + 1/hidden at the right offsets per norm type +4. ~~Fix flash_attention_gqa_test.py~~ — fixed: `args.hlen = h_qkv` before `setup_hw`, plus BEHAVIOR.CONFIG section in per-build TOML so PlenaCompiler sees the right HLEN + +### Medium Priority +5. **Update EMULATOR_MODEL_CONFIG_MATRIX.md** — previous results need re-verification at correct MLEN +6. **Re-run native SmolVLM2 profile** at correct MLEN=256 with the down K-split fix +7. **Fix `quant.*` imports properly** — current shim works but compiler submodule should be updated to use `plena_quant.*` + +### Low Priority +8. **nix + uv env consolidation** — `flake.nix` changes for dropping conda (tested locally, not committed) +9. **Weight manifest generation** — compiler needs to emit `weight_manifest.json` for streaming model testing + +## Key Files Modified + +### PR #60 (11 commits) +``` +transactional_emulator/src/load_config.rs — PLENA_SETTINGS_TOML env var support +transactional_emulator/src/cli.rs — --settings flag +transactional_emulator/src/main.rs — env var wiring, Topology at warn level, PREFETCH clamp +transactional_emulator/testbench/aten/golden.py — NEW: hardware-accurate golden helpers +transactional_emulator/testbench/aten/configurable.py — add_hw_args(), setup_hw(), TOML text dump +transactional_emulator/testbench/aten/*_test.py — all 9 tests configurable + golden migration +transactional_emulator/testbench/emulator_runner.py — --settings passthrough, config mismatch detection +transactional_emulator/testbench/sim_env_utils.py — TRANSACTIONAL mode for load_toml_config +quant/ — NEW: compatibility shim for compiler imports +justfile — all test-aten-* accept *args +``` + +### PR #58 (11 commits, draft) +``` +transactional_emulator/testbench/aten/compare/asm_profiler.py — renamed from isa_analysis.py +transactional_emulator/testbench/board_configs/nexys_a7.yaml — DDR3-1600 x16 validated +transactional_emulator/testbench/board_configs/v80.yaml — Versal V80 +transactional_emulator/lib/memory/src/streaming.rs — HostStream + LayerSwapping +doc/SMOLVLM2_ISA_PROFILE.md — per-kernel cycle breakdown +``` + +## How to Run Tests + +```bash +# Default (MLEN=64) +just test-aten-linear + +# Custom config +just test-aten-linear --mlen 128 --blen 16 +just test-aten-ffn --mlen 128 --blen 16 --hidden-size 128 --inter-dim 256 + +# Full sweep (passing tests only) +for test in linear rms-norm layer-norm ffn softmax embedding-add; do + for mlen in 64 128; do + echo "=== $test MLEN=$mlen ===" && just test-aten-$test --mlen $mlen --blen 16 + done +done +``` diff --git a/justfile b/justfile index 4ea13182..c4a8d2a0 100644 --- a/justfile +++ b/justfile @@ -102,20 +102,20 @@ test-sw: python3 PLENA_Tools/plena_quant/quant_operations/sqrt.py python3 PLENA_Tools/plena_quant/quant_operations/reciprocal.py -test-aten-softmax: - python3 transactional_emulator/testbench/aten/fpvar_softmax_test.py +test-aten-softmax *args: + python3 transactional_emulator/testbench/aten/fpvar_softmax_test.py {{args}} -test-aten-linear: - python3 transactional_emulator/testbench/aten/linear_test.py +test-aten-linear *args: + python3 transactional_emulator/testbench/aten/linear_test.py {{args}} -test-aten-rms-norm: - python3 transactional_emulator/testbench/aten/rms_norm_test.py +test-aten-rms-norm *args: + python3 transactional_emulator/testbench/aten/rms_norm_test.py {{args}} -test-aten-layer-norm: - python3 transactional_emulator/testbench/aten/layer_norm_test.py +test-aten-layer-norm *args: + python3 transactional_emulator/testbench/aten/layer_norm_test.py {{args}} -test-aten-ffn: - python3 transactional_emulator/testbench/aten/ffn_test.py +test-aten-ffn *args: + python3 transactional_emulator/testbench/aten/ffn_test.py {{args}} # Unified model compile/emulate (use model nickname from YAML configs) # Examples: @@ -141,8 +141,8 @@ test-large-immediate: asm-profile asm_path="": python3 analytic_models/roofline/asm_profiler.py {{asm_path}} -test-aten-flash-attention: - python3 transactional_emulator/testbench/aten/flash_attention_gqa_test.py +test-aten-flash-attention *args: + python3 transactional_emulator/testbench/aten/flash_attention_gqa_test.py {{args}} test-aten-bmm: python3 transactional_emulator/testbench/direct_emit/bmm_test.py @@ -157,11 +157,11 @@ test-aten-conv2d preset="all": python3 transactional_emulator/testbench/aten/vision/conv2d_test.py --preset {{preset}}; \ fi -test-aten-embedding-add: - python3 transactional_emulator/testbench/aten/embedding_add_test.py +test-aten-embedding-add *args: + python3 transactional_emulator/testbench/aten/embedding_add_test.py {{args}} -test-aten-rope: - python3 transactional_emulator/testbench/aten/rope_test.py +test-aten-rope *args: + python3 transactional_emulator/testbench/aten/rope_test.py {{args}} # Generate and profile multi-layer decoder ASM (smolvlm2: 30 layers, 1 step; llada: 32 layers x 64 denoising steps + LM head) multilayer-decoder-profile model="smolvlm2": diff --git a/pyproject.toml b/pyproject.toml index 3fcc8036..11e9a8de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,8 +36,8 @@ torch = { index = "pytorch-cu126" } indent-width = 4 line-length = 120 target-version = "py312" -# PLENA_Compiler is a git submodule with its own linting; it is not part of this repo's source. -extend-exclude = ["PLENA_Compiler"] +# PLENA_Compiler and PLENA_Tools are git submodules with their own linting; not part of this repo's source. +extend-exclude = ["PLENA_Compiler", "PLENA_Tools"] [tool.ruff.lint] select = [ @@ -91,6 +91,7 @@ include-package-data = false where = ["PLENA_Tools", "."] include = [ "plena_quant*", + "quant*", "plena_utils*", "memory_mapping*", "sim_env_utils*", @@ -108,6 +109,7 @@ exclude = [ [tool.setuptools.package-dir] "" = "." plena_quant = "PLENA_Tools/plena_quant" +quant = "PLENA_Tools/plena_quant" plena_utils = "PLENA_Tools/plena_utils" memory_mapping = "PLENA_Tools/memory_mapping" sim_env_utils = "PLENA_Tools/sim_env_utils" diff --git a/quant b/quant new file mode 120000 index 00000000..2cfaf959 --- /dev/null +++ b/quant @@ -0,0 +1 @@ +PLENA_Tools/plena_quant \ No newline at end of file diff --git a/transactional_emulator/lib/memory/src/lib.rs b/transactional_emulator/lib/memory/src/lib.rs index af804628..e2ec4b8e 100644 --- a/transactional_emulator/lib/memory/src/lib.rs +++ b/transactional_emulator/lib/memory/src/lib.rs @@ -114,7 +114,17 @@ impl MemoryBacked { impl MemoryModel for MemoryBacked { /// Read 64-bytes of memory. async fn read(&self, addr: u64) -> [u8; 64] { - self.data.lock().unwrap()[addr as usize / 64] + // HBM bursts read aligned 64-byte words covering [addr, addr+len); an + // H_PREFETCH that requests more rows than a tensor has (e.g. blen > seq_len) + // over-reads past the tensor's end. Those bytes land in unused (padding) VRAM + // rows and are never compared, so return zeros for out-of-capacity addresses + // instead of panicking on the backing Vec index. + self.data + .lock() + .unwrap() + .get(addr as usize / 64) + .copied() + .unwrap_or([0u8; 64]) } /// Write 64-bytes of memory. diff --git a/transactional_emulator/src/cli.rs b/transactional_emulator/src/cli.rs index ee821017..74774565 100644 --- a/transactional_emulator/src/cli.rs +++ b/transactional_emulator/src/cli.rs @@ -159,4 +159,9 @@ pub(crate) struct Opts { /// only a small HBM prefix can pass e.g. `--hbm-size 256M` to bound the /// steady-state RSS. pub(crate) hbm_size: Option, + + #[arg(long)] + /// Path to plena_settings.toml. Overrides PLENA_SETTINGS_TOML env var and + /// the default ../plena_settings.toml lookup. + pub(crate) settings: Option, } diff --git a/transactional_emulator/src/dma.rs b/transactional_emulator/src/dma.rs index 2fd4784f..c22e5cc9 100644 --- a/transactional_emulator/src/dma.rs +++ b/transactional_emulator/src/dma.rs @@ -58,7 +58,10 @@ impl MxLayout { assert!(element_bits.is_power_of_two()); let len_in_bits = element_bits as u32 * dim; - assert!(len_in_bits.is_multiple_of(8 * 64)); + // A load must be a whole number of bytes. This was previously required + // to be a full 64-byte HBM burst (`8 * 64`); relaxed for sub-64 MLEN, + // which packs fewer than 64 bytes per row. + assert!(len_in_bits.is_multiple_of(8)); let len_in_bytes = len_in_bits / 8; let scale_len_in_bytes = if let MxDataType::Mx { @@ -171,17 +174,32 @@ pub(crate) fn transfer_mx_from_hbm( as usize + block_idx as usize * scale_len_in_bytes_per_load as usize; - // Element chunks: - for i in 0..(len_in_bytes_per_load as usize).div_ceil(64) { - let chunk_offset = byte_offset + i * 64; - let chunk_size = std::cmp::min(64, total_bytes - chunk_offset); - let addr = element_addr + (i * 64) as u64; - assert!(addr.is_multiple_of(64)); + // Element chunks: walk the byte range + // [element_addr, element_addr + len_in_bytes_per_load) one + // 64-byte block at a time, emitting a ChunkRead clamped to each + // block's boundaries. `gather` truncates a read at the block + // end, so no single ChunkRead may straddle a boundary. For + // MLEN >= 64 (element_addr 64-aligned, len a 64-multiple) this + // reduces to exactly one full-64-byte read per block. + let element_end = element_addr + len_in_bytes_per_load as u64; + let mut blk = (element_addr / 64) * 64; + while blk < element_end { + let copy_start = std::cmp::max(blk, element_addr); + let copy_end = std::cmp::min(blk + 64, element_end); + let addr = copy_start; + let dst_offset = byte_offset + (copy_start - element_addr) as usize; + // Clamp against the gather buffer end (matches the previous + // `min(64, total_bytes - chunk_offset)` behaviour). + let mut len = (copy_end - copy_start) as usize; + if dst_offset + len > total_bytes { + len = total_bytes - dst_offset; + } reads.push(memory::chunked::ChunkRead { addr, - dst_offset: chunk_offset, - len: chunk_size, + dst_offset, + len, }); + blk += 64; } // Scale chunk (if Mx type). The byte primitive fetches the @@ -348,8 +366,11 @@ pub(crate) async fn transfer_mx_to_hbm( let element_addr = index + (store_iter * stride) as u64; let scale_addr = scale_index + (store_iter as f32 * layout.stride_scale) as u64; - // Write element bytes to HBM (64-byte aligned chunks) - memory::chunked::write_aligned( + // Write element bytes to HBM via read-modify-write. element_addr need + // not be 64-aligned (sub-64 MLEN), and write_unaligned avoids + // clobbering neighbouring bytes. For MLEN >= 64 (element_addr + // 64-aligned, len a 64-multiple) this is equivalent to write_aligned. + let _ = memory::chunked::write_unaligned( &hbm, element_addr, len_in_bytes_per_store as usize, diff --git a/transactional_emulator/src/load_config.rs b/transactional_emulator/src/load_config.rs index afb436bb..43f651c8 100644 --- a/transactional_emulator/src/load_config.rs +++ b/transactional_emulator/src/load_config.rs @@ -396,6 +396,12 @@ pub static CONFIG: LazyLock = LazyLock::new(|| { // Configuration loading functions pub fn load_config() -> Result> { + // 1. Check PLENA_SETTINGS_TOML env var (set by per-build test harness) + if let Ok(path) = env::var("PLENA_SETTINGS_TOML") { + return load_config_from_file(&path); + } + + // 2. Fallback to hardcoded ../plena_settings.toml let config_path = env::current_dir() .unwrap() .parent() diff --git a/transactional_emulator/src/main.rs b/transactional_emulator/src/main.rs index d1a5c8cb..281c571d 100644 --- a/transactional_emulator/src/main.rs +++ b/transactional_emulator/src/main.rs @@ -58,7 +58,31 @@ static MATRIX_WEIGHT_TYPE: LazyLock = LazyLock::new(|| matrix_weight static MATRIX_KV_TYPE: LazyLock = LazyLock::new(|| matrix_kv_type()); static VECTOR_ACTIVATION_TYPE: LazyLock = LazyLock::new(|| vector_activation_type()); static VECTOR_KV_TYPE: LazyLock = LazyLock::new(|| vector_kv_type()); -static PREFETCH_M_AMOUNT: LazyLock = LazyLock::new(|| hbm_m_prefetch_amount()); +static PREFETCH_M_AMOUNT: LazyLock = LazyLock::new(|| { + let raw = hbm_m_prefetch_amount(); + let mlen = mlen(); + // Must be a multiple of MLEN (one full matrix tile per write). + // Round up to the nearest multiple of MLEN if needed. + if raw < mlen { + tracing::warn!( + "HBM_M_Prefetch_Amount ({}) < MLEN ({}); clamping to MLEN", + raw, + mlen + ); + mlen + } else if raw % mlen != 0 { + let clamped = ((raw + mlen - 1) / mlen) * mlen; + tracing::warn!( + "HBM_M_Prefetch_Amount ({}) not a multiple of MLEN ({}); rounding up to {}", + raw, + mlen, + clamped + ); + clamped + } else { + raw + } +}); static PREFETCH_V_AMOUNT: LazyLock = LazyLock::new(|| hbm_v_prefetch_amount()); static STORE_V_AMOUNT: LazyLock = LazyLock::new(|| hbm_v_writeback_amount()); @@ -795,6 +819,15 @@ impl Accelerator { async fn start() { let opts = Opts::parse(); + // If --settings is given, set PLENA_SETTINGS_TOML env var BEFORE any + // LazyLock access (which triggers load_config()). This ensures the + // per-build TOML is used for all config values. + if let Some(ref settings_path) = opts.settings { + // SAFETY: set_var is called before any threads are spawned and before + // LazyLock statics are accessed, so no concurrent readers exist. + unsafe { std::env::set_var("PLENA_SETTINGS_TOML", settings_path.as_os_str()) }; + } + // Initialize tracing subscriber. // // Filter precedence: `--log-level` (full override) > `RUST_LOG` > default (debug). @@ -833,7 +866,7 @@ async fn start() { .with(file_layer) .init(); - tracing::info!( + tracing::warn!( mlen = *MLEN, vlen = *VLEN, hlen = *HLEN, @@ -855,6 +888,11 @@ async fn start() { max_loop_instructions = *MAX_LOOP_INSTRUCTIONS, "Pipeline" ); + tracing::info!( + settings = %std::env::var("PLENA_SETTINGS_TOML") + .unwrap_or_else(|_| "default (../plena_settings.toml)".to_string()), + "Config source" + ); let mram = Arc::new(MatrixSram::new(*MLEN, *MATRIX_SRAM_SIZE, *MATRIX_SRAM_TYPE)); // Matrix SRAM let vram = Arc::new(VectorSram::from_mx_type( diff --git a/transactional_emulator/testbench/aten/configurable.py b/transactional_emulator/testbench/aten/configurable.py index 18b5e49f..fbd889d5 100644 --- a/transactional_emulator/testbench/aten/configurable.py +++ b/transactional_emulator/testbench/aten/configurable.py @@ -98,6 +98,10 @@ def from_args( mlen = int(args.mlen) vlen = int(args.vlen if args.vlen is not None else mlen) blen = int(args.blen) + # Sub-64 MLEN is supported: the emulator packs HBM rows tightly and reads + # aligned 64-byte words with byte-granular extraction (transfer_mx_from_hbm). + # The only HBM requirement is a byte-aligned element row, which 8-bit MXFP + # always satisfies. (mlen % blen alignment is checked in setup_hw.) hlen = int(args.hlen if args.hlen is not None else default_hlen or base["HLEN"]) broadcast_amount = int( args.broadcast_amount @@ -135,22 +139,48 @@ def write_toml(self, build_dir: Path) -> Path: with source_path.open() as f: config = tomlkit.load(f) - behavior = config["TRANSACTIONAL"]["CONFIG"] - behavior["MLEN"]["value"] = self.mlen - behavior["VLEN"]["value"] = self.vlen - behavior["BLEN"]["value"] = self.blen - behavior["HLEN"]["value"] = self.hlen - behavior["BROADCAST_AMOUNT"]["value"] = self.broadcast_amount - behavior["HBM_M_Prefetch_Amount"]["value"] = self.hbm_m_prefetch_amount or self.mlen - behavior["HBM_V_Prefetch_Amount"]["value"] = self.hbm_v_prefetch_amount or self.blen - behavior["HBM_V_Writeback_Amount"]["value"] = self.hbm_v_writeback_amount or self.blen - behavior["HBM_WIDTH"]["value"] = max(self.mlen * 8, behavior["HBM_WIDTH"]["value"]) + txn = config["TRANSACTIONAL"]["CONFIG"] + txn["MLEN"]["value"] = self.mlen + txn["VLEN"]["value"] = self.vlen + txn["BLEN"]["value"] = self.blen + txn["HLEN"]["value"] = self.hlen + txn["BROADCAST_AMOUNT"]["value"] = self.broadcast_amount + txn["HBM_M_Prefetch_Amount"]["value"] = self.hbm_m_prefetch_amount or self.mlen + txn["HBM_V_Prefetch_Amount"]["value"] = self.hbm_v_prefetch_amount or self.blen + txn["HBM_V_Writeback_Amount"]["value"] = self.hbm_v_writeback_amount or self.blen + txn["HBM_WIDTH"]["value"] = max(self.mlen * 8, txn["HBM_WIDTH"]["value"]) apply_latency_profile_config( config, dc_en=self.dc_en, latency_profile=self.latency_profile, ) + # PlenaCompiler reads from BEHAVIOR.CONFIG — mirror tile dimensions AND + # the HBM prefetch/writeback amounts there. The compiler's preload + # codegen (load_batch) advances the destination by + # BEHAVIOR.CONFIG.HBM_V_Prefetch_Amount rows per H_PREFETCH_V, while the + # emulator's H_PREFETCH_V writes TRANSACTIONAL.CONFIG.HBM_V_Prefetch_Amount + # rows. If these disagree, the preload writes overlapping windows that + # spill past the tensor into adjacent VRAM (e.g. a freshly-alloc'd output + # region) with NaN over-read data — silently corrupting ops that read + # their output region before writing (e.g. softmax's S += X). + if "BEHAVIOR" not in config: + config["BEHAVIOR"] = {} + if "CONFIG" not in config["BEHAVIOR"]: + config["BEHAVIOR"]["CONFIG"] = {} + beh = config["BEHAVIOR"]["CONFIG"] + for key in ( + "MLEN", + "VLEN", + "BLEN", + "HLEN", + "BROADCAST_AMOUNT", + "HBM_M_Prefetch_Amount", + "HBM_V_Prefetch_Amount", + "HBM_V_Writeback_Amount", + ): + beh[key] = {"value": txn[key]["value"]} + build_dir.mkdir(parents=True, exist_ok=True) out_path = build_dir / "plena_settings.toml" with out_path.open("w") as f: @@ -158,6 +188,80 @@ def write_toml(self, build_dir: Path) -> Path: return out_path +def add_hw_args(parser: argparse.ArgumentParser) -> None: + """Add standard hardware tile-size arguments to an argparse parser.""" + parser.add_argument("--mlen", type=int, default=64) + parser.add_argument("--vlen", type=int, default=None) + parser.add_argument("--blen", type=int, default=4) + parser.add_argument("--hlen", type=int, default=None) + parser.add_argument("--batch-size", type=int, default=None) + parser.add_argument("--seq-len", type=int, default=None) + parser.add_argument("--hidden-size", type=int, default=None) + parser.add_argument("--seed", type=int, default=42) + + +def resolve_rows(args, default_seq): + """Resolve the total token-row count for the unified [batch_size, seq_len, hidden] + interface: rows = batch_size * seq_len. + + `batch_size` defaults to 1 and `seq_len` defaults to `default_seq` (each test's + historical row count), so a no-arg invocation reproduces the previous behavior. + The non-attention ops are per-row independent, so a [batch, seq, hidden] input is + numerically identical to a flat [rows, hidden] one. + + Returns (rows, batch_size, seq_len); raises if rows is not a positive multiple of blen. + """ + batch_size = args.batch_size if args.batch_size is not None else 1 + seq_len = args.seq_len if args.seq_len is not None else default_seq + rows = batch_size * seq_len + if rows <= 0 or rows % args.blen != 0: + raise ValueError( + f"rows = batch_size*seq_len = {batch_size}*{seq_len} = {rows} " + f"must be a positive multiple of BLEN ({args.blen})" + ) + return rows, batch_size, seq_len + + +def setup_hw(args: argparse.Namespace, build_dir: Path) -> HardwareConfig: + """Create a HardwareConfig from parsed args, write per-build TOML, set env var. + + Returns the HardwareConfig for use in test setup. + """ + mlen = args.mlen + vlen = args.vlen if args.vlen is not None else mlen + blen = args.blen + + if mlen % blen != 0: + raise ValueError(f"MLEN ({mlen}) must be divisible by BLEN ({blen})") + if vlen != mlen: + raise ValueError(f"VLEN ({vlen}) must equal MLEN ({mlen}) for ATen tests") + # Sub-64 MLEN is supported: the Python HBM writer packs rows tightly and the + # emulator reads aligned 64-byte words with byte-granular extraction + # (transfer_mx_from_hbm / transfer_mx_to_hbm). 8-bit MXFP rows are always + # byte-aligned, so no MLEN%64 constraint is needed. + + base = read_behavior_config() + hlen = args.hlen if args.hlen is not None else base["HLEN"] + broadcast_amount = mlen // hlen + + hw = HardwareConfig( + mlen=mlen, + vlen=vlen, + blen=blen, + hlen=hlen, + broadcast_amount=broadcast_amount, + dc_en=None, + latency_profile=None, + hbm_m_prefetch_amount=None, + hbm_v_prefetch_amount=None, + hbm_v_writeback_amount=None, + ) + toml_path = hw.write_toml(build_dir) + os.environ["PLENA_SETTINGS_TOML"] = str(toml_path) + os.environ.setdefault("PLENA_TEXT_DUMP_MAX_VALUES", "10000000") + return hw + + class AtenTemplateTestbench: """Base class for configurable ATen template tests.""" diff --git a/transactional_emulator/testbench/aten/embedding_add_test.py b/transactional_emulator/testbench/aten/embedding_add_test.py index b4192a91..f8be135b 100644 --- a/transactional_emulator/testbench/aten/embedding_add_test.py +++ b/transactional_emulator/testbench/aten/embedding_add_test.py @@ -1,5 +1,6 @@ -""" -ATen-style Learned Positional Embedding Add Test +"""ATen-style Learned Positional Embedding Add Test. + + python embedding_add_test.py [--mlen 128] [--blen 16] [--batch-size 8] [--seq-len 4] SigLIP vision encoder step: embeddings = patch_embeds + position_embedding(position_ids) @@ -13,54 +14,63 @@ golden = ops.embedding_add(X, pos_weight) """ +import argparse +import json from pathlib import Path - import torch -import json from compiler.aten.ops.registry import OpRegistry, Backend import compiler.aten.ops as ops from compiler.aten.plena import PlenaCompiler -from verification.create_sim_env import create_sim_env -from compiler.sim_env_utils import create_mem_for_sim -from plena_utils import load_precision_from_toml +from transactional_emulator.testbench.aten.golden import golden_embedding_add from transactional_emulator.testbench.emulator_runner import run_and_assert +from transactional_emulator.testbench.sim_env_utils import create_mem_for_sim +from transactional_emulator.tools.create_sim_env import create_sim_env +from transactional_emulator.testbench.aten.configurable import add_hw_args, resolve_rows, setup_hw if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + add_hw_args(parser) + args = parser.parse_args() + + mlen = args.mlen + blen = args.blen + hidden_size = args.hidden_size or mlen + # Total token rows = batch_size * seq_len (unified [batch, seq, hidden] interface). + # embedding_add is per-row independent, so the rows are flattened to [rows, hidden]. + rows, batch_size, seq_len = resolve_rows(args, default_seq=max(4, mlen // 16)) + + if hidden_size % mlen != 0: + raise ValueError(f"hidden_size ({hidden_size}) must be divisible by MLEN ({mlen})") + + build_dir = Path(__file__).parent / "build" / "embedding_add" + hw = setup_hw(args, build_dir) + print("=" * 80) - print("ATen-style Embedding Add Test (plena.ops.embedding_add)") + print( + f"ATen-style Embedding Add Test (mlen={mlen}, blen={blen}, batch={batch_size}, seq={seq_len}, rows={rows}, hidden={hidden_size})" + ) print("=" * 80) - # ======================================================================== - # Parameters - # ======================================================================== - hidden_size = 128 # vision encoder hidden dim - seq_len = 4 # number of patches (batch dimension) - mlen = 64 - blen = 4 - real_data_ratio = (8 * 8 + 8) / (8 * 8) - - torch.manual_seed(42) + torch.manual_seed(args.seed) # ======================================================================== # Test data: patch embeddings + position embedding table # ======================================================================== - X = torch.randn(seq_len, hidden_size) # patch embeddings - pos_weight = torch.randn(seq_len, hidden_size) # learned position embeddings + X = torch.randn(rows, hidden_size) # patch embeddings + pos_weight = torch.randn(rows, hidden_size) # learned position embeddings print(f"\nInput X: {X.shape}, range [{X.min():.3f}, {X.max():.3f}]") print(f"pos_weight: {pos_weight.shape}, range [{pos_weight.min():.3f}, {pos_weight.max():.3f}]") # ======================================================================== - # CPU golden reference + # Hardware-accurate golden reference # ======================================================================== - print("\n--- CPU Golden Reference ---") - registry = OpRegistry.load() - registry.set_backend(Backend.CPU) - golden_out = ops.embedding_add(X, pos_weight) + print("\n--- Hardware-Accurate Golden Reference ---") + golden_out = golden_embedding_add(X, pos_weight) print(f" golden_out: {golden_out.shape}") print(f" golden_out[0,:4]: {golden_out[0, :4].tolist()}") @@ -68,12 +78,13 @@ # PLENA backend # ======================================================================== print("\n--- PLENA Backend (ISA generation) ---") + registry = OpRegistry.load() registry.set_backend(Backend.PLENA) - prog = PlenaCompiler(mlen=mlen, blen=blen, real_data_ratio=real_data_ratio) + prog = PlenaCompiler(mlen=mlen, blen=blen, real_data_ratio=hw.real_data_ratio) - x_input = prog.input("X", shape=(seq_len, hidden_size)) - pe_input = prog.input("POS", shape=(seq_len, hidden_size)) + x_input = prog.input("X", shape=(rows, hidden_size)) + pe_input = prog.input("POS", shape=(rows, hidden_size)) X_batch = prog.load_batch(x_input, name="X") PE_batch = prog.load_batch(pe_input, name="POS") @@ -87,24 +98,25 @@ # ======================================================================== # Build simulation environment # ======================================================================== - build_dir = Path(__file__).parent / "build" / "embedding_add" - build_dir.mkdir(parents=True, exist_ok=True) - - input_tensor = {"X": X, "POS": pos_weight} + input_tensors = {"X": X, "POS": pos_weight} golden_result = {"original_output": golden_out} - create_sim_env(input_tensor, gen_code, golden_result, [], build_dir=str(build_dir)) + create_sim_env(input_tensors, gen_code, golden_result, [], build_dir=str(build_dir)) + + # Place each tensor at the compiler's actual HBM address. At MLEN>=256 the + # compiler tile-aligns HBM allocations (gaps between tensors); a contiguous + # writer would put POS where the prefetch never reads -> zero/garbage. + hbm_addrs = {name: prog._compiler.get_hbm_layout(name).hbm_base_addr for name in input_tensors} create_mem_for_sim( - precision_settings=load_precision_from_toml( - Path(__file__).resolve().parents[3] / "plena_settings.toml", mode="TRANSACTIONAL" - ), data_size=256, mode="behave_sim", asm="embedding_add_aten", data=None, specified_data_order=["X", "POS"], build_path=build_dir, + input_tensors=input_tensors, + hbm_addrs=hbm_addrs, ) # embedding_add is in-place: result is at same VRAM location as X @@ -112,8 +124,8 @@ comparison_params = { "start_row_idx": x_vram_addr // mlen, - "num_rows": (seq_len * hidden_size) // mlen, - "num_batches": seq_len, + "num_rows": (rows * hidden_size) // mlen, + "num_batches": rows, "elements_per_batch": hidden_size, "row_dim": mlen, "use_stride_mode": hidden_size > mlen, diff --git a/transactional_emulator/testbench/aten/ffn_test.py b/transactional_emulator/testbench/aten/ffn_test.py index 219a9f04..a3429a89 100644 --- a/transactional_emulator/testbench/aten/ffn_test.py +++ b/transactional_emulator/testbench/aten/ffn_test.py @@ -1,5 +1,6 @@ -""" -ATen-style FFN Test +"""ATen-style FFN Test. + + python ffn_test.py [--mlen 128] [--blen 16] [--batch-size 8] Uses the PLENA ATen-style registry: import compiler.aten.ops as ops @@ -11,40 +12,53 @@ FFN formula: w_down @ (silu(w_gate @ x) * (w_up @ x)) """ +import argparse +import json from pathlib import Path - import torch import torch.nn.functional as F -import json from compiler.aten.ops.registry import OpRegistry, Backend import compiler.aten.ops as ops from compiler.aten.plena import PlenaCompiler -from verification.create_sim_env import create_sim_env -from compiler.sim_env_utils import create_mem_for_sim -from plena_utils import load_precision_from_toml from transactional_emulator.testbench.emulator_runner import run_and_assert +from transactional_emulator.testbench.sim_env_utils import create_mem_for_sim from transactional_emulator.testbench.sliced_layer_test_builder import quantize_to_mxfp +from transactional_emulator.tools.create_sim_env import create_sim_env +from transactional_emulator.testbench.aten.configurable import add_hw_args, resolve_rows, setup_hw if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + add_hw_args(parser) + parser.add_argument("--inter-dim", type=int, default=None, help="Intermediate FFN dimension (default: 4*mlen)") + args = parser.parse_args() + + mlen = args.mlen + blen = args.blen + # Total token rows = batch_size * seq_len (unified [batch, seq, hidden] interface). + # FFN is per-row independent, so the rows are flattened to [rows, hidden_size]. + rows, batch_size, seq_len = resolve_rows(args, default_seq=mlen) + hidden_size = args.hidden_size or mlen + inter_dim = args.inter_dim or 2 * mlen + + if hidden_size % mlen != 0: + raise ValueError(f"hidden_size ({hidden_size}) must be divisible by MLEN ({mlen})") + if inter_dim % mlen != 0: + raise ValueError(f"inter_dim ({inter_dim}) must be divisible by MLEN ({mlen})") + + build_dir = Path(__file__).parent / "build" / "ffn" + hw = setup_hw(args, build_dir) + print("=" * 80) - print("ATen-style FFN Test (plena.ops.ffn)") + print( + f"ATen-style FFN Test (mlen={mlen}, blen={blen}, batch={batch_size}, seq={seq_len}, rows={rows}, hidden={hidden_size}, inter={inter_dim})" + ) print("=" * 80) - # ======================================================================== - # Parameters - # ======================================================================== - hidden_size = 128 - inter_dim = 256 - batch_size = 4 - mlen = 64 - blen = 4 - real_data_ratio = (8 * 8 + 8) / (8 * 8) - - torch.manual_seed(42) + torch.manual_seed(args.seed) # ======================================================================== # Test data @@ -53,7 +67,7 @@ scale_in = 1.0 / (hidden_size**0.5) scale_out = 1.0 / (inter_dim**0.5) - X = torch.randn(batch_size, hidden_size) + X = torch.randn(rows, hidden_size) W_gate = torch.randn(hidden_size, inter_dim) * scale_in # (hidden, inter_dim) W_up = torch.randn(hidden_size, inter_dim) * scale_in # (hidden, inter_dim) W_down = torch.randn(inter_dim, hidden_size) * scale_out # (inter_dim, hidden) @@ -78,15 +92,15 @@ W_up_q = quantize_to_mxfp(W_up) W_down_q = quantize_to_mxfp(W_down) - # Stage 1 & 2: up and gate projections → store as BF16 + # Stage 1 & 2: up and gate projections -> store as BF16 # Hardware order: up projection written to gp4 (SiLU input), gate to gp6 up_out = torch.matmul(X_q.float(), W_up_q.float()).to(torch.bfloat16) gate_out = torch.matmul(X_q.float(), W_gate_q.float()).to(torch.bfloat16) - # Stage 3: SiLU(up) * gate → store as BF16 (hardware applies SiLU to up, not gate) + # Stage 3: SiLU(up) * gate -> store as BF16 (hardware applies SiLU to up, not gate) silu_gate = (F.silu(up_out.float()) * gate_out.float()).to(torch.bfloat16) - # Stage 4: down projection → BF16 output + # Stage 4: down projection -> BF16 output golden_out = torch.matmul(silu_gate.float(), W_down_q.float()).to(torch.bfloat16) print(f" golden_out: {golden_out.shape}") @@ -98,12 +112,12 @@ print("\n--- PLENA Backend (ISA generation) ---") registry.set_backend(Backend.PLENA) - prog = PlenaCompiler(mlen=mlen, blen=blen, real_data_ratio=real_data_ratio) + prog = PlenaCompiler(mlen=mlen, blen=blen, real_data_ratio=hw.real_data_ratio) # Declare inputs: - # activation → loaded to VRAM via load_batch - # weights → remain in HBM (accessed block-by-block by ffn_asm) - x_input = prog.input("X", shape=(batch_size, hidden_size)) + # activation -> loaded to VRAM via load_batch + # weights -> remain in HBM (accessed block-by-block by ffn_asm) + x_input = prog.input("X", shape=(rows, hidden_size)) w_gate_input = prog.input("W_gate", shape=(hidden_size, inter_dim)) w_up_input = prog.input("W_up", shape=(hidden_size, inter_dim)) w_down_input = prog.input("W_down", shape=(inter_dim, hidden_size)) @@ -121,10 +135,7 @@ # ======================================================================== # Build simulation environment # ======================================================================== - build_dir = Path(__file__).parent / "build" / "ffn" - build_dir.mkdir(parents=True, exist_ok=True) - - input_tensor = { + input_tensors = { "X": X, "W_gate": W_gate, "W_up": W_up, @@ -135,18 +146,22 @@ # FP SRAM preload: [0]=0.0, [1]=1.0 (legacy), [5]=1.0 (for SiLU sigmoid via ffn_plena slot 5) fp_preload = [0.0, 1.0, 0.0, 0.0, 0.0, 1.0] + [0.0] * 4 - create_sim_env(input_tensor, gen_code, golden_result, fp_preload, build_dir=str(build_dir)) + create_sim_env(input_tensors, gen_code, golden_result, fp_preload, build_dir=str(build_dir)) + + # Place each tensor at the compiler's actual HBM address. At MLEN>=256 the + # compiler tile-aligns HBM allocations (gaps between tensors); a contiguous + # writer would put weights where the prefetch never reads -> zero weights. + hbm_addrs = {name: prog._compiler.get_hbm_layout(name).hbm_base_addr for name in input_tensors} create_mem_for_sim( - precision_settings=load_precision_from_toml( - Path(__file__).resolve().parents[3] / "plena_settings.toml", mode="TRANSACTIONAL" - ), data_size=256, mode="behave_sim", asm="ffn_aten", data=None, specified_data_order=["X", "W_gate", "W_up", "W_down"], build_path=build_dir, + input_tensors=input_tensors, + hbm_addrs=hbm_addrs, ) # FFN result overwrites activation area in VRAM (in-place) @@ -154,8 +169,8 @@ comparison_params = { "start_row_idx": x_vram_addr // mlen, - "num_rows": (batch_size * hidden_size) // mlen, - "num_batches": batch_size, + "num_rows": (rows * hidden_size) // mlen, + "num_batches": rows, "elements_per_batch": hidden_size, "row_dim": mlen, } diff --git a/transactional_emulator/testbench/aten/flash_attention_gqa_test.py b/transactional_emulator/testbench/aten/flash_attention_gqa_test.py index 8db52148..d1f80fd0 100644 --- a/transactional_emulator/testbench/aten/flash_attention_gqa_test.py +++ b/transactional_emulator/testbench/aten/flash_attention_gqa_test.py @@ -1,31 +1,40 @@ -""" -GQA flash attention via the proper ATen dispatch. +"""GQA flash attention via the proper ATen dispatch. + + python flash_attention_gqa_test.py [--mlen 128] [--blen 16] \ + [--batch-size 2] [--seq-len 64] -Uses `ops.flash_attention(prog, Q, K, V, scale, hq=4, hkv=1, h_qkv=16)` — +Uses `ops.flash_attention(prog, Q, K, V, scale, hq=4, hkv=1, h_qkv=16, ...)` -- dispatches through the registry to `flash_attention_plena`, which detects GQA and emits fused codegen using main's `flash_attn_asm` template. -Dims match main's prefill: batch=1, s_q=s_kv=64, hq=4, hkv=1, h_qkv=16. +Dims follow the unified [batch_size, seq_len, heads, head_dim] interface: +Q is [batch_size, seq_len, hq, h_qkv] and K/V are [batch_size, seq_len, hkv, +h_qkv]. The packed GQA lowering supports one sequence tile, so seq_len and +kv_seq_len must each be <= MLEN; multiple batches are emitted as a per-batch +loop. With the defaults (batch_size=1, seq_len=mlen) this reproduces the +previous single-tile prefill test exactly. """ -import math +import argparse import json +import math from pathlib import Path - import torch import torch.nn.functional as F from compiler.aten.ops.registry import OpRegistry, Backend import compiler.aten.ops as ops from compiler.aten.plena import PlenaCompiler -from verification.create_sim_env import create_sim_env -from compiler.sim_env_utils import create_mem_for_sim -from plena_utils import load_precision_from_toml +from transactional_emulator.testbench.aten.golden import quantize_to_mxfp from transactional_emulator.testbench.emulator_runner import run_and_assert +from transactional_emulator.testbench.sim_env_utils import create_mem_for_sim +from transactional_emulator.tools.create_sim_env import create_sim_env +from transactional_emulator.testbench.aten.configurable import add_hw_args, setup_hw def gqa_sdpa(q, k, v, scale, hq, hkv): + # q: [batch, seq, hq, h_qkv]; k/v: [batch, seq, hkv, h_qkv] q_t = q.transpose(1, 2) k_t = k.transpose(1, 2).repeat_interleave(hq // hkv, dim=1) v_t = v.transpose(1, 2).repeat_interleave(hq // hkv, dim=1) @@ -34,71 +43,187 @@ def gqa_sdpa(q, k, v, scale, hq, hkv): if __name__ == "__main__": - print("=" * 80) - print("GQA Flash Attention via ATen dispatch (ops.flash_attention)") - print("=" * 80) + parser = argparse.ArgumentParser(description=__doc__) + add_hw_args(parser) + args = parser.parse_args() + + mlen = args.mlen + blen = args.blen - batch_size = 1 - s_q = 64 - s_kv = 64 + # GQA head counts are fixed (architectural constants, not hardware tile params) hq = 4 hkv = 1 - h_qkv = 16 - hidden_size = hq * h_qkv # 64 = mlen - mlen = 64 - blen = 4 - real_data_ratio = (8 * 8 + 8) / (8 * 8) + h_qkv = mlen // hq # per-head dim: scales with mlen (e.g. 16 for mlen=64, 32 for mlen=128) + + # Unified [batch_size, seq_len, heads, head_dim] interface. + batch_size = args.batch_size if args.batch_size is not None else 1 + s_q = args.seq_len if args.seq_len is not None else mlen + s_kv = s_q + hidden_size = hq * h_qkv # equals mlen + + if mlen % hq != 0: + raise ValueError(f"MLEN ({mlen}) must be divisible by hq ({hq})") + if mlen % blen != 0: + raise ValueError(f"MLEN ({mlen}) must be divisible by BLEN ({blen})") + if batch_size <= 0: + raise ValueError(f"batch_size ({batch_size}) must be positive") + # Packed GQA lowering supports exactly one sequence tile per batch. + if s_q > mlen: + raise ValueError( + f"seq_len ({s_q}) exceeds the one-tile packed-GQA limit MLEN ({mlen}). " + f"Supported range: 1 <= seq_len <= MLEN." + ) + rows = batch_size * s_q + if rows % blen != 0: + raise ValueError(f"rows = batch_size*seq_len = {batch_size}*{s_q} = {rows} must be a multiple of BLEN ({blen})") + + # Per-batch physical rows must be a whole number of MLEN tiles (the GQA loop + # advances Q/K/V/O bases by MLEN-aligned per-batch strides). + rows_per_batch = max(mlen, s_q) + if rows_per_batch % mlen != 0: + rows_per_batch = ((rows_per_batch + mlen - 1) // mlen) * mlen + scale = 1.0 / math.sqrt(h_qkv) - torch.manual_seed(42) + args.hlen = h_qkv # HLEN must equal per-head dim for packed attention + + build_dir = Path(__file__).parent / "build" / "flash_attention_gqa" + hw = setup_hw(args, build_dir) + + print("=" * 80) + print( + f"GQA Flash Attention via ATen dispatch (mlen={mlen}, blen={blen}, " + f"batch={batch_size}, seq={s_q}, hq={hq}, hkv={hkv}, h_qkv={h_qkv})" + ) + print("=" * 80) + + torch.manual_seed(args.seed) + # [batch_size, seq_len, heads, head_dim] q = torch.randn(batch_size, s_q, hq, h_qkv) * 0.5 k = torch.randn(batch_size, s_kv, hkv, h_qkv) * 0.5 v = torch.randn(batch_size, s_kv, hkv, h_qkv) * 0.5 - # Pad KV to mlen-wide for main-template compatibility (hkv=1 → 4 slots, 3 zero) - k_padded = torch.zeros(batch_size, s_kv, mlen // h_qkv, h_qkv) - v_padded = torch.zeros(batch_size, s_kv, mlen // h_qkv, h_qkv) - k_padded[:, :, :hkv, :] = k - v_padded[:, :, :hkv, :] = v + # Pad KV heads to mlen-wide for main-template compatibility (hkv=1 -> 4 slots, + # 3 zero) and pad each batch's sequence rows up to rows_per_batch tiles. + kv_head_slots = mlen // h_qkv + k_padded = torch.zeros(batch_size, rows_per_batch, kv_head_slots, h_qkv) + v_padded = torch.zeros(batch_size, rows_per_batch, kv_head_slots, h_qkv) + k_padded[:, :s_kv, :hkv, :] = k + v_padded[:, :s_kv, :hkv, :] = v - # Golden via SDPA - golden = gqa_sdpa(q.float(), k.float(), v.float(), scale, hq, hkv) + # Hardware-accurate golden: MXFP8-quantize K, V before GQA SDPA. + k_q = quantize_to_mxfp(k) + v_q = quantize_to_mxfp(v) + golden = gqa_sdpa(q.float(), k_q.float(), v_q.float(), scale, hq, hkv) # PLENA program using proper ATen dispatch registry = OpRegistry.load() registry.set_backend(Backend.PLENA) - prog = PlenaCompiler(mlen=mlen, blen=blen, real_data_ratio=real_data_ratio) + prog = PlenaCompiler(mlen=mlen, blen=blen, real_data_ratio=hw.real_data_ratio) + # Q is prestaged at VRAM addr=0 by the test harness (matches main's prefill - # test which also preloads Q to VRAM row 0 via preload_act_asm). - q_input = prog.input("Q", shape=(s_q, hidden_size), prestaged_vram_addr=0) - k_input = prog.input("K", shape=(s_kv, mlen)) # padded to mlen - v_input = prog.input("V", shape=(s_kv, mlen)) + # test which also preloads Q to VRAM row 0 via preload_act_asm). Each batch + # occupies a rows_per_batch-tall physical block; the logical Q is + # [batch_size*seq_len, hidden_size]. + q_phys_rows = batch_size * rows_per_batch + kv_phys_rows = batch_size * rows_per_batch + q_input = prog.input( + "Q", + shape=(rows, hidden_size), + physical_shape=(q_phys_rows, mlen), + prestaged_vram_addr=0, + ) + k_input = prog.input( + "K", + shape=(batch_size * s_kv, mlen), + physical_shape=(kv_phys_rows, mlen), + ) + v_input = prog.input( + "V", + shape=(batch_size * s_kv, mlen), + physical_shape=(kv_phys_rows, mlen), + ) Q_batch = prog.load_batch(q_input, name="Q") # no ISA emitted (prestaged) - # Dispatch through ops.flash_attention with GQA params - O = ops.flash_attention(prog, Q_batch, k_input, v_input, scale, hq=hq, hkv=hkv, h_qkv=h_qkv) + # Dispatch through ops.flash_attention with GQA + batch/seq params. + O = ops.flash_attention( + prog, + Q_batch, + k_input, + v_input, + scale, + hq=hq, + hkv=hkv, + h_qkv=h_qkv, + batch_size=batch_size, + seq_len=s_q, + kv_seq_len=s_kv, + ) gen_code = prog.compile() print(f"\nGenerated {len(gen_code.splitlines())} lines of ISA") - build_dir = Path(__file__).parent / "build" / "flash_attention_gqa" - build_dir.mkdir(parents=True, exist_ok=True) - + # HBM images: K/V occupy batch_size * rows_per_batch physical rows of mlen, + # each batch's seq rows packed at the front of its tile block. input_tensor = { - "Q": q.reshape(1, -1), - "K": k_padded.reshape(1, -1), - "V": v_padded.reshape(1, -1), + "Q": q.reshape(batch_size, s_q, hidden_size).reshape(1, -1), + "K": k_padded.reshape(batch_size * rows_per_batch, mlen).reshape(1, -1), + "V": v_padded.reshape(batch_size * rows_per_batch, mlen).reshape(1, -1), } golden_result = { "input_tensor": input_tensor, - "original_output": golden.reshape(s_q, hidden_size), + "original_output": golden.reshape(rows, hidden_size), + } + + # FP SRAM slot 1 holds the softmax scale that the online-softmax kernel + # multiplies QK^T by. M_BTMM already applies the emulator's fixed bmm_scale + # (0.25), so the kernel must apply scale/0.25 to recover the caller's QK + # scale of `scale`. (For h_qkv=16 this equals 1.0 and the kernel skips the + # multiply, masking the issue; for larger h_qkv the value is actually read.) + softmax_scale = scale / 0.25 + fp_preload = [0.0, softmax_scale, float("-inf")] + [0.0] * 45 + + # Q is prestaged in VRAM at addr=0: provide flat fp16 VRAM image. Each batch + # occupies a rows_per_batch-tall mlen-wide block; the first seq_len rows hold + # the [seq_len, hidden_size] data, remaining rows are zero padding. + q_vram = torch.zeros(batch_size, rows_per_batch, mlen) + q_vram[:, :s_q, :hidden_size] = q.reshape(batch_size, s_q, hidden_size) + q_vram_flat = q_vram.reshape(-1).to(torch.float16) + + # Explicit HBM layouts for the matrix-prefetch writer. The input_tensor dict + # passes K/V/Q flattened to (1, -1), so create_mem_for_sim cannot infer the + # real per-tile row geometry from tensor.shape. Without an explicit layout it + # falls back to whatever stale tensor_layouts.json is left in the build dir + # from a previous (e.g. larger-mlen) run, which lays K/V out at the wrong row + # stride and zero-pads away almost all the data -> K/V load as ~0 into matrix + # SRAM and the attention output collapses to exp(0)=uniform softmax. Each + # K/V/Q tile is mlen-wide; K/V occupy kv_phys_rows physical rows, Q occupies + # q_phys_rows. (Mirrors flash_attention_mha_test.py's tensor_layouts.) + tensor_layouts = { + "Q": { + "physical_shape": [q_phys_rows, mlen], + "source_rows": q_phys_rows, + "storage_rows": q_phys_rows, + "source_row_elements": mlen, + "storage_row_elements": mlen, + }, + "K": { + "physical_shape": [kv_phys_rows, mlen], + "source_rows": kv_phys_rows, + "storage_rows": kv_phys_rows, + "source_row_elements": mlen, + "storage_row_elements": mlen, + }, + "V": { + "physical_shape": [kv_phys_rows, mlen], + "source_rows": kv_phys_rows, + "storage_rows": kv_phys_rows, + "source_row_elements": mlen, + "storage_row_elements": mlen, + }, } - fp_preload = [0.0, scale, float("-inf")] + [0.0] * 45 - # Q is prestaged in VRAM at addr=0: provide flat fp16 VRAM image starting - # with Q's elements (row-major, hidden_size=64 elements per row). - q_vram_flat = q.reshape(-1).to(torch.float16) create_sim_env( input_tensor, gen_code, @@ -106,24 +231,30 @@ def gqa_sdpa(q, k, v, scale, hq, hkv): fp_preload, build_dir=str(build_dir), vram_preload=q_vram_flat, + tensor_layouts=tensor_layouts, ) + + # At MLEN>=256 the compiler tile-aligns HBM allocations, leaving gaps between + # tensors; a contiguous writer would place K/V where the prefetch never reads + # (-> zero K/V tiles). Pin each tensor at its compiler-assigned hbm_base_addr. + hbm_addrs = {name: prog._compiler.get_hbm_layout(name).hbm_base_addr for name in input_tensor} create_mem_for_sim( - precision_settings=load_precision_from_toml( - Path(__file__).resolve().parents[3] / "plena_settings.toml", mode="TRANSACTIONAL" - ), data_size=256, mode="behave_sim", asm="flash_attention_gqa_aten", data=None, specified_data_order=["Q", "K", "V"], build_path=build_dir, + input_tensors=input_tensor, + tensor_layouts=tensor_layouts, + hbm_addrs=hbm_addrs, ) o_vram_addr = prog._compiler.get_vram_addr(O.name) comparison_params = { "start_row_idx": o_vram_addr // mlen, - "num_rows": (s_q * hidden_size) // mlen, - "num_batches": s_q, + "num_rows": (rows * hidden_size) // mlen, + "num_batches": rows, "elements_per_batch": hidden_size, "row_dim": mlen, "use_stride_mode": False, diff --git a/transactional_emulator/testbench/aten/flash_attention_mha_test.py b/transactional_emulator/testbench/aten/flash_attention_mha_test.py index 2a412bc8..9b80654a 100644 --- a/transactional_emulator/testbench/aten/flash_attention_mha_test.py +++ b/transactional_emulator/testbench/aten/flash_attention_mha_test.py @@ -37,8 +37,8 @@ def pad_batch_rows(tensor: torch.Tensor, physical_rows: int, physical_cols: int) def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--batch-size", type=int, default=1) - parser.add_argument("--seq-len", type=int, default=64) - parser.add_argument("--head-dim", type=int, default=64) + parser.add_argument("--seq-len", type=int, default=None, help="default: mlen") + parser.add_argument("--head-dim", type=int, default=None, help="default: mlen") parser.add_argument("--unroll-attention", action="store_true") AtenTemplateTestbench.add_common_args( parser, @@ -47,6 +47,10 @@ def main() -> None: args = parser.parse_args() tb = AtenTemplateTestbench(args, name="flash_attention_mha_aten", default_build_dir=args.build_dir) + if args.seq_len is None: + args.seq_len = tb.hw.mlen + if args.head_dim is None: + args.head_dim = tb.hw.mlen if args.seq_len > tb.hw.mlen: raise ValueError(f"seq_len={args.seq_len} exceeds one-tile MHA test MLEN={tb.hw.mlen}") if args.head_dim > tb.hw.mlen: diff --git a/transactional_emulator/testbench/aten/fpvar_softmax_test.py b/transactional_emulator/testbench/aten/fpvar_softmax_test.py index 7af6570e..c01b30cc 100644 --- a/transactional_emulator/testbench/aten/fpvar_softmax_test.py +++ b/transactional_emulator/testbench/aten/fpvar_softmax_test.py @@ -1,5 +1,6 @@ -""" -ATen-style Online Softmax Test +"""ATen-style Online Softmax Test. + + python fpvar_softmax_test.py [--mlen 128] [--blen 16] This is the ATen-style version of fpvar_softmax_test.py. Instead of manually calling PlenaCompiler methods inline, we use: @@ -15,60 +16,67 @@ golden = ops.softmax(X_tensor, scale=1.0) """ +import argparse +import json +import math from pathlib import Path - import torch -import json -# ATen-style imports from compiler.aten.ops.registry import OpRegistry, Backend import compiler.aten.ops as ops -# Existing infrastructure (unchanged) from compiler.aten.plena import PlenaCompiler -from verification.create_sim_env import create_sim_env -from compiler.sim_env_utils import create_mem_for_sim -from plena_utils import load_precision_from_toml +from transactional_emulator.testbench.aten.golden import golden_softmax from transactional_emulator.testbench.emulator_runner import run_and_assert +from transactional_emulator.testbench.sim_env_utils import create_mem_for_sim +from transactional_emulator.tools.create_sim_env import create_sim_env +from transactional_emulator.testbench.aten.configurable import add_hw_args, resolve_rows, setup_hw if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + add_hw_args(parser) + args = parser.parse_args() + + mlen = args.mlen + blen = args.blen + # softmax operates on a [rows, mlen] attention score matrix; rows = batch_size * seq_len. + # The default (batch_size=1, seq_len=mlen) reproduces the prior [mlen, mlen] score matrix. + rows, batch_size, seq_len = resolve_rows(args, default_seq=mlen) + scale = 1.0 / math.sqrt(mlen) + + build_dir = Path(__file__).parent / "build" / "fpvar_softmax" + hw = setup_hw(args, build_dir) + print("=" * 80) - print("ATen-style Online Softmax Test (plena.ops.softmax)") + print( + f"ATen-style Online Softmax Test (mlen={mlen}, blen={blen}, batch={batch_size}, seq={seq_len}, rows={rows}, scale={scale:.4f})" + ) print("=" * 80) - # ======================================================================== - # Parameters - # ======================================================================== - mlen = 64 - blen = 4 - real_data_ratio = (8 * 8 + 8) / (8 * 8) - scale = 1.0 - - torch.manual_seed(42) + torch.manual_seed(args.seed) # ======================================================================== # Test data # ======================================================================== - X = torch.randn(mlen, mlen) * 0.5 + X = torch.randn(rows, mlen) * 0.5 print(f"\nInput X: {X.shape}, range [{X.min():.3f}, {X.max():.3f}]") # ======================================================================== - # Load ATen-style operator registry + # Hardware-accurate golden reference # ======================================================================== - registry = OpRegistry.load() - print(f"\nLoaded ops: {registry.list_ops()}") + print("\n--- Hardware-Accurate Golden Reference ---") + golden_P = golden_softmax(X, scale) + print(f" golden_P: {golden_P.shape}") + print(f" golden_P[0,:4]: {golden_P[0, :4].tolist()}") + print(f" golden_P[0,:].sum(): {golden_P[0, :].sum():.6f} (should be ~1.0)") # ======================================================================== - # CPU golden reference (via registry, Backend.CPU) + # Load ATen-style operator registry # ======================================================================== - print("\n--- CPU Golden Reference ---") - registry.set_backend(Backend.CPU) - golden_P = ops.softmax(X, scale=scale) - print(f" golden_P: {golden_P.shape}") - print(f" golden_P[0,:4]: {golden_P[0, :4].tolist()}") - print(f" golden_P[0,:].sum(): {golden_P[0, :].sum():.6f} (should be ≈1.0)") + registry = OpRegistry.load() + print(f"\nLoaded ops: {registry.list_ops()}") # ======================================================================== # PLENA backend (via registry, Backend.PLENA) @@ -77,10 +85,10 @@ print("\n--- PLENA Backend (ISA generation) ---") registry.set_backend(Backend.PLENA) - prog = PlenaCompiler(mlen=mlen, blen=blen, real_data_ratio=real_data_ratio) + prog = PlenaCompiler(mlen=mlen, blen=blen, real_data_ratio=hw.real_data_ratio) # Register input tensor - x_input = prog.input("X", shape=(mlen, mlen)) + x_input = prog.input("X", shape=(rows, mlen)) X_batch = prog.load_batch(x_input, name="X") # ATen-style dispatch: softmax_plena() is called with (prog, X_batch, scale) @@ -92,37 +100,36 @@ print(f"\nGenerated {len(lines)} lines of ISA code") # ======================================================================== - # Build simulation environment (same as original test) + # Build simulation environment # ======================================================================== - build_dir = Path(__file__).parent / "build" / "fpvar_softmax" - build_dir.mkdir(parents=True, exist_ok=True) - - input_tensor = {"X": X} + input_tensors = {"X": X} golden_result = {"original_output": golden_P} # FP SRAM preload: [0]=0.0, [1]=scale, [2]=-inf fp_preload = [0.0, scale, float("-inf")] + [0.0] * 7 - create_sim_env(input_tensor, gen_code, golden_result, fp_preload, build_dir=str(build_dir)) + create_sim_env(input_tensors, gen_code, golden_result, fp_preload, build_dir=str(build_dir)) + + # Place each tensor at the compiler's actual HBM address (tile-aligned at MLEN>=256). + hbm_addrs = {name: prog._compiler.get_hbm_layout(name).hbm_base_addr for name in input_tensors} create_mem_for_sim( - precision_settings=load_precision_from_toml( - Path(__file__).resolve().parents[3] / "plena_settings.toml", mode="TRANSACTIONAL" - ), data_size=256, mode="behave_sim", asm="fpvar_softmax_test", data=None, specified_data_order=["X"], build_path=build_dir, + input_tensors=input_tensors, + hbm_addrs=hbm_addrs, ) s_vram_addr = prog._compiler.get_vram_addr(S.name) comparison_params = { "start_row_idx": s_vram_addr // mlen, - "num_rows": (mlen * mlen) // mlen, - "num_batches": mlen, + "num_rows": (rows * mlen) // mlen, + "num_batches": rows, "elements_per_batch": mlen, "row_dim": mlen, "use_stride_mode": True, diff --git a/transactional_emulator/testbench/aten/golden.py b/transactional_emulator/testbench/aten/golden.py new file mode 100644 index 00000000..afeb923c --- /dev/null +++ b/transactional_emulator/testbench/aten/golden.py @@ -0,0 +1,103 @@ +"""Hardware-accurate golden reference functions for ATen testbenches. + +Each function matches the emulator's precision path: + HBM (MXFP8) → VRAM (BF16) → step-by-step quantized compute → VRAM (BF16) + +Re-exports from sliced_layer_test_builder where the functions originate. +""" + +from __future__ import annotations + +import torch + +from transactional_emulator.testbench.sliced_layer_test_builder import ( + _active_precision_settings, + _flash_attn_ref, + _load_to_vector_fp, + _rms_norm_vector_ref, + golden_ffn, + quantize_to_mxfp, + quantize_to_vector_fp, +) + +__all__ = [ + "golden_embedding_add", + "golden_ffn", + "golden_flash_attention", + "golden_layer_norm", + "golden_linear", + "golden_rms_norm", + "golden_rope", + "golden_softmax", + "quantize_to_mxfp", + "quantize_to_vector_fp", +] + + +def golden_linear(X: torch.Tensor, W: torch.Tensor) -> torch.Tensor: + """MXFP8 quantised inputs → BF16 matmul.""" + X_q = quantize_to_mxfp(X) + W_q = quantize_to_mxfp(W) + return torch.matmul(X_q.float(), W_q.float()).to(torch.bfloat16) + + +def golden_rms_norm(X: torch.Tensor, eps: float) -> torch.Tensor: + """Step-by-step BF16 RMS norm matching emulator ISA path.""" + precision = _active_precision_settings() + X_hbm = _load_to_vector_fp(X, precision["HBM_V_ACT_TYPE"], precision) + return _rms_norm_vector_ref(X_hbm, eps, precision) + + +def golden_layer_norm(X: torch.Tensor, eps: float) -> torch.Tensor: + """Step-by-step BF16 layer norm (mean-center + RMS norm).""" + precision = _active_precision_settings() + X_hbm = _load_to_vector_fp(X, precision["HBM_V_ACT_TYPE"], precision) + + def qvfp(t): + return quantize_to_vector_fp(t, precision) + + mean = qvfp(X_hbm.float().mean(dim=-1, keepdim=True)) + centered = qvfp(X_hbm - mean) + rms = qvfp(torch.rsqrt(centered.float().pow(2).mean(-1, keepdim=True) + eps)) + return qvfp(centered * rms) + + +def golden_softmax(X: torch.Tensor, scale: float) -> torch.Tensor: + """Step-by-step BF16 softmax matching emulator ISA path.""" + precision = _active_precision_settings() + + def qvfp(t): + return quantize_to_vector_fp(t, precision) + + scores = qvfp(X.float() * scale) + row_max = qvfp(scores.max(dim=-1, keepdim=True).values) + shifted = qvfp(scores - row_max) + exp_shifted = qvfp(shifted.float().exp()) + row_sum = qvfp(exp_shifted.sum(dim=-1, keepdim=True)) + return qvfp(exp_shifted / row_sum) + + +def golden_rope(Q: torch.Tensor, Q_rot: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor: + """Step-by-step BF16 RoPE: (Q * cos) + (Q_rot * sin), with HBM MXFP8 quantization.""" + precision = _active_precision_settings() + hbm_act = precision["HBM_V_ACT_TYPE"] + + def load_hbm(t): + return _load_to_vector_fp(t, hbm_act, precision) + + def qvfp(t): + return quantize_to_vector_fp(t, precision) + + Q_q, Qr_q, cos_q, sin_q = load_hbm(Q), load_hbm(Q_rot), load_hbm(cos), load_hbm(sin) + return qvfp(qvfp(Q_q * cos_q) + qvfp(Qr_q * sin_q)) + + +def golden_flash_attention(Q: torch.Tensor, K: torch.Tensor, V: torch.Tensor, scale: float) -> torch.Tensor: + """Step-by-step BF16 flash attention matching emulator ISA path.""" + precision = _active_precision_settings() + return _flash_attn_ref(Q, K, V, scale, precision=precision) + + +def golden_embedding_add(X: torch.Tensor, POS: torch.Tensor) -> torch.Tensor: + """BF16 element-wise add (simple, no MXFP quantization needed).""" + return (X.float() + POS.float()).to(torch.bfloat16) diff --git a/transactional_emulator/testbench/aten/layer_norm_test.py b/transactional_emulator/testbench/aten/layer_norm_test.py index 13b5e3e6..23a86c85 100644 --- a/transactional_emulator/testbench/aten/layer_norm_test.py +++ b/transactional_emulator/testbench/aten/layer_norm_test.py @@ -1,5 +1,6 @@ -""" -ATen-style Layer Normalization Test +"""ATen-style Layer Normalization Test. + + python layer_norm_test.py [--mlen 128] [--blen 16] [--batch-size 8] Uses the PLENA ATen-style registry: import compiler.aten.ops as ops @@ -10,60 +11,72 @@ golden = ops.layer_norm(X_tensor) """ +import argparse +import json from pathlib import Path - import torch -import json from compiler.aten.ops.registry import OpRegistry, Backend import compiler.aten.ops as ops from compiler.aten.plena import PlenaCompiler -from verification.create_sim_env import create_sim_env -from compiler.sim_env_utils import create_mem_for_sim -from plena_utils import load_precision_from_toml +from transactional_emulator.testbench.aten.golden import golden_layer_norm from transactional_emulator.testbench.emulator_runner import run_and_assert +from transactional_emulator.testbench.sim_env_utils import create_mem_for_sim +from transactional_emulator.tools.create_sim_env import create_sim_env +from transactional_emulator.testbench.aten.configurable import add_hw_args, resolve_rows, setup_hw if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + add_hw_args(parser) + args = parser.parse_args() + + mlen = args.mlen + blen = args.blen + # Total token rows = batch_size * seq_len (unified [batch, seq, hidden] interface). + # layer_norm is per-row independent, so the rows are flattened to [rows, hidden_size]. + # default_seq=blen preserves the prior no-arg default row count (was max(blen, blen)=blen). + rows, batch_size, seq_len = resolve_rows(args, default_seq=blen) + hidden_size = args.hidden_size or mlen + + if hidden_size % mlen != 0: + raise ValueError(f"hidden_size ({hidden_size}) must be divisible by MLEN ({mlen})") + + build_dir = Path(__file__).parent / "build" / "layer_norm" + hw = setup_hw(args, build_dir) + print("=" * 80) - print("ATen-style Layer Normalization Test (plena.ops.layer_norm)") + print( + f"ATen-style Layer Normalization Test (mlen={mlen}, blen={blen}, batch={batch_size}, seq={seq_len}, rows={rows})" + ) print("=" * 80) - # ======================================================================== - # Parameters - # ======================================================================== - hidden_size = 128 - batch_size = 4 - mlen = 64 - blen = 4 - real_data_ratio = (8 * 8 + 8) / (8 * 8) - - torch.manual_seed(42) + torch.manual_seed(args.seed) # ======================================================================== # Test data # ======================================================================== - X = torch.randn(batch_size, hidden_size) + X = torch.randn(rows, hidden_size) print(f"\nInput X: {X.shape}, range [{X.min():.3f}, {X.max():.3f}]") # ======================================================================== - # Load ATen-style operator registry + # Hardware-accurate golden reference # ======================================================================== - registry = OpRegistry.load() - print(f"\nLoaded ops: {registry.list_ops()}") + eps = 1e-6 + print("\n--- Hardware-Accurate Golden Reference ---") + golden_out = golden_layer_norm(X, eps) + print(f" golden_out: {golden_out.shape}") + print(f" golden_out[0,:4]: {golden_out[0, :4].tolist()}") + print(f" golden_out[0,:].mean(): {golden_out[0, :].mean():.6f} (should be ~0.0)") + print(f" golden_out[0,:].std(): {golden_out[0, :].std():.6f} (should be ~1.0)") # ======================================================================== - # CPU golden reference (via registry, Backend.CPU) + # Load ATen-style operator registry # ======================================================================== - print("\n--- CPU Golden Reference ---") - registry.set_backend(Backend.CPU) - golden_out = ops.layer_norm(X) - print(f" golden_out: {golden_out.shape}") - print(f" golden_out[0,:4]: {golden_out[0, :4].tolist()}") - print(f" golden_out[0,:].mean(): {golden_out[0, :].mean():.6f} (should be ≈0.0)") - print(f" golden_out[0,:].std(): {golden_out[0, :].std():.6f} (should be ≈1.0)") + registry = OpRegistry.load() + print(f"\nLoaded ops: {registry.list_ops()}") # ======================================================================== # PLENA backend (via registry, Backend.PLENA) @@ -71,10 +84,10 @@ print("\n--- PLENA Backend (ISA generation) ---") registry.set_backend(Backend.PLENA) - prog = PlenaCompiler(mlen=mlen, blen=blen, real_data_ratio=real_data_ratio) + prog = PlenaCompiler(mlen=mlen, blen=blen, real_data_ratio=hw.real_data_ratio) # Load activation into VRAM, then apply Layer norm in-place - x_input = prog.input("X", shape=(batch_size, hidden_size)) + x_input = prog.input("X", shape=(rows, hidden_size)) X_batch = prog.load_batch(x_input, name="X") # ATen-style dispatch: layer_norm_plena() is called with (prog, X_batch) @@ -88,27 +101,22 @@ # ======================================================================== # Build simulation environment # ======================================================================== - build_dir = Path(__file__).parent / "build" / "layer_norm" - build_dir.mkdir(parents=True, exist_ok=True) - - input_tensor = {"X": X} + input_tensors = {"X": X} golden_result = {"original_output": golden_out} # FP SRAM preload: [0]=0.0, [1]=eps(1e-6), [2]=1/hidden_size fp_preload = [0.0, 1e-6, 1.0 / hidden_size] + [0.0] * 7 - create_sim_env(input_tensor, gen_code, golden_result, fp_preload, build_dir=str(build_dir)) + create_sim_env(input_tensors, gen_code, golden_result, fp_preload, build_dir=str(build_dir)) create_mem_for_sim( - precision_settings=load_precision_from_toml( - Path(__file__).resolve().parents[3] / "plena_settings.toml", mode="TRANSACTIONAL" - ), data_size=256, mode="behave_sim", asm="layer_norm_aten", data=None, specified_data_order=["X"], build_path=build_dir, + input_tensors=input_tensors, ) # Layer norm is in-place: result is at same VRAM location as input @@ -116,8 +124,8 @@ comparison_params = { "start_row_idx": x_vram_addr // mlen, - "num_rows": (batch_size * hidden_size) // mlen, - "num_batches": batch_size, + "num_rows": (rows * hidden_size) // mlen, + "num_batches": rows, "elements_per_batch": hidden_size, "row_dim": mlen, "use_stride_mode": hidden_size > mlen, diff --git a/transactional_emulator/testbench/aten/linear_test.py b/transactional_emulator/testbench/aten/linear_test.py index 093b4086..52a3f05c 100644 --- a/transactional_emulator/testbench/aten/linear_test.py +++ b/transactional_emulator/testbench/aten/linear_test.py @@ -1,125 +1,105 @@ -""" -ATen-style Linear Projection Test - -Uses the PLENA ATen-style registry: - import compiler.aten.ops as ops - Y = ops.linear(prog, X_batch, w_input) +"""ATen-style Linear Projection Test. -CPU golden reference: - registry.set_backend(Backend.CPU) - golden_Y = ops.linear(X_tensor, W_tensor) +python linear_test.py [--mlen 128] [--blen 16] [--batch-size 8] """ +import argparse +import json from pathlib import Path - import torch -import json from compiler.aten.ops.registry import OpRegistry, Backend import compiler.aten.ops as ops - from compiler.aten.plena import PlenaCompiler -from verification.create_sim_env import create_sim_env -from compiler.sim_env_utils import create_mem_for_sim +from transactional_emulator.testbench.aten.configurable import add_hw_args, resolve_rows, setup_hw +from transactional_emulator.testbench.aten.golden import golden_linear from transactional_emulator.testbench.emulator_runner import run_and_assert -from plena_utils import load_precision_from_toml +from transactional_emulator.testbench.sim_env_utils import create_mem_for_sim +from transactional_emulator.tools.create_sim_env import create_sim_env if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + add_hw_args(parser) + parser.add_argument("--out-features", type=int, default=None) + args = parser.parse_args() + + mlen = args.mlen + blen = args.blen + vlen = args.vlen or mlen + # Total token rows = batch_size * seq_len (unified [batch, seq, hidden] interface). + # linear is per-row independent, so the rows are flattened to [rows, in_features]. + rows, batch_size, seq_len = resolve_rows(args, default_seq=mlen) + in_features = args.hidden_size or mlen + out_features = args.out_features or 2 * mlen + + if in_features % mlen != 0: + raise ValueError(f"in_features ({in_features}) must be divisible by MLEN ({mlen})") + if out_features % mlen != 0: + raise ValueError(f"out_features ({out_features}) must be divisible by MLEN ({mlen})") + + build_dir = Path(__file__).parent / "build" / "linear" + hw = setup_hw(args, build_dir) + print("=" * 80) - print("ATen-style Linear Projection Test (plena.ops.linear)") + print( + f"ATen-style Linear Projection Test (mlen={mlen}, blen={blen}, batch={batch_size}, seq={seq_len}, rows={rows})" + ) print("=" * 80) - # ======================================================================== - # Parameters - # ======================================================================== - in_features = 128 - out_features = 256 - batch_size = 64 # must be multiple of mlen - mlen = 64 - blen = 4 - real_data_ratio = (8 * 8 + 8) / (8 * 8) - - torch.manual_seed(42) - - # ======================================================================== - # Test data - # ======================================================================== - X = torch.randn(batch_size, in_features) + torch.manual_seed(args.seed) + X = torch.randn(rows, in_features) W = torch.randn(in_features, out_features) print(f"\nInput X: {X.shape}, W: {W.shape}") - # ======================================================================== - # Load ATen-style operator registry - # ======================================================================== - registry = OpRegistry.load() - print(f"\nLoaded ops: {registry.list_ops()}") - - # ======================================================================== - # CPU golden reference (via registry, Backend.CPU) - # ======================================================================== - print("\n--- CPU Golden Reference ---") - registry.set_backend(Backend.CPU) - golden_Y = ops.linear(X, W) + print("\n--- Hardware-Accurate Golden Reference (MXFP8 + BF16) ---") + golden_Y = golden_linear(X, W) print(f" golden_Y: {golden_Y.shape}") print(f" golden_Y[0,:4]: {golden_Y[0, :4].tolist()}") - # ======================================================================== - # PLENA backend (via registry, Backend.PLENA) - # ======================================================================== print("\n--- PLENA Backend (ISA generation) ---") + registry = OpRegistry.load() registry.set_backend(Backend.PLENA) - prog = PlenaCompiler(mlen=mlen, blen=blen, real_data_ratio=real_data_ratio) + prog = PlenaCompiler(mlen=mlen, blen=blen, real_data_ratio=hw.real_data_ratio) - # Declare inputs: activation in VRAM (load_batch), weight stays in HBM - x_input = prog.input("X", shape=(batch_size, in_features)) + x_input = prog.input("X", shape=(rows, in_features)) w_input = prog.input("W", shape=(in_features, out_features)) X_batch = prog.load_batch(x_input, name="X") - - # ATen-style dispatch: linear_plena() is called with (prog, X_batch, w_input) Y = ops.linear(prog, X_batch, w_input) - # Compile to ISA gen_code = prog.compile() - lines = gen_code.splitlines() - print(f"\nGenerated {len(lines)} lines of ISA code") - - # ======================================================================== - # Build simulation environment - # ======================================================================== - build_dir = Path(__file__).parent / "build" / "linear" - build_dir.mkdir(parents=True, exist_ok=True) + print(f"\nGenerated {len(gen_code.splitlines())} lines of ISA code") - input_tensor = {"X": X, "W": W} + input_tensors = {"X": X, "W": W} golden_result = {"original_output": golden_Y} - - # FP SRAM preload: [0]=0.0, [1]=eps(1e-6), [2]=1/in_features fp_preload = [0.0, 1e-6, 1.0 / in_features] + [0.0] * 7 - create_sim_env(input_tensor, gen_code, golden_result, fp_preload, build_dir=str(build_dir)) + create_sim_env(input_tensors, gen_code, golden_result, fp_preload, build_dir=str(build_dir)) - # Load precision settings from plena_settings.toml - toml_path = Path(__file__).parent.parent.parent.parent / "plena_settings.toml" - precision_settings = load_precision_from_toml(toml_path, mode="TRANSACTIONAL") + # Place each tensor at the compiler's actual HBM address. At MLEN>=256 the + # compiler tile-aligns HBM allocations (gaps between tensors); a contiguous + # writer would put weights where the prefetch never reads -> zero weights. + hbm_addrs = {name: prog._compiler.get_hbm_layout(name).hbm_base_addr for name in input_tensors} create_mem_for_sim( - precision_settings=precision_settings, data_size=256, mode="behave_sim", asm="linear_aten", data=None, specified_data_order=["X", "W"], build_path=build_dir, + input_tensors=input_tensors, + hbm_addrs=hbm_addrs, ) y_vram_addr = prog._compiler.get_vram_addr(Y.name) comparison_params = { "start_row_idx": y_vram_addr // mlen, - "num_rows": (batch_size * out_features) // mlen, - "num_batches": batch_size, + "num_rows": (rows * out_features) // mlen, + "num_batches": rows, "elements_per_batch": out_features, "row_dim": mlen, } diff --git a/transactional_emulator/testbench/aten/norm_test.py b/transactional_emulator/testbench/aten/norm_test.py index 13657c91..8a2951e8 100644 --- a/transactional_emulator/testbench/aten/norm_test.py +++ b/transactional_emulator/testbench/aten/norm_test.py @@ -1,4 +1,7 @@ -"""Configurable ATen-style normalization test (RMSNorm or LayerNorm).""" +"""Configurable ATen-style normalization test (RMSNorm or LayerNorm). + +python norm_test.py [--norm-type rms|layer] [--mlen 128] [--blen 16] [--batch 8] [--hidden 256] +""" from pathlib import Path import argparse @@ -7,59 +10,66 @@ from compiler.aten.ops.registry import OpRegistry, Backend from compiler.aten.plena import PlenaCompiler -from transactional_emulator.tools.create_sim_env import create_sim_env -from compiler.sim_env_utils import create_mem_for_sim +from transactional_emulator.testbench.aten.golden import golden_rms_norm, golden_layer_norm from transactional_emulator.testbench.emulator_runner import run_and_assert +from transactional_emulator.testbench.sim_env_utils import create_mem_for_sim +from transactional_emulator.tools.create_sim_env import create_sim_env +from transactional_emulator.testbench.aten.configurable import add_hw_args, setup_hw if __name__ == "__main__": - parser = argparse.ArgumentParser() + parser = argparse.ArgumentParser(description=__doc__) + add_hw_args(parser) parser.add_argument("--norm-type", choices=["rms", "layer"], default="rms") - parser.add_argument("--batch", type=int, default=4) - parser.add_argument("--hidden", type=int, default=128) - parser.add_argument("--mlen", type=int, default=64) - parser.add_argument("--blen", type=int, default=4) + parser.add_argument("--batch", type=int, default=None, help="Batch size (default: blen)") + parser.add_argument("--hidden", type=int, default=None, help="Hidden size (default: 2*mlen)") parser.add_argument("--build-dir", type=Path, default=None) args = parser.parse_args() norm_type = args.norm_type - batch, hidden = args.batch, args.hidden - mlen, blen = args.mlen, args.blen - real_data_ratio = (8 * 8 + 8) / (8 * 8) + mlen = args.mlen + blen = args.blen + batch = args.batch or blen + hidden = args.hidden or mlen + + if batch % blen != 0: + raise ValueError(f"batch ({batch}) must be divisible by BLEN ({blen})") + if hidden % mlen != 0: + raise ValueError(f"hidden ({hidden}) must be divisible by MLEN ({mlen})") build_dir = args.build_dir or (Path(__file__).parent / "build" / f"{norm_type}_norm") - build_dir.mkdir(parents=True, exist_ok=True) + hw = setup_hw(args, build_dir) label = "RMSNorm" if norm_type == "rms" else "LayerNorm" - print(f"{'=' * 80}\nATen-style {label} Test\n{'=' * 80}") + print( + f"{'=' * 80}\nATen-style {label} Test (mlen={mlen}, blen={blen}, batch={batch}, hidden={hidden})\n{'=' * 80}" + ) - torch.manual_seed(42) + torch.manual_seed(args.seed) X = torch.randn(batch, hidden) eps = 1e-5 - registry = OpRegistry.load() - registry.set_backend(Backend.CPU) - if norm_type == "rms": - import compiler.aten.ops as ops - - golden = ops.rms_norm(X.clone()) + golden = golden_rms_norm(X.clone(), eps) else: - import compiler.aten.ops as ops - - golden = ops.layer_norm(X.clone()) + golden = golden_layer_norm(X.clone(), eps) print(f" golden: {golden.shape} golden[0,:4]: {golden[0, :4].tolist()}") + import compiler.aten.ops as ops + + registry = OpRegistry.load() registry.set_backend(Backend.PLENA) - prog = PlenaCompiler(mlen=mlen, blen=blen, real_data_ratio=real_data_ratio) + prog = PlenaCompiler(mlen=mlen, blen=blen, real_data_ratio=hw.real_data_ratio) x_input = prog.input("X", shape=(batch, hidden)) x_batch = prog.load_batch(x_input, name="X") if norm_type == "rms": ops.rms_norm(prog, x_batch, eps_offset=0, reci_hid_offset=1) + fp_offsets = (0, 1) else: - ops.layer_norm(prog, x_batch) + ops.layer_norm(prog, x_batch, eps_offset=1, reci_hid_offset=2) + fp_offsets = (1, 2) gen_code = prog.compile() print(f"\nGenerated {len(gen_code.splitlines())} lines of ISA code") @@ -79,12 +89,24 @@ "slice_per_row": hidden, } - input_tensor = {"X": X} + input_tensors = {"X": X} golden_result = {"original_output": golden} - create_sim_env(input_tensor, gen_code, golden_result, fp_preload=None, build_dir=str(build_dir)) + fp_preload = [0.0] * 10 + fp_preload[fp_offsets[0]] = eps + fp_preload[fp_offsets[1]] = 1.0 / hidden + create_sim_env(input_tensors, gen_code, golden_result, fp_preload=fp_preload, build_dir=str(build_dir)) + # Place each tensor at the compiler's actual HBM address (tile-aligned at MLEN>=256). + hbm_addrs = {name: prog._compiler.get_hbm_layout(name).hbm_base_addr for name in input_tensors} create_mem_for_sim( - data_size=256, mode="behave_sim", asm="linear_aten", data=None, specified_data_order=["X"], build_path=build_dir + data_size=256, + mode="behave_sim", + asm="linear_aten", + data=None, + specified_data_order=["X"], + build_path=build_dir, + input_tensors=input_tensors, + hbm_addrs=hbm_addrs, ) with open(build_dir / "comparison_params.json", "w") as f: diff --git a/transactional_emulator/testbench/aten/rms_norm_test.py b/transactional_emulator/testbench/aten/rms_norm_test.py index 456d85ee..fed0d497 100644 --- a/transactional_emulator/testbench/aten/rms_norm_test.py +++ b/transactional_emulator/testbench/aten/rms_norm_test.py @@ -1,5 +1,6 @@ -""" -ATen-style RMS Normalization Test +"""ATen-style RMS Normalization Test. + + python rms_norm_test.py [--mlen 128] [--blen 16] [--batch-size 8] Uses the PLENA ATen-style registry: import compiler.aten.ops as ops @@ -10,58 +11,70 @@ golden = ops.rms_norm(X_tensor) """ +import argparse +import json from pathlib import Path - import torch -import json from compiler.aten.ops.registry import OpRegistry, Backend import compiler.aten.ops as ops from compiler.aten.plena import PlenaCompiler -from verification.create_sim_env import create_sim_env -from compiler.sim_env_utils import create_mem_for_sim -from plena_utils import load_precision_from_toml +from transactional_emulator.testbench.aten.golden import golden_rms_norm from transactional_emulator.testbench.emulator_runner import run_and_assert +from transactional_emulator.testbench.sim_env_utils import create_mem_for_sim +from transactional_emulator.tools.create_sim_env import create_sim_env +from transactional_emulator.testbench.aten.configurable import add_hw_args, resolve_rows, setup_hw if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + add_hw_args(parser) + args = parser.parse_args() + + mlen = args.mlen + blen = args.blen + # Total token rows = batch_size * seq_len (unified [batch, seq, hidden] interface). + # rms_norm is per-row independent, so the rows are flattened to [rows, hidden_size]. + # default_seq=blen preserves the prior default row count (max(batch_size or blen, blen)). + rows, batch_size, seq_len = resolve_rows(args, default_seq=blen) + hidden_size = args.hidden_size or mlen + + if hidden_size % mlen != 0: + raise ValueError(f"hidden_size ({hidden_size}) must be divisible by MLEN ({mlen})") + + build_dir = Path(__file__).parent / "build" / "rms_norm" + hw = setup_hw(args, build_dir) + print("=" * 80) - print("ATen-style RMS Normalization Test (plena.ops.rms_norm)") + print( + f"ATen-style RMS Normalization Test (mlen={mlen}, blen={blen}, batch={batch_size}, seq={seq_len}, rows={rows})" + ) print("=" * 80) - # ======================================================================== - # Parameters - # ======================================================================== - hidden_size = 128 - batch_size = 4 - mlen = 64 - blen = 4 - real_data_ratio = (8 * 8 + 8) / (8 * 8) - - torch.manual_seed(42) + torch.manual_seed(args.seed) # ======================================================================== # Test data # ======================================================================== - X = torch.randn(batch_size, hidden_size) + X = torch.randn(rows, hidden_size) print(f"\nInput X: {X.shape}, range [{X.min():.3f}, {X.max():.3f}]") # ======================================================================== - # Load ATen-style operator registry + # Hardware-accurate golden reference # ======================================================================== - registry = OpRegistry.load() - print(f"\nLoaded ops: {registry.list_ops()}") + eps = 1e-6 + print("\n--- Hardware-Accurate Golden Reference ---") + golden_out = golden_rms_norm(X, eps) + print(f" golden_out: {golden_out.shape}") + print(f" golden_out[0,:4]: {golden_out[0, :4].tolist()}") # ======================================================================== - # CPU golden reference (via registry, Backend.CPU) + # Load ATen-style operator registry # ======================================================================== - print("\n--- CPU Golden Reference ---") - registry.set_backend(Backend.CPU) - golden_out = ops.rms_norm(X) - print(f" golden_out: {golden_out.shape}") - print(f" golden_out[0,:4]: {golden_out[0, :4].tolist()}") + registry = OpRegistry.load() + print(f"\nLoaded ops: {registry.list_ops()}") # ======================================================================== # PLENA backend (via registry, Backend.PLENA) @@ -69,10 +82,10 @@ print("\n--- PLENA Backend (ISA generation) ---") registry.set_backend(Backend.PLENA) - prog = PlenaCompiler(mlen=mlen, blen=blen, real_data_ratio=real_data_ratio) + prog = PlenaCompiler(mlen=mlen, blen=blen, real_data_ratio=hw.real_data_ratio) # Load activation into VRAM, then apply RMS norm in-place - x_input = prog.input("X", shape=(batch_size, hidden_size)) + x_input = prog.input("X", shape=(rows, hidden_size)) X_batch = prog.load_batch(x_input, name="X") # ATen-style dispatch: rms_norm_plena() is called with (prog, X_batch) @@ -86,27 +99,22 @@ # ======================================================================== # Build simulation environment # ======================================================================== - build_dir = Path(__file__).parent / "build" / "rms_norm" - build_dir.mkdir(parents=True, exist_ok=True) - - input_tensor = {"X": X} + input_tensors = {"X": X} golden_result = {"original_output": golden_out} # FP SRAM preload: [0]=0.0, [1]=eps(1e-6), [2]=1/hidden_size fp_preload = [0.0, 1e-6, 1.0 / hidden_size] + [0.0] * 7 - create_sim_env(input_tensor, gen_code, golden_result, fp_preload, build_dir=str(build_dir)) + create_sim_env(input_tensors, gen_code, golden_result, fp_preload, build_dir=str(build_dir)) create_mem_for_sim( - precision_settings=load_precision_from_toml( - Path(__file__).resolve().parents[3] / "plena_settings.toml", mode="TRANSACTIONAL" - ), data_size=256, mode="behave_sim", asm="rms_norm_aten", data=None, specified_data_order=["X"], build_path=build_dir, + input_tensors=input_tensors, ) # RMS norm is in-place: result is at same VRAM location as input @@ -114,8 +122,8 @@ comparison_params = { "start_row_idx": x_vram_addr // mlen, - "num_rows": (batch_size * hidden_size) // mlen, - "num_batches": batch_size, + "num_rows": (rows * hidden_size) // mlen, + "num_batches": rows, "elements_per_batch": hidden_size, "row_dim": mlen, "use_stride_mode": hidden_size > mlen, diff --git a/transactional_emulator/testbench/aten/rope_test.py b/transactional_emulator/testbench/aten/rope_test.py index 047e9b15..b5d5ed87 100644 --- a/transactional_emulator/testbench/aten/rope_test.py +++ b/transactional_emulator/testbench/aten/rope_test.py @@ -1,5 +1,6 @@ -""" -ATen-style Rotary Position Embedding (RoPE) Test +"""ATen-style Rotary Position Embedding (RoPE) Test. + + python rope_test.py [--mlen 128] [--blen 16] [--seq-len 4] [--head-dim 64] SmolLM2 language model 1D PE step: Q_rotated = Q * cos + rotate_half(Q) * sin @@ -16,20 +17,21 @@ before loading to PLENA VRAM. """ +import argparse +import json from pathlib import Path - import torch -import json from compiler.aten.ops.registry import OpRegistry, Backend import compiler.aten.ops as ops from compiler.aten.plena import PlenaCompiler -from verification.create_sim_env import create_sim_env -from compiler.sim_env_utils import create_mem_for_sim -from plena_utils import load_precision_from_toml +from transactional_emulator.testbench.aten.golden import golden_rope from transactional_emulator.testbench.emulator_runner import run_and_assert +from transactional_emulator.testbench.sim_env_utils import create_mem_for_sim +from transactional_emulator.tools.create_sim_env import create_sim_env +from transactional_emulator.testbench.aten.configurable import add_hw_args, resolve_rows, setup_hw def rotate_half(x: torch.Tensor) -> torch.Tensor: @@ -53,27 +55,41 @@ def make_rope_tables(seq_len: int, head_dim: int, theta: float = 10000.0): if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + add_hw_args(parser) + parser.add_argument("--head-dim", type=int, default=None, help="Head dimension (default: mlen, must be <= mlen)") + args = parser.parse_args() + + mlen = args.mlen + blen = args.blen + # Total token rows = batch_size * seq_len (unified [batch, seq, hidden] interface). + # RoPE is per-row (per-position) independent, so the rows are flattened to + # [rows, head_dim]. Default rows == prior seq_len default (4). + rows, batch_size, seq_len = resolve_rows(args, default_seq=4) + head_dim = args.head_dim or mlen # must equal mlen so one VRAM row = one position vector + + if head_dim > mlen: + raise ValueError(f"head_dim ({head_dim}) must be <= MLEN ({mlen})") + if head_dim % 2 != 0: + raise ValueError(f"head_dim ({head_dim}) must be even for RoPE") + + build_dir = Path(__file__).parent / "build" / "rope" + hw = setup_hw(args, build_dir) + print("=" * 80) - print("ATen-style RoPE Test (plena.ops.rope)") + print( + f"ATen-style RoPE Test (mlen={mlen}, blen={blen}, batch={batch_size}, seq={seq_len}, rows={rows}, head_dim={head_dim})" + ) print("=" * 80) - # ======================================================================== - # Parameters - # ======================================================================== - seq_len = 4 - head_dim = 64 # must equal mlen so one VRAM row = one position vector - mlen = 64 - blen = 4 - real_data_ratio = (8 * 8 + 8) / (8 * 8) - - torch.manual_seed(42) + torch.manual_seed(args.seed) # ======================================================================== # Test data # ======================================================================== - Q = torch.randn(seq_len, head_dim) + Q = torch.randn(rows, head_dim) Q_rot = rotate_half(Q) - cos, sin = make_rope_tables(seq_len, head_dim) + cos, sin = make_rope_tables(rows, head_dim) print(f"\nQ: {Q.shape}, range [{Q.min():.3f}, {Q.max():.3f}]") print(f"Q_rot: {Q_rot.shape}") @@ -81,12 +97,10 @@ def make_rope_tables(seq_len: int, head_dim: int, theta: float = 10000.0): print(f"sin: {sin.shape}, range [{sin.min():.3f}, {sin.max():.3f}]") # ======================================================================== - # CPU golden reference + # Hardware-accurate golden reference # ======================================================================== - print("\n--- CPU Golden Reference ---") - registry = OpRegistry.load() - registry.set_backend(Backend.CPU) - golden_out = ops.rope(Q, Q_rot, cos, sin) + print("\n--- Hardware-Accurate Golden Reference ---") + golden_out = golden_rope(Q, Q_rot, cos, sin) print(f" golden_out: {golden_out.shape}") print(f" golden_out[0,:4]: {golden_out[0, :4].tolist()}") @@ -94,15 +108,16 @@ def make_rope_tables(seq_len: int, head_dim: int, theta: float = 10000.0): # PLENA backend # ======================================================================== print("\n--- PLENA Backend (ISA generation) ---") + registry = OpRegistry.load() registry.set_backend(Backend.PLENA) - prog = PlenaCompiler(mlen=mlen, blen=blen, real_data_ratio=real_data_ratio) + prog = PlenaCompiler(mlen=mlen, blen=blen, real_data_ratio=hw.real_data_ratio) # All four tensors loaded from HBM into VRAM - q_input = prog.input("Q", shape=(seq_len, head_dim)) - qrot_input = prog.input("QROT", shape=(seq_len, head_dim)) - cos_input = prog.input("COS", shape=(seq_len, head_dim)) - sin_input = prog.input("SIN", shape=(seq_len, head_dim)) + q_input = prog.input("Q", shape=(rows, head_dim)) + qrot_input = prog.input("QROT", shape=(rows, head_dim)) + cos_input = prog.input("COS", shape=(rows, head_dim)) + sin_input = prog.input("SIN", shape=(rows, head_dim)) Q_var = prog.load_batch(q_input, name="Q") Qrot_var = prog.load_batch(qrot_input, name="QROT") @@ -119,24 +134,47 @@ def make_rope_tables(seq_len: int, head_dim: int, theta: float = 10000.0): # ======================================================================== # Build simulation environment # ======================================================================== - build_dir = Path(__file__).parent / "build" / "rope" - build_dir.mkdir(parents=True, exist_ok=True) - - input_tensor = {"Q": Q, "QROT": Q_rot, "COS": cos, "SIN": sin} + input_tensors = {"Q": Q, "QROT": Q_rot, "COS": cos, "SIN": sin} golden_result = {"original_output": golden_out} - create_sim_env(input_tensor, gen_code, golden_result, [], build_dir=str(build_dir)) + # Match the compiler's compact HBM layout exactly. Without this, a stale + # tensor_layouts.json (or default rounding) can pad tensors so subsequent + # inputs land off their compiler-assigned addresses. + tensor_layouts = { + name: { + "logical_shape": [rows, head_dim], + "physical_shape": [rows, head_dim], + "source_rows": rows, + "storage_rows": rows, + "source_row_elements": head_dim, + "storage_row_elements": head_dim, + } + for name in ("Q", "QROT", "COS", "SIN") + } + # Use the compiler's actual HBM byte offsets so each tensor is written + # exactly where the prefetch ISA expects it (handles MLEN-alignment + # padding the compiler inserts at MLEN>=128 that the HBM writer does not). + hbm_addrs = {name: prog._compiler.get_hbm_layout(name).hbm_base_addr for name in ("Q", "QROT", "COS", "SIN")} + + create_sim_env( + input_tensors, + gen_code, + golden_result, + [], + build_dir=str(build_dir), + tensor_layouts=tensor_layouts, + ) create_mem_for_sim( - precision_settings=load_precision_from_toml( - Path(__file__).resolve().parents[3] / "plena_settings.toml", mode="TRANSACTIONAL" - ), data_size=256, mode="behave_sim", asm="rope_aten", data=None, specified_data_order=["Q", "QROT", "COS", "SIN"], build_path=build_dir, + input_tensors=input_tensors, + tensor_layouts=tensor_layouts, + hbm_addrs=hbm_addrs, ) # RoPE is in-place: result is at Q's VRAM location @@ -144,8 +182,8 @@ def make_rope_tables(seq_len: int, head_dim: int, theta: float = 10000.0): comparison_params = { "start_row_idx": q_vram_addr // mlen, - "num_rows": (seq_len * head_dim) // mlen, - "num_batches": seq_len, + "num_rows": (rows * head_dim) // mlen, + "num_batches": rows, "elements_per_batch": head_dim, "row_dim": mlen, "use_stride_mode": head_dim > mlen, diff --git a/transactional_emulator/testbench/emulator_runner.py b/transactional_emulator/testbench/emulator_runner.py index 0d2a609b..2b907491 100644 --- a/transactional_emulator/testbench/emulator_runner.py +++ b/transactional_emulator/testbench/emulator_runner.py @@ -84,6 +84,12 @@ def run_emulator(build_dir: Path, hbm_size: int | None = None) -> dict: if hbm_size is not None: cmd += ["--hbm-size", str(hbm_size)] + # Per-build settings TOML: pass explicitly so the emulator reads the + # correct config (not the global ../plena_settings.toml). + settings_path = os.environ.get("PLENA_SETTINGS_TOML") + if settings_path: + cmd += ["--settings", settings_path] + # Optional VRAM preload: inject prestaged tensor data before execution. if vram_preload_path.exists(): cmd += ["--vram", str(vram_preload_path)] @@ -117,6 +123,7 @@ def run_emulator(build_dir: Path, hbm_size: int | None = None) -> dict: } sim_latency_re = re.compile(r"Simulation completed\. Latency\s+([0-9.eE+-]+)ns") + topology_re = re.compile(r"mlen=(\d+)\s+vlen=(\d+)\s+.*blen=(\d+)") hbm_stats_re = re.compile( r"HBM Statistics - Bytes read:\s*([0-9]+)\s*\|\s*" r"Bytes written:\s*([0-9]+)\s*\|\s*" @@ -144,6 +151,12 @@ def run_emulator(build_dir: Path, hbm_size: int | None = None) -> dict: metrics["sim_latency_ns"] = sim_latency_ns metrics["sim_latency_ms"] = sim_latency_ns / 1_000_000.0 + topo_match = topology_re.search(line) + if topo_match: + metrics["emu_mlen"] = int(topo_match.group(1)) + metrics["emu_vlen"] = int(topo_match.group(2)) + metrics["emu_blen"] = int(topo_match.group(3)) + hbm_match = hbm_stats_re.search(line) if hbm_match: metrics["hbm_bytes_read"] = int(hbm_match.group(1)) @@ -260,6 +273,7 @@ def compare_emulator_output(build_dir: Path) -> tuple: use_stride_mode=params.get("use_stride_mode", True), use_slice_mode=params.get("use_slice_mode", False), slice_per_row=params.get("slice_per_row", None), + physical_rows=params.get("physical_rows", None), ) return results, params @@ -295,6 +309,19 @@ def run_and_assert(build_dir: Path, op_name: str, mlen: int = 64, blen: int = 4, print("\n--- Running Rust transactional emulator ---") run_metrics = run_emulator(build_dir) + emu_mlen = run_metrics.get("emu_mlen") + emu_blen = run_metrics.get("emu_blen") + if emu_mlen is not None and emu_mlen != mlen: + raise RuntimeError( + f"Config mismatch: emulator ran at MLEN={emu_mlen} but test compiled for MLEN={mlen}. " + f"Check PLENA_SETTINGS_TOML points to the per-build TOML." + ) + if emu_blen is not None and emu_blen != blen: + raise RuntimeError( + f"Config mismatch: emulator ran at BLEN={emu_blen} but test compiled for BLEN={blen}. " + f"Check PLENA_SETTINGS_TOML points to the per-build TOML." + ) + print("\n--- Comparing emulator output vs golden ---") results, params = compare_emulator_output(build_dir) print_comparison_results(results, verbose=True, comparison_params=params) diff --git a/transactional_emulator/testbench/model_configs/smolvlm2_256m.yaml b/transactional_emulator/testbench/model_configs/smolvlm2_256m.yaml index 64188f16..87216b4a 100644 --- a/transactional_emulator/testbench/model_configs/smolvlm2_256m.yaml +++ b/transactional_emulator/testbench/model_configs/smolvlm2_256m.yaml @@ -40,6 +40,36 @@ hardware: mram_tile_capacity: 16 hardware_presets: + native_16x16x4_b1: + mode: native + mlen: 16 + vlen: 16 + blen: 4 + batch_size: 1 + hlen: 16 + broadcast: 1 + mram_tile_capacity: 4 + + native_32x32x8_b1: + mode: native + mlen: 32 + vlen: 32 + blen: 8 + batch_size: 1 + hlen: 32 + broadcast: 1 + mram_tile_capacity: 4 + + native_32x32x4_b1: + mode: native + mlen: 32 + vlen: 32 + blen: 4 + batch_size: 1 + hlen: 32 + broadcast: 1 + mram_tile_capacity: 4 + native_64x64x16_b1: mode: native mlen: 64 diff --git a/transactional_emulator/testbench/sim_env_utils.py b/transactional_emulator/testbench/sim_env_utils.py index 819a55a7..49aa9c8a 100644 --- a/transactional_emulator/testbench/sim_env_utils.py +++ b/transactional_emulator/testbench/sim_env_utils.py @@ -88,7 +88,6 @@ def _map_mx_byte_aligned( if blocks_array.ndim == 1: blocks_array = blocks_array.reshape(-1, 1) bias_array = np.asarray(bias).reshape(-1) - total_bytes_written = 0 with open(output_file, mode) as f: for row_idx in range(logical_rows): @@ -102,7 +101,6 @@ def _map_mx_byte_aligned( raise ValueError(f"Packed element row ({len(row_bytes)} B) > HBM row ({hbm_row_bytes} B)") row_bytes += b"\x00" * (hbm_row_bytes - len(row_bytes)) f.write(row_bytes) - total_bytes_written += len(row_bytes) for row_idx in range(logical_rows): row_start = row_idx * blocks_per_source_row @@ -115,11 +113,18 @@ def _map_mx_byte_aligned( raise ValueError(f"Packed scale row ({len(row_bytes)} B) > scale row ({scale_row_bytes} B)") row_bytes += b"\x00" * (scale_row_bytes - len(row_bytes)) f.write(row_bytes) - total_bytes_written += len(row_bytes) - remainder = total_bytes_written % 64 - if remainder != 0: - f.write(b"\x00" * (64 - remainder)) + # NOTE: no per-tensor pad to a 64-byte boundary. Each tensor is written at + # its compiler-assigned hbm_addr (forward-padded above) and packed at its + # exact element+scale byte length, matching the compiler's tight HBM base + # allocation (advances by hbm_size, not rounded to 64). A per-tensor tail + # pad to 64 would shift the NEXT tensor past its compiler base whenever a + # tensor's size is not a 64-multiple (only happens at sub-64 MLEN, e.g. + # X(16,16) = 288 B -> padded 320 -> next tensor read at the wrong addr -> + # zero weights). The emulator's MemoryBacked capacity (hbm-size, a 64-byte + # multiple sized to ~2x the preload) zero-covers the final aligned-block + # read past the file end. At MLEN>=64 tensor sizes are already 64-multiples, + # so dropping this pad is a no-op. def map_mx_data_to_hbm_for_behave_sim( @@ -157,9 +162,16 @@ def map_mx_data_to_hbm_for_behave_sim( if source_row_elements > logical_row_elements: raise ValueError(f"source_row_elements ({source_row_elements}) > logical_row_elements ({logical_row_elements})") - physical_hbm_row_bytes = (hbm_row_width + 7) // 8 + # Pack each element row tightly at its real byte width. The compiler + # addresses HBM rows at the logical element stride (e.g. MLEN bytes for 8-bit + # elements) and places scales at the tight element-region offset, so the + # writer must match that stride rather than padding rows up to a 64-byte HBM + # burst. The emulator reads aligned 64-byte words and extracts the real bytes + # (transfer_mx_from_hbm), so a tight layout is read correctly. At MLEN>=64 the + # element row is already >=64 bytes, so this equals the previous value + # (max with the 64-byte burst floor was a no-op there). element_row_bits = logical_row_elements * element_width - hbm_row_bytes = max(physical_hbm_row_bytes, (element_row_bits + 7) // 8) + hbm_row_bytes = (element_row_bits + 7) // 8 blocks_per_source_row = (source_row_elements + block_width - 1) // block_width blocks_per_logical_row = (logical_row_elements + block_width - 1) // block_width inferred_source_rows = (len(blocks) + blocks_per_source_row - 1) // blocks_per_source_row @@ -312,8 +324,8 @@ def create_mem_for_sim( hbm_addrs: dict[str, int] | None = None, ): plena_toml_path = os.environ.get("PLENA_SETTINGS_TOML", str(REPO_ROOT / "plena_settings.toml")) - config_settings = load_toml_config(plena_toml_path, "CONFIG") - precision_settings = load_toml_config(plena_toml_path, "PRECISION") + config_settings = load_toml_config(plena_toml_path, "CONFIG", mode="TRANSACTIONAL") + precision_settings = load_toml_config(plena_toml_path, "PRECISION", mode="TRANSACTIONAL") if mode == "behave_sim": target_dir = ( diff --git a/transactional_emulator/tools/create_sim_env.py b/transactional_emulator/tools/create_sim_env.py index 3e2ed638..62132341 100644 --- a/transactional_emulator/tools/create_sim_env.py +++ b/transactional_emulator/tools/create_sim_env.py @@ -139,13 +139,20 @@ def create_sim_env( f.write(json.dumps(summary, indent=2, sort_keys=True)) if vram_preload is not None: - # vram_preload: a flat tensor or numpy array of fp16 values representing - # the initial VRAM contents. Written as raw fp16 bytes so the emulator - # can load it via --vram vram_preload.bin. + # vram_preload: a flat tensor or numpy array representing the initial VRAM + # contents. The emulator's VRAM (VectorSram) stores bfloat16 (1 sign / + # 8 exponent / 7 mantissa) and `load_from_bytes` decodes the raw preload + # bytes using that native VRAM type. The file MUST therefore be encoded + # as bfloat16, not float16 — writing IEEE float16 (5 exp / 10 mantissa) + # bytes here would be reinterpreted as bfloat16 and silently corrupt the + # preloaded values (e.g. a prestaged Q tensor collapsing to ~0). with open(os.path.join(build_dir, "vram_preload.bin"), "wb") as f: - _vram_data = vram_preload.numpy() if hasattr(vram_preload, "numpy") else vram_preload - vram_fp16 = np.array(_vram_data, dtype=np.float16) - f.write(vram_fp16.tobytes()) + if hasattr(vram_preload, "detach"): + _vram_t = vram_preload.detach().cpu().to(torch.bfloat16).contiguous() + else: + _vram_t = torch.as_tensor(np.asarray(vram_preload, dtype=np.float32)).to(torch.bfloat16) + vram_bf16_bits = _vram_t.view(torch.uint16).numpy() + f.write(vram_bf16_bits.tobytes()) def _count_tensor_values(value):