From 5d6e9f8878f7de24e54de4d3f781dd6dfc36a718 Mon Sep 17 00:00:00 2001 From: Aryan Ravishankar Date: Thu, 27 Aug 2026 19:11:37 -0500 Subject: [PATCH 01/10] Freeze the bfp16ebs8 toolchain surface and probe plan (#146) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Everything the pinned v2026.08 toolchain reveals about XDNA2's block-8 format, cited to file and line: 8-value blocks, signed 8-bit mantissas with one shared 8-bit exponent (9 bytes/block, 72-byte v64), native 8x8x8 and 8x8x16 MMUL with FP32 accumulation, conversion in through the crrnd-governed to_v64bfp16ebs8, and no conversion out except MMUL against an identity matrix. That identity construction (diagonal 1.0 = mantissa 0x40, exponent 127) is the load-bearing find: it implies int8 mantissas with 6 fractional bits under a bias-127 scale, which is exactly OCP MXINT8's element contract. If silicon confirms it, the #110 mapping hypothesis reduces to block-size decomposition alone. Also material to #145: the MX-shaped mx4/mx6/mx9 family in this AIE API is block-16 and AIE-MLv2-only. XDNA2 has no native MX type at all, so any released TOSA block-32 contract executes here through decomposition — strengthening ADR-0001's separate-labels rule. Six probes (P0-P5) turn the six extracted hypotheses into on-metal verdicts, each a backend-local precompiled artifact through the released Accelerator lifecycle, none touching TOSA admission or the serving compiler helper. Co-Authored-By: Claude Opus 5 --- .../amdxdna-bfp16ebs8-characterization.md | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 docs/research/amdxdna-bfp16ebs8-characterization.md diff --git a/docs/research/amdxdna-bfp16ebs8-characterization.md b/docs/research/amdxdna-bfp16ebs8-characterization.md new file mode 100644 index 0000000..bbc6b18 --- /dev/null +++ b/docs/research/amdxdna-bfp16ebs8-characterization.md @@ -0,0 +1,164 @@ +# AMD XDNA2 `bfp16ebs8` characterization (issue #146) + +Status: **in progress** — toolchain surface frozen 2026-08-27; silicon probes not yet run. +Part of #110. Companion to +[tosa-int8-block-fp-status.md](tosa-int8-block-fp-status.md) and the forward spec +`docs/research/amdxdna-blockfp-tier.md` (branch `grilling/first-numerical-tier`). + +This note advertises nothing. Per the ticket boundary, no public TOSA `Target`, extension bit, +`TosaCapabilityProvider` row, or stable protocol value is added by this work. + +## Pinned identities + +Every fact below is cited against the pinned toolchain; a different revision re-opens the fact. + +| Component | Identity | +|---|---| +| Toolchain prefix | `~/toolchains/amdxdna-hrx-v2026.08` | +| `mlir_aie` wheel | `1.4.1` (cp312, manylinux_2_35_x86_64) | +| Peano | `llvm-aie==21.0.0.2026080301+c9c5ecb7` | +| Fork `amd-npu-compiler` | commit `c95544269f0c074d6d3e213ee43cc34dc4100801` | +| Reference NPU | PCI `1022:17f0` rev `0x20` (XDNA2, `__AIE_ARCH__ == 21`) | + +Header paths below are relative to +`ironenv/lib/python3.12/site-packages/` inside the prefix; `aie_api` is +`mlir_aie/include/aie_api`, `aie2p` is `llvm-aie/lib/clang/21/include/aie2p`. + +## 1. Frozen from the pinned toolchain + +These are compiler-surface facts, not silicon behavior. They constrain what a kernel can express; +the probes in §3 decide what the hardware actually computes. + +**Format parameters** (`aie_api/aie_doc.hpp:230-237`): `bfp16ebs8` is a block of **8** values, +**8** mantissa bits each, **no sub-tile shifts**, one shared **8-bit** exponent, **9 bytes per +block**. `bfp16ebs16` is the block-16 analogue (17 bytes). A 64-element vector is 8 blocks, +72 bytes; its load/store FIFO width is 576 bits (`aie2p/aie2p_ldst.h:287,559`). + +**Register-level split** (`aie2p/aie2p_srs.h:1308-1313`): `v64bfp16ebs8` is +`{ mantissa: v64char, exponent: v8char }` — mantissas are *signed bytes* at the C level. The +in-memory byte order of the 72-byte unit (exponent plane vs mantissa plane, per-block vs grouped) +is **not** derivable from the headers; probe P0 pins it. + +**XDNA2 supports no MX-native type** (`aie_api/aie_doc.hpp:247-252`): the supported block types on +XDNA2 are `bfp16ebs8` and `bfp16ebs16` at 32/64/128/256 elements. The `mx4`/`mx6`/`mx9` types +(block-16, E8M0 scale, with sub-tile shifts — the OCP-shaped family) exist only on **AIE-MLv2**. +Consequence: on this part, OCP MX block-32 semantics can only be reached through the +block-8 decomposition hypothesis (#110 stage 1). There is no native alternative to characterize +against. + +**Native MMUL surface** (`aie_api/detail/aie2p/mmul_bfp16_bfp16.hpp`): + +| Shape (M×K×N) | Types | Accumulator | Intrinsic | +|---|---|---|---| +| 8×8×8 | `bfp16ebs8 × bfp16ebs8` | `accfloat` (FP32) | `mul_8x8_8x8T` / `mac_8x8_8x8T_conf` | +| 8×8×16 | `bfp16ebs8 × bfp16ebs8` | `accfloat` ×2 | `mul_4x8_8x16T` ×2 (shuffle high half) | +| 8×8×8 | `bfloat16 × bfp16ebs8` | `accfloat` | lhs converted in-core via `to_v64bfp16ebs8` | + +Accumulation for `bfp16ebs8 × bfp16ebs8` is **`accfloat` (FP32)** on XDNA2 +(`aie_api/aie_doc.hpp:1027-1035`). The `T` suffix on every intrinsic indicates a transposed-B +layout; the exact operand layout contract is confirmed by probe P4. + +**Conversion in** (`aie2p/aie2p_srs.h:1308,1331`): `to_v64bfp16ebs8(v64accfloat)` converts an FP32 +accumulator to one 64-element block vector. Rounding is governed by the core's `crrnd` control +register — `to_v64bfp16ebs8_conf(a, rnd)` brackets the conversion with `set_rnd(rnd)`. The mode +vocabulary (`aie2p/aie2p_defines.h:27-47`): `rnd_floor(0)`, `rnd_ceil(1)`, `rnd_sym_floor(2)`, +`rnd_sym_ceil(3)`, `rnd_neg_inf(8)`, `rnd_pos_inf(9)`, `rnd_sym_zero(10)`, `rnd_sym_inf(11)`, +`rnd_conv_even(12)`, `rnd_conv_odd(13)`. The forward spec's assumption that kernels force +round-to-nearest-even corresponds to `rnd_conv_even = 12`; the *default* mode at kernel entry is +a runtime fact for probe P1. + +**No direct conversion out** (`aie_api/detail/aie2p/accum.hpp:745-760`): there is no +bfp16ebs8→float intrinsic. The AIE API converts out by **MMUL against an identity matrix**, and +only on `__AIE_ARCH__ == 21` (with `bfp16ebs16` unsupported, per the `CRVO-9745` FIXME). Any +"storage conversion" tier for this format is therefore matrix-unit math, not an SRS-style cast — +a structural difference from the FP8→BF16 tier. + +**Sparse variants exist** (`v64bfp16ebs8_sparse`, `aie2p/aie2p_aie_api_compat.h:59-62`) — noted +and out of scope for #146. + +## 2. Hypotheses extracted from the toolchain (to confirm or refute on silicon) + +The identity matrix used by the API's own convert-out path +(`aie_api/detail/aie2p/accum.hpp:753-760`) encodes diagonal **1.0** as mantissa byte `0x40` with +exponent byte `127 + shift`. That single construction implies most of the numerical contract: + +- **H1 — element semantics**: value = `m · 2^(e − 127 − 6)` with `m` a two's-complement int8 + mantissa (so `0x40` = 64 = `1.0·2^6` mantissa units at bias-127) and `e` the shared unsigned + 8-bit exponent. Equivalently: int8 mantissa with 6 fractional bits, range (−2, +2), E8-style + bias-127 scale. **If H1 holds, the element semantics coincide with OCP MXINT8's** (int8 element, + 2 integer + 6 fractional bits, shared E8M0 scale) — the mapping hypothesis reduces to block-size + decomposition alone. +- **H2 — memory layout**: undetermined between exponent-plane-then-mantissa-plane, per-block + interleaving, or another order. The register struct keeps them separate; DMA order is unknown. +- **H3 — rounding**: conversion honors the `crrnd` register; default mode at kernel entry + presumed `rnd_conv_even` but unverified. +- **H4 — normalization**: the shared exponent of a converted block is presumed + `max(exponent of members)` aligned so the largest member uses the full mantissa range; + members with smaller exponents lose low bits by the H3 rounding. Unverified. +- **H5 — exceptional values**: E8M0-style exponents have no sign, and the OCP MX spec reserves + `0xFF` as NaN; whether `to_v64bfp16ebs8` saturates, produces `0xFF`, or wraps on FP32 + overflow/Inf/NaN input is unknown. Signed zero and FP32 subnormal handling likewise. +- **H6 — the #110 stage-1 mapping**: one OCP MXINT8 block-32 (one E8M0 scale, 32 int8 elements) + decomposed into four `bfp16ebs8` blocks with *equal* exponent bytes produces bit-identical + dot-product results in FP32 accumulation. Given H1 this is plausible by construction; it must + still be proven under exponent disagreement between sub-blocks (where the four-block form is + *more* precise than block-32, and the mapping direction matters). + +## 3. Probe plan + +Each probe is a backend-local precompiled artifact (`XDNA_PRECOMPILED_FORMAT` via +`virtio_accel_xdna::artifact::encode`), run through the released `Accelerator` lifecycle with +direct binding on the reference NPU — the same harness discipline as `tests/hardware.rs`, kept +entirely out of the TOSA admission path. Probe kernels are compiled by a standalone IRON driver +under `research/` (deliberately separate from `compiler/xdna_compile.py`, which the serving path +owns). + +- **P0 — encoding & layout dump.** Kernel converts a chosen `v64accfloat` pattern with + `to_v64bfp16ebs8` and DMA-writes the raw 72-byte unit to the output buffer. Host decodes under + each candidate layout; distinct FP32 inputs with known exponents/mantissas separate the planes. + Freezes H1 + H2. +- **P1 — rounding.** Tie-case FP32 inputs (`…0.5` mantissa-unit boundaries, odd/even neighbors) + converted under the default mode and under explicit `set_rnd` values; compared to the reference + model per mode. Freezes H3 and the default. +- **P2 — normalization & intra-block loss.** Blocks mixing magnitudes (equal, off-by-one + exponent, extreme spread, all-zero block) to observe shared-exponent selection and low-bit + loss. Freezes H4. +- **P3 — exceptional values.** ±0, FP32 subnormals, ±Inf, NaN, and overflow-magnitude inputs + through conversion; exponent byte `0xFF` and mantissa patterns recorded. Freezes H5. +- **P4 — MMUL contract.** Host-crafted raw blocks (bypassing conversion entirely) through + `mul_8x8_8x8T`; FP32 accumulator out. Establishes operand layout (the `T` transpose), the + multiply semantics `(m_a·2^{e_a−133})·(m_b·2^{e_b−133})` summed in FP32, and accumulation + order effects if any. Includes the mixed `bfloat16 × bfp16ebs8` shape. +- **P5 — MX mapping verdict.** Pinned OCP MX v1.0 MXINT8 reference quantizer (host, Rust) + produces block-32 vectors; decomposition to four equal-exponent block-8 groups runs on the + matrix unit; results compared bit-exactly against the reference dot product in FP32, including + constructed cases where the four sub-block exponents *disagree* (the mapping's directionality + evidence). Decides H6 — and with it, #110 stage 1: exact MX mapping, transform with documented + loss, or distinct vendor-only contract. + +Every probe records the toolchain identity table above plus driver/firmware versions at run time, +and preserves its artifact and raw output vectors under `research/` for repeatability. + +## 4. Reference model + +A host-side Rust model, kept in the standalone probe project (not in any published crate): + +- `bfp16ebs8` codec parameterized by the H1/H2 candidates until P0 pins them, then frozen; +- OCP MX v1.0 MXINT8 quantizer/dequantizer (block-32, E8M0 scale, round-to-nearest-even) as the + independent oracle for P5 — deliberately a *separate formulation* from the codec, following the + two-independent-formulations pattern the RESCALE tier uses; +- exact FP32 dot-product references (`f32` accumulation in kernel order) for P4/P5. + +## 5. Feed into #145 (draft tracking) + +The `mx4/mx6/mx9` discovery is itself #145 evidence: the AIE-API's MX-shaped family is block-16 +with 1-bit sub-tile shifts — not the TOSA 1.1-draft's block-32 — and targets AIE-MLv2, not XDNA2. +Neither AMD surface matches the draft's `BLOCK_SHAPE_32` vocabulary natively. Any released TOSA +MX contract will be executed on this part through a decomposition, which strengthens the case for +keeping the vendor tier and the (future) MX tier separately labeled, as ADR-0001 requires. + +## 6. Out of scope for #146 + +Sparse block vectors; `bfp16ebs16`; performance of any kind; TOSA schema or protocol work +(#110 stages 2+); any advertisement or capability row (#148 prototypes only after this ticket's +verdict). From 3f3bfe8bf2de32a9820169ad3cb5077f52fdb3b6 Mon Sep 17 00:00:00 2001 From: Aryan Ravishankar Date: Thu, 27 Aug 2026 19:31:50 -0500 Subject: [PATCH 02/10] P0 on silicon: bfp16ebs8 encoding, layout, rounding, normalization (#146) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first probe ran on the reference 1022:17f0 NPU through the released Accelerator lifecycle and settled four of the six hypotheses: H1 confirmed: value = m * 2^(e-133) with a two's-complement int8 mantissa and an unsigned shared exponent byte — OCP MXINT8's element contract, differing only in block size. H2 pinned: the native memory layout is the 64-byte mantissa plane then the 8-byte exponent plane; a zero block is e=0, m=0. H3 refuted as assumed: the rounding mode at kernel entry is rnd_floor, not round-to-nearest-even, so an MX-exact kernel must set rnd_conv_even explicitly. H4 sharper than hypothesized: the shared exponent is the max member's IEEE FP32 exponent (the conversion never emits m = -128 and normalizes the max member into |m| in [64, 127]), matching OCP MX v1.0's scale rule on every probed case. The probe pipeline is deliberately standalone under research/bfp16ebs8/ (kernel, IRON driver, runner with its own lockfile, preserved artifact and raw output) so the serving compiler helper stays untouched while the throughput work owns it. Co-Authored-By: Claude Opus 5 --- .../amdxdna-bfp16ebs8-characterization.md | 51 +++- research/bfp16ebs8/.gitignore | 2 + research/bfp16ebs8/artifacts/final.xclbin | Bin 0 -> 9718 bytes research/bfp16ebs8/artifacts/insts.bin | Bin 0 -> 300 bytes research/bfp16ebs8/kernel_p0.cc | 46 ++++ research/bfp16ebs8/probe_compile.py | 126 +++++++++ research/bfp16ebs8/results/p0-2026-08-27.txt | 50 ++++ research/bfp16ebs8/runner/Cargo.lock | 70 +++++ research/bfp16ebs8/runner/Cargo.toml | 13 + research/bfp16ebs8/runner/src/main.rs | 245 ++++++++++++++++++ 10 files changed, 601 insertions(+), 2 deletions(-) create mode 100644 research/bfp16ebs8/.gitignore create mode 100644 research/bfp16ebs8/artifacts/final.xclbin create mode 100644 research/bfp16ebs8/artifacts/insts.bin create mode 100644 research/bfp16ebs8/kernel_p0.cc create mode 100644 research/bfp16ebs8/probe_compile.py create mode 100644 research/bfp16ebs8/results/p0-2026-08-27.txt create mode 100644 research/bfp16ebs8/runner/Cargo.lock create mode 100644 research/bfp16ebs8/runner/Cargo.toml create mode 100644 research/bfp16ebs8/runner/src/main.rs diff --git a/docs/research/amdxdna-bfp16ebs8-characterization.md b/docs/research/amdxdna-bfp16ebs8-characterization.md index bbc6b18..b568049 100644 --- a/docs/research/amdxdna-bfp16ebs8-characterization.md +++ b/docs/research/amdxdna-bfp16ebs8-characterization.md @@ -1,6 +1,7 @@ # AMD XDNA2 `bfp16ebs8` characterization (issue #146) -Status: **in progress** — toolchain surface frozen 2026-08-27; silicon probes not yet run. +Status: **in progress** — toolchain surface frozen 2026-08-27; probe P0 run on silicon +2026-08-27 (results in §6); P1–P5 pending. Part of #110. Companion to [tosa-int8-block-fp-status.md](tosa-int8-block-fp-status.md) and the forward spec `docs/research/amdxdna-blockfp-tier.md` (branch `grilling/first-numerical-tier`). @@ -157,7 +158,53 @@ Neither AMD surface matches the draft's `BLOCK_SHAPE_32` vocabulary natively. An MX contract will be executed on this part through a decomposition, which strengthens the case for keeping the vendor tier and the (future) MX tier separately labeled, as ADR-0001 requires. -## 6. Out of scope for #146 +## 6. P0 results (silicon, 2026-08-27) + +Probe sources: `research/bfp16ebs8/` (kernel `kernel_p0.cc`, IRON driver `probe_compile.py`, +runner `runner/`); raw output `research/bfp16ebs8/results/p0-2026-08-27.txt`; the compiled +artifact is preserved under `research/bfp16ebs8/artifacts/`. Run on the reference `1022:17f0` +rev `0x20` NPU with HRX `hrx-amdxdna-2026.07.30-amdxdna-hal-native` through the released +`Accelerator` lifecycle (direct binding, `XDNA_PRECOMPILED_FORMAT`). + +**H1 — element semantics: CONFIRMED.** For every non-flagged case, +`value = m · 2^(e − 127 − 6)` with `m` a two's-complement signed int8 mantissa and `e` the +unsigned shared exponent byte. Negative values produce negative mantissa bytes (two's +complement, not sign-magnitude): `−1.0 → m = −32` at `e = 128`, `−0.5 → m = −16`. Powers of +two encode as `m = 64` with `e = 127 + log2(v)` when they are the block maximum +(case A: `0.125..16 → e = 124..131`, all `m = 64`). **This is OCP MXINT8's element contract** +(int8, 2 integer + 6 fraction bits, E8-biased scale), differing only in block size. + +**H2 — layout: PINNED.** The native in-memory form of `v64bfp16ebs8` is the 64-byte mantissa +plane followed by the 8-byte exponent plane (element order within blocks, block order across +the vector; confirmed by comparing an explicit plane dump against the struct stored as-is). +An all-zero block encodes as `e = 0, m = 0`. + +**H3 — rounding: REFUTED as assumed.** `get_rnd()` at kernel entry is **0 = `rnd_floor`** +(round toward −∞), not round-to-nearest-even. Observed: `127/64` at forced `e = 128` has exact +mantissa 63.5 and encodes as `m = 63` (floor), `1/128` at `e = 127` (exact 0.5) encodes as +`m = 0`, and `1.5/64` (exact 1.5) as `m = 1`. Consequence for #110/#148: an MX-exact kernel +must explicitly `set_rnd(rnd_conv_even)` (12) around every conversion — the forward spec's +"kernels force round-to-nearest-even" is a requirement on our kernels, not a hardware default. +Probe P1 sweeps the modes and the negative-tie cases to pin each mode's exact function. + +**H4 — normalization: characterized, sharper than hypothesized.** The shared exponent is the +maximum member's *IEEE FP32 exponent* — not the smallest exponent that fits the int8 range. +Case B (max member `−2.0`) chose `e = 128` with `m = −64`, although `e = 127` with `m = −128` +is representable and would have kept a full extra bit for every other member (it would have +made `127/64` exact). The conversion therefore normalizes the largest-magnitude member into +`|m| ∈ [64, 127]` (exactly 64 for powers of two) and never emits `m = −128`. Mixed-magnitude +members are quantized at that exponent and lose low bits under the active rounding mode +(case C: at `e = 127`, `1/128 → m = 0`, `1.5/64 → m = 1`, while `1 ± 1/64 → m = ±65` exactly). +This matches OCP MX v1.0's scale rule (`X = 2^(floor(log2(max|v|)) − emax_elem)` with +`emax_elem = 0` for INT8's 2.0 ceiling — same outcome as "max member's IEEE exponent" for all +P0 cases); P2 adds the boundary cases where the two formulations could differ. + +**Bonus finding.** The conversion path `vector → accum → +to_v64bfp16ebs8` compiles and runs on the first attempt through the standalone probe pipeline, +which validates the P4/P5 plan of crafting raw planes host-side: the layout is now known well +enough to construct arbitrary mantissa/exponent combinations without the converter. + +## 7. Out of scope for #146 Sparse block vectors; `bfp16ebs16`; performance of any kind; TOSA schema or protocol work (#110 stages 2+); any advertisement or capability row (#148 prototypes only after this ticket's diff --git a/research/bfp16ebs8/.gitignore b/research/bfp16ebs8/.gitignore new file mode 100644 index 0000000..90af22a --- /dev/null +++ b/research/bfp16ebs8/.gitignore @@ -0,0 +1,2 @@ +out/ +runner/target/ diff --git a/research/bfp16ebs8/artifacts/final.xclbin b/research/bfp16ebs8/artifacts/final.xclbin new file mode 100644 index 0000000000000000000000000000000000000000..b1f48894f8dc31f84449ee71832810a33f9816c3 GIT binary patch literal 9718 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 literal 0 HcmV?d00001 diff --git a/research/bfp16ebs8/artifacts/insts.bin b/research/bfp16ebs8/artifacts/insts.bin new file mode 100644 index 0000000000000000000000000000000000000000..fd21924cd23011ee66bcdb248ac7209383e78140 GIT binary patch literal 300 zcmZQzWMN}uWMJT6U|`Sz(m(_O7Z@1~fGh_n7l}FmWHUkeOpQPWNF6$00qVyGL@puZ zC4dTHJO!w|s+gJ&0EGzJs{*wbW**EKW*7rzFF#N~o`FFICJv-opmIRH4Ino)0I>iN Ig8?v*0HQz(q5uE@ literal 0 HcmV?d00001 diff --git a/research/bfp16ebs8/kernel_p0.cc b/research/bfp16ebs8/kernel_p0.cc new file mode 100644 index 0000000..afed37f --- /dev/null +++ b/research/bfp16ebs8/kernel_p0.cc @@ -0,0 +1,46 @@ +// P0 encoding/layout probe (issue #146). +// +// Converts 64 caller-chosen FP32 values to one v64bfp16ebs8 with the core's +// to_v64bfp16ebs8 conversion, then writes three things the host can decode: +// +// bytes [0..64) the mantissa plane, one signed byte per element, element order +// bytes [64..72) the exponent plane, one byte per 8-element block, block order +// bytes [72..144) the native v64bfp16ebs8 struct stored to memory as-is +// (the compiler/DMA-facing byte layout, which the register +// plane split above does not reveal) +// bytes [144..148) the core's rounding-mode register at kernel entry (LE u32) +// +// The plane dump freezes the element encoding (H1); the struct store freezes the +// in-memory layout (H2); the mode word records the default rounding (H3 input). +#include + +#include + +extern "C" void probe_p0(const float *__restrict input, uint32_t *__restrict output) { + uint8_t *bytes = reinterpret_cast(output); + + aie::vector values = aie::load_v<64>(input); + aie::accum acc; + acc.from_vector(values); + v64bfp16ebs8 encoded = ::to_v64bfp16ebs8(acc); + + // Register-level planes. Spilling through the stack is fine in a probe. + v64int8 mantissa = encoded.mantissa; + const uint8_t *mantissa_bytes = reinterpret_cast(&mantissa); + for (unsigned i = 0; i < 64; ++i) { + bytes[i] = mantissa_bytes[i]; + } + v8int8 exponent = encoded.exponent; + const uint8_t *exponent_bytes = reinterpret_cast(&exponent); + for (unsigned i = 0; i < 8; ++i) { + bytes[64 + i] = exponent_bytes[i]; + } + + // Native in-memory layout of the block-vector type itself. + const uint8_t *raw = reinterpret_cast(&encoded); + for (unsigned i = 0; i < sizeof(v64bfp16ebs8); ++i) { + bytes[72 + i] = raw[i]; + } + + output[36] = static_cast(::get_rnd()); +} diff --git a/research/bfp16ebs8/probe_compile.py b/research/bfp16ebs8/probe_compile.py new file mode 100644 index 0000000..c3358e3 --- /dev/null +++ b/research/bfp16ebs8/probe_compile.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +"""Standalone IRON driver for the bfp16ebs8 characterization probes (issue #146). + +Deliberately separate from crates/virtio-accel-xdna/compiler/xdna_compile.py: that helper is +the serving path's compiler; these probes are backend-local research artifacts. The toolchain +env setup mirrors the helper's `_configure_toolchain_env` against the same pinned prefix. + +Usage: + VIRTIO_ACCEL_AMDXDNA_TOOLCHAIN=~/toolchains/amdxdna-hrx-v2026.08 \ + python3 probe_compile.py p0 + +Writes /final.xclbin and /insts.bin. +""" + +import os +import sys +from pathlib import Path + +PROBE_DIR = Path(__file__).resolve().parent + +P0_INPUT_FLOATS = 64 +P0_OUTPUT_WORDS = 37 # 64 mantissa + 8 exponent + 72 native-struct bytes + 1 rnd word + + +def configure_toolchain_env() -> None: + import sysconfig + + site_packages = Path(sysconfig.get_paths()["purelib"]) + mlir_dir = site_packages / "mlir_aie" + peano = site_packages / "llvm-aie" + + aie_python = mlir_dir / "python" + sys.path.insert(0, str(aie_python)) + + os.environ["MLIR_AIE_INSTALL_DIR"] = str(mlir_dir) + if peano.is_dir(): + os.environ["PEANO_INSTALL_DIR"] = str(peano) + os.environ["NPU_RUNTIME"] = "hrx" + os.environ["NPU2"] = "1" + + prefix = os.environ.get("VIRTIO_ACCEL_AMDXDNA_TOOLCHAIN") + if not prefix: + raise SystemExit("VIRTIO_ACCEL_AMDXDNA_TOOLCHAIN is not set") + xclbinutil = Path(prefix) / "amd-npu-compiler/bld-xclbinutil/tools/hrx-xclbinutil" + if xclbinutil.is_file(): + os.environ["AIE_XCLBINUTIL"] = str(xclbinutil) + hrx_root = Path(prefix) / "amd-npu-compiler/third_party/.hrx-release" + if hrx_root.is_dir(): + for candidate in sorted(hrx_root.iterdir()): + if (candidate / "lib/libhrx.so").is_file(): + os.environ["HRX_DIR"] = str(candidate) + break + + def prepend(var: str, value: str) -> None: + current = os.environ.get(var, "") + os.environ[var] = f"{value}{':' + current if current else ''}" + + prepend("PYTHONPATH", str(aie_python)) + prepend("PATH", f"{mlir_dir / 'bin'}:{peano / 'bin'}") + prepend("LD_LIBRARY_PATH", str(mlir_dir / "lib")) + + +def build_p0(): + """One worker: 64 FP32 in, one raw 148-byte encoding dump out.""" + import aie.iron as iron + import numpy as np + from aie.iron import In, ObjectFifo, Out, Program, Runtime, Worker + from aie.iron.kernel import ExternalFunction + + kernel_source = (PROBE_DIR / "kernel_p0.cc").read_text() + + @iron.jit + def probe_p0(x_in: In, y_out: Out): + input_ty = np.ndarray[(P0_INPUT_FLOATS,), np.dtype[np.float32]] + output_ty = np.ndarray[(P0_OUTPUT_WORDS,), np.dtype[np.uint32]] + kernel = ExternalFunction( + "probe_p0", + source_string=kernel_source, + arg_types=[input_ty, output_ty], + ) + of_in = ObjectFifo(input_ty, name="p0_in") + of_out = ObjectFifo(output_ty, name="p0_out") + + def core_fn(in_fifo, out_fifo, probe): + source = in_fifo.acquire(1) + destination = out_fifo.acquire(1) + probe(source, destination) + in_fifo.release(1) + out_fifo.release(1) + + worker = Worker(core_fn, [of_in.cons(), of_out.prod(), kernel]) + + def sequence(source, destination, in_prod, out_cons): + in_prod.fill(source) + out_cons.drain(destination, wait=True) + + rt = Runtime(sequence, [input_ty, output_ty, of_in.prod(), of_out.cons()]) + return Program(iron.get_current_device(), rt, workers=[worker]).resolve_program() + + return probe_p0 + + +def main() -> int: + if len(sys.argv) != 3 or sys.argv[1] not in ("p0",): + print(__doc__, file=sys.stderr) + return 2 + out_dir = Path(sys.argv[2]) + out_dir.mkdir(parents=True, exist_ok=True) + + configure_toolchain_env() + import aie.iron as iron + from aie.iron.device import from_name + + iron.set_current_device(from_name("npu2")) + + design = build_p0() + design.compile( + xclbin_path=str(out_dir / "final.xclbin"), + inst_path=str(out_dir / "insts.bin"), + ) + print(f"wrote {out_dir}/final.xclbin and {out_dir}/insts.bin") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/research/bfp16ebs8/results/p0-2026-08-27.txt b/research/bfp16ebs8/results/p0-2026-08-27.txt new file mode 100644 index 0000000..f593efe --- /dev/null +++ b/research/bfp16ebs8/results/p0-2026-08-27.txt @@ -0,0 +1,50 @@ +P0 run 2026-08-28T00:21:39Z +host: 7.1.8-200.fc44.x86_64, NPU 1022:17f0 rev 0x20 +toolchain: amdxdna-hrx-v2026.08 (mlir-aie 1.4.1, llvm-aie 21.0.0.2026080301+c9c5ecb7) +HRX: hrx-amdxdna-2026.07.30-amdxdna-hal-native-rel-eb0b39f-linux-x86_64 + +== case A (rnd mode at entry: 0) + exponent plane: [7c, 7d, 7e, 7f, 80, 81, 82, 83] + block 0 (e=124): 0.125->m=64 0.125->m=64 0.125->m=64 0.125->m=64 0.125->m=64 0.125->m=64 0.125->m=64 0.125->m=64 + block 1 (e=125): 0.25->m=64 0.25->m=64 0.25->m=64 0.25->m=64 0.25->m=64 0.25->m=64 0.25->m=64 0.25->m=64 + block 2 (e=126): 0.5->m=64 0.5->m=64 0.5->m=64 0.5->m=64 0.5->m=64 0.5->m=64 0.5->m=64 0.5->m=64 + block 3 (e=127): 1->m=64 1->m=64 1->m=64 1->m=64 1->m=64 1->m=64 1->m=64 1->m=64 + block 4 (e=128): 2->m=64 2->m=64 2->m=64 2->m=64 2->m=64 2->m=64 2->m=64 2->m=64 + block 5 (e=129): 4->m=64 4->m=64 4->m=64 4->m=64 4->m=64 4->m=64 4->m=64 4->m=64 + block 6 (e=130): 8->m=64 8->m=64 8->m=64 8->m=64 8->m=64 8->m=64 8->m=64 8->m=64 + block 7 (e=131): 16->m=64 16->m=64 16->m=64 16->m=64 16->m=64 16->m=64 16->m=64 16->m=64 + native struct layout: mantissa-then-exponent=true exponent-then-mantissa=false +== case B (rnd mode at entry: 0) + exponent plane: [80, 00, 00, 00, 00, 00, 00, 00] + block 0 (e=128): 1->m=32 -1->m=-32 0.5->m=16 -0.5->m=-16 1.5->m=48 -1.5->m=-48 1.984375->m=63! -2->m=-64 + block 1 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 2 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 3 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + native struct layout: mantissa-then-exponent=true exponent-then-mantissa=false +== case C (rnd mode at entry: 0) + exponent plane: [7f, 00, 00, 00, 00, 00, 00, 00] + block 0 (e=127): 1->m=64 0.015625->m=1 0.0078125->m=0! 0.0234375->m=1! -0.015625->m=-1 0->m=0 1.015625->m=65 -1.015625->m=-65 + block 1 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 2 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 3 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + native struct layout: mantissa-then-exponent=true exponent-then-mantissa=false +== case D (rnd mode at entry: 0) + exponent plane: [00, 00, 00, 00, 00, 00, 00, 00] + block 0 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 1 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 2 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 3 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + native struct layout: mantissa-then-exponent=true exponent-then-mantissa=true +P0 complete diff --git a/research/bfp16ebs8/runner/Cargo.lock b/research/bfp16ebs8/runner/Cargo.lock new file mode 100644 index 0000000..10a4488 --- /dev/null +++ b/research/bfp16ebs8/runner/Cargo.lock @@ -0,0 +1,70 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bfp16ebs8-probe-runner" +version = "0.0.0" +dependencies = [ + "virtio-accel-core", + "virtio-accel-xdna", +] + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "flatbuffers" +version = "25.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1045398c1bfd89168b5fd3f1fc11f6e70b34f6f66300c87d44d3de849463abf1" +dependencies = [ + "bitflags", + "rustc_version", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "virtio-accel-core" +version = "0.3.4" +dependencies = [ + "bitflags", + "virtio-accel-transport", +] + +[[package]] +name = "virtio-accel-tosa" +version = "0.3.4" +dependencies = [ + "flatbuffers", + "virtio-accel-core", +] + +[[package]] +name = "virtio-accel-transport" +version = "0.3.4" + +[[package]] +name = "virtio-accel-xdna" +version = "0.3.4" +dependencies = [ + "virtio-accel-core", + "virtio-accel-tosa", +] diff --git a/research/bfp16ebs8/runner/Cargo.toml b/research/bfp16ebs8/runner/Cargo.toml new file mode 100644 index 0000000..c8d09d7 --- /dev/null +++ b/research/bfp16ebs8/runner/Cargo.toml @@ -0,0 +1,13 @@ +# Standalone probe runner (issue #146) — deliberately NOT a workspace member: nothing here is +# published, and the parent workspace's release policy must not see it. +[package] +name = "bfp16ebs8-probe-runner" +version = "0.0.0" +edition = "2024" +publish = false + +[dependencies] +virtio-accel-core = { path = "../../../crates/virtio-accel-core" } +virtio-accel-xdna = { path = "../../../crates/virtio-accel-xdna" } + +[workspace] diff --git a/research/bfp16ebs8/runner/src/main.rs b/research/bfp16ebs8/runner/src/main.rs new file mode 100644 index 0000000..dc0be0e --- /dev/null +++ b/research/bfp16ebs8/runner/src/main.rs @@ -0,0 +1,245 @@ +//! P0 probe runner (issue #146): drive the encoding-dump kernel through the released +//! `Accelerator` lifecycle and decode the raw `bfp16ebs8` planes against hypothesis H1. +//! +//! Usage: `bfp16ebs8-probe-runner ` + +use std::time::{Duration, Instant}; + +use virtio_accel_core::{ + AccessMode, Accelerator, ArtifactRef, BackendError, BindingRef, BufferDesc, BufferRange, + BufferUsage, ByteSink, ByteSource, ContextDesc, EventState, MemoryDomain, QueueDesc, + TargetIdentity, Timeout, +}; +use virtio_accel_xdna::{XDNA_PRECOMPILED_FORMAT, XdnaAccelerator, artifact}; + +#[derive(Debug)] +struct Slice<'a>(&'a [u8]); + +impl ByteSource for Slice<'_> { + fn len(&self) -> u64 { + self.0.len() as u64 + } + + fn read_at(&self, offset: u64, dst: &mut [u8]) -> Result<(), BackendError> { + let start = usize::try_from(offset).map_err(|_| BackendError::OutOfBounds)?; + let end = start + .checked_add(dst.len()) + .filter(|end| *end <= self.0.len()) + .ok_or(BackendError::OutOfBounds)?; + dst.copy_from_slice(&self.0[start..end]); + Ok(()) + } +} + +#[derive(Debug)] +struct SliceMut<'a>(&'a mut [u8]); + +impl ByteSink for SliceMut<'_> { + fn len(&self) -> u64 { + self.0.len() as u64 + } + + fn write_at(&mut self, offset: u64, src: &[u8]) -> Result<(), BackendError> { + let start = usize::try_from(offset).map_err(|_| BackendError::OutOfBounds)?; + let end = start + .checked_add(src.len()) + .filter(|end| *end <= self.0.len()) + .ok_or(BackendError::OutOfBounds)?; + self.0[start..end].copy_from_slice(src); + Ok(()) + } +} + +const INPUT_BYTES: u64 = 64 * 4; +const OUTPUT_BYTES: u64 = 148; + +/// H1: value = mantissa * 2^(exponent - 127 - 6), two's-complement int8 mantissa. +fn h1_decode(mantissa: i8, exponent: u8) -> f64 { + f64::from(mantissa) * (f64::from(exponent) - 133.0).exp2() +} + +fn run_case( + backend: &A, + queue: &A::Queue, + program: &A::Program, + input: &mut A::Buffer, + output: &A::Buffer, + name: &str, + values: &[f32; 64], +) { + let mut payload = Vec::with_capacity(INPUT_BYTES as usize); + for v in values { + payload.extend_from_slice(&v.to_le_bytes()); + } + backend + .write_buffer(input, 0, &Slice(&payload)) + .expect("write input"); + + let bindings = [ + BindingRef { + slot: 0, + buffer: input, + range: BufferRange::new(0, INPUT_BYTES).unwrap(), + access: AccessMode::Read, + }, + BindingRef { + slot: 1, + buffer: output, + range: BufferRange::new(0, OUTPUT_BYTES).unwrap(), + access: AccessMode::Write, + }, + ]; + let event = match backend.submit(queue, program, &bindings, Timeout::Infinite) { + Ok(event) => event, + Err(virtio_accel_core::SubmitFailure::Rejected(error)) => { + panic!("submit rejected: {error:?}") + } + Err(_) => panic!("submit indeterminate"), + }; + let deadline = Instant::now() + Duration::from_secs(30); + loop { + match backend.poll_event(&event).expect("poll") { + EventState::Pending if Instant::now() < deadline => std::thread::yield_now(), + EventState::Pending => panic!("probe did not complete in 30s"), + EventState::Failed(error) => panic!("probe dispatch failed: {error:?}"), + _ => break, + } + } + if backend.destroy_event(event).is_err() { + panic!("destroy event failed"); + } + + let mut raw = vec![0u8; OUTPUT_BYTES as usize]; + backend + .read_buffer(output, 0, &mut SliceMut(&mut raw)) + .expect("read output"); + + let mantissa = &raw[0..64]; + let exponent = &raw[64..72]; + let native = &raw[72..144]; + let rnd = u32::from_le_bytes(raw[144..148].try_into().unwrap()); + + println!("== case {name} (rnd mode at entry: {rnd})"); + println!(" exponent plane: {exponent:02x?}"); + for block in 0..8 { + let e = exponent[block]; + print!(" block {block} (e={e:3}): "); + for lane in 0..8 { + let idx = block * 8 + lane; + let m = mantissa[idx] as i8; + let input_value = values[idx]; + let decoded = h1_decode(m, e); + let mark = if (decoded - f64::from(input_value)).abs() < 1e-9 { + ' ' + } else { + '!' + }; + print!("{input_value}->m={m}{mark} "); + } + println!(); + } + // Native-layout comparison: where do the register planes land in the stored struct? + let plane_order = native[0..64] == raw[0..64] && native[64..72] == raw[64..72]; + let exp_first = native[0..8] == raw[64..72] && native[8..72] == raw[0..64]; + println!( + " native struct layout: mantissa-then-exponent={plane_order} exponent-then-mantissa={exp_first}" + ); + if !plane_order && !exp_first { + println!(" native bytes: {native:02x?}"); + } +} + +fn main() { + let dir = std::env::args().nth(1).expect("usage: runner "); + let xclbin = std::fs::read(format!("{dir}/final.xclbin")).expect("read final.xclbin"); + let insts = std::fs::read(format!("{dir}/insts.bin")).expect("read insts.bin"); + let container = artifact::encode( + "MLIR_AIE", + &[INPUT_BYTES], + &[OUTPUT_BYTES], + &xclbin, + &insts, + ); + + let backend = XdnaAccelerator::new().expect("construct XDNA backend (needs the NPU + HRX)"); + let context = backend + .create_context(ContextDesc::default()) + .expect("context"); + let queue = backend + .create_queue(&context, QueueDesc::default()) + .expect("queue"); + let program = backend + .load_program( + &context, + ArtifactRef { + format: XDNA_PRECOMPILED_FORMAT, + target: TargetIdentity([0; 12]), + payload: &Slice(&container), + resident_bytes: u64::MAX, + }, + ) + .expect("load probe artifact"); + + let (mut input, _) = backend + .allocate_buffer( + &context, + BufferDesc::new( + INPUT_BYTES, + 4096, + MemoryDomain::Shared, + BufferUsage::TRANSFER_DESTINATION | BufferUsage::PROGRAM_INPUT, + ) + .unwrap(), + ) + .expect("input buffer") + .into_parts(); + let (output, _) = backend + .allocate_buffer( + &context, + BufferDesc::new( + OUTPUT_BYTES, + 4096, + MemoryDomain::Shared, + BufferUsage::TRANSFER_SOURCE | BufferUsage::PROGRAM_OUTPUT, + ) + .unwrap(), + ) + .expect("output buffer") + .into_parts(); + + // Case A: each block holds one constant power of two -> distinct exponent per block. + let mut a = [0f32; 64]; + for block in 0..8 { + for lane in 0..8 { + a[block * 8 + lane] = (2f32).powi(block as i32 - 3); + } + } + // Case B: one block probing sign and range; other blocks zero. + let mut b = [0f32; 64]; + b[..8].copy_from_slice(&[1.0, -1.0, 0.5, -0.5, 1.5, -1.5, 127.0 / 64.0, -2.0]); + // Case C: mixed magnitudes in one block -> normalization loss. + let mut c = [0f32; 64]; + c[..8].copy_from_slice(&[ + 1.0, + 1.0 / 64.0, + 1.0 / 128.0, + 1.5 / 64.0, + -1.0 / 64.0, + 0.0, + 1.0 + 1.0 / 64.0, + -1.0 - 1.0 / 64.0, + ]); + // Case D: all zeros. + let d = [0f32; 64]; + + for (name, values) in [("A", &a), ("B", &b), ("C", &c), ("D", &d)] { + run_case(&backend, &queue, &program, &mut input, &output, name, values); + } + + assert!(backend.free_buffer(input).is_ok(), "free input"); + assert!(backend.free_buffer(output).is_ok(), "free output"); + assert!(backend.unload_program(program).is_ok(), "unload"); + assert!(backend.destroy_queue(queue).is_ok(), "destroy queue"); + assert!(backend.destroy_context(context).is_ok(), "destroy context"); + println!("P0 complete"); +} From 0a38cefa054ad32fc9575572cd29e1243ef7a0ec Mon Sep 17 00:00:00 2001 From: Aryan Ravishankar Date: Thu, 27 Aug 2026 19:36:55 -0500 Subject: [PATCH 03/10] P1-P3 on silicon: rounding, normalization, exceptional values (#146) All ten crrnd rounding modes match the reference bit-exactly on every lane, ties included, so round-to-nearest-even is available and exact -- it just is not the default. Post-rounding overflow renormalizes: a mantissa that rounds up to 128 bumps the shared exponent and re-quantizes, and never saturates or wraps. Normalization selects the max member's IEEE FP32 exponent field (verified through e=254), flushes all-subnormal blocks to zero, and is sign-symmetric. Inf and NaN pass through structurally at e=255 with the implicit-one (+-64) and quiet-bit (+-96) mantissa patterns; block neighbors quantize at that scale with per-block isolation. Everything observed fits one model: IEEE exponent-field max, signed 1.fraction mantissas shifted to the shared scale, crrnd rounding, post-rounding renormalization. That model becomes the P4/P5 reference. The remaining silicon questions are the MMUL contract on crafted raw planes and the block-32 MXINT8 decomposition verdict. Co-Authored-By: Claude Opus 5 --- .../amdxdna-bfp16ebs8-characterization.md | 39 ++- .../{final.xclbin => p0-final.xclbin} | Bin .../artifacts/{insts.bin => p0-insts.bin} | Bin research/bfp16ebs8/artifacts/p1-final.xclbin | Bin 0 -> 9638 bytes research/bfp16ebs8/artifacts/p1-insts.bin | Bin 0 -> 300 bytes research/bfp16ebs8/kernel_p1.cc | 41 +++ research/bfp16ebs8/probe_compile.py | 24 +- research/bfp16ebs8/results/p1-2026-08-27.txt | 26 ++ research/bfp16ebs8/results/p2-2026-08-27.txt | 48 +++ research/bfp16ebs8/results/p3-2026-08-27.txt | 26 ++ research/bfp16ebs8/runner/src/main.rs | 331 +++++++++++++++--- 11 files changed, 480 insertions(+), 55 deletions(-) rename research/bfp16ebs8/artifacts/{final.xclbin => p0-final.xclbin} (100%) rename research/bfp16ebs8/artifacts/{insts.bin => p0-insts.bin} (100%) create mode 100644 research/bfp16ebs8/artifacts/p1-final.xclbin create mode 100644 research/bfp16ebs8/artifacts/p1-insts.bin create mode 100644 research/bfp16ebs8/kernel_p1.cc create mode 100644 research/bfp16ebs8/results/p1-2026-08-27.txt create mode 100644 research/bfp16ebs8/results/p2-2026-08-27.txt create mode 100644 research/bfp16ebs8/results/p3-2026-08-27.txt diff --git a/docs/research/amdxdna-bfp16ebs8-characterization.md b/docs/research/amdxdna-bfp16ebs8-characterization.md index b568049..8e7d431 100644 --- a/docs/research/amdxdna-bfp16ebs8-characterization.md +++ b/docs/research/amdxdna-bfp16ebs8-characterization.md @@ -1,7 +1,7 @@ # AMD XDNA2 `bfp16ebs8` characterization (issue #146) -Status: **in progress** — toolchain surface frozen 2026-08-27; probe P0 run on silicon -2026-08-27 (results in §6); P1–P5 pending. +Status: **in progress** — toolchain surface frozen 2026-08-27; probes P0–P3 run on silicon +2026-08-27 (results in §6); P4–P5 (MMUL contract, MX mapping verdict) pending. Part of #110. Companion to [tosa-int8-block-fp-status.md](tosa-int8-block-fp-status.md) and the forward spec `docs/research/amdxdna-blockfp-tier.md` (branch `grilling/first-numerical-tier`). @@ -199,6 +199,41 @@ This matches OCP MX v1.0's scale rule (`X = 2^(floor(log2(max|v|)) − emax_elem `emax_elem = 0` for INT8's 2.0 ceiling — same outcome as "max member's IEEE exponent" for all P0 cases); P2 adds the boundary cases where the two formulations could differ. +**P1 — rounding modes: all ten exact.** Every `crrnd` mode (`floor, ceil, sym_floor, sym_ceil, +neg_inf, pos_inf, sym_zero, sym_inf, conv_even, conv_odd`) matched the reference rounding +function on all 64 lanes, including negative ties and large-mantissa ties +(`results/p1-2026-08-27.txt`). `set_rnd`/`to_v64bfp16ebs8_conf` work from kernel context and +restore the previous mode. **Round-to-nearest-even is therefore available and exact**, which is +what the MX tier needs — it just is not the default. + +**P1b — overflow renormalizes, never saturates or wraps.** With exact mantissa `±127.5` at +`e = 127`, every mode that rounds the magnitude up to 128 (ceil, sym_ceil, pos_inf, sym_inf, +conv_even) bumps the shared exponent to 128 and re-quantizes the whole block; every mode that +rounds down (floor, sym_floor, neg_inf, sym_zero, conv_odd — 127.5 ties to odd 127) keeps +`e = 127`. The observed mantissas match the reference at the *post-bump* exponent in every mode. + +**P2 — normalization: `e` is the max member's IEEE FP32 exponent *field*.** Verified through +`e = 254` (`f32::MAX → m = 127`). An all-subnormal-FP32 block flushes to zero (`e = 0, m = 0`) — +input FTZ. A negative-only block is symmetric (`−1.984375 → m = −127` exact at `e = 127`). +Members far below the block maximum quantize at the shared scale under the active mode — under +the default floor mode a tiny *negative* member becomes `m = −1` rather than 0, one more reason +the MX tier must run under `conv_even`. (`results/p2-2026-08-27.txt`) + +**P3 — exceptional values are structural, not special-cased.** `+Inf → e = 255, m = +64` +(the implicit-one pattern), `−Inf → m = −64`, `NaN → e = 255, m = ±96` (quiet-bit pattern), +`±0 → m = 0`. Other members of an Inf/NaN block simply quantize at the `e = 255` scale +(`2^122`): positives floor to 0, negatives to −1 under the default mode — the max-exponent rule +extended to field 255, with per-block isolation confirmed. Consequence for the MX mapping: OCP +E8M0 reserves 255 as NaN and MXINT8 has no Inf, so `e = 255` blocks are outside MXINT8's domain; +the mapping direction MX → `bfp16ebs8` never produces them, and any `bfp16ebs8 → MX` path must +reject or canonicalize them. (`results/p3-2026-08-27.txt`) + +**Emerging model.** All P0–P3 observations are consistent with one description: the conversion +takes each member's IEEE FP32 representation, selects the block exponent as the maximum member +exponent field (0 treated as flush-to-zero, 255 passed through), forms each member's signed +mantissa `sign · 1.fraction · 2^6` shifted right by `e − e_member`, and rounds per the `crrnd` +mode with post-rounding renormalization. This is the reference model P4/P5 will encode. + **Bonus finding.** The conversion path `vector → accum → to_v64bfp16ebs8` compiles and runs on the first attempt through the standalone probe pipeline, which validates the P4/P5 plan of crafting raw planes host-side: the layout is now known well diff --git a/research/bfp16ebs8/artifacts/final.xclbin b/research/bfp16ebs8/artifacts/p0-final.xclbin similarity index 100% rename from research/bfp16ebs8/artifacts/final.xclbin rename to research/bfp16ebs8/artifacts/p0-final.xclbin diff --git a/research/bfp16ebs8/artifacts/insts.bin b/research/bfp16ebs8/artifacts/p0-insts.bin similarity index 100% rename from research/bfp16ebs8/artifacts/insts.bin rename to research/bfp16ebs8/artifacts/p0-insts.bin diff --git a/research/bfp16ebs8/artifacts/p1-final.xclbin b/research/bfp16ebs8/artifacts/p1-final.xclbin new file mode 100644 index 0000000000000000000000000000000000000000..639f22fbf805887bba2f4af0f660d48d93e379cb GIT binary patch literal 9638 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 literal 0 HcmV?d00001 diff --git a/research/bfp16ebs8/artifacts/p1-insts.bin b/research/bfp16ebs8/artifacts/p1-insts.bin new file mode 100644 index 0000000000000000000000000000000000000000..d526e88f06f9d0f5517ed0924cad0387c5d845df GIT binary patch literal 300 zcmZQzWMN}uWMJT6U|`Sz(m(_O7Z@1~fGh_n7l}FmWHUkeOpQPWNF6$00qVyGL@puZ zC4dTHJO!w|TQM~s016SbR|RS>%siMe%rFMbUVfl}JOhIaOdLqFK;?jX8$fPq0Ac|k J1_NLq0RZ)Y3^D)! literal 0 HcmV?d00001 diff --git a/research/bfp16ebs8/kernel_p1.cc b/research/bfp16ebs8/kernel_p1.cc new file mode 100644 index 0000000..e1473c9 --- /dev/null +++ b/research/bfp16ebs8/kernel_p1.cc @@ -0,0 +1,41 @@ +// P1 rounding probe (issue #146). +// +// Converts the same 64 caller-chosen FP32 values under every crrnd rounding mode via +// to_v64bfp16ebs8_conf, dumping the mantissa+exponent planes per mode: +// +// bytes [k*72 .. k*72+64) mantissa plane under mode MODES[k] +// bytes [k*72+64 .. k*72+72) exponent plane under mode MODES[k] +// bytes [720..724) get_rnd() at kernel entry (LE u32) +// +// MODES = {0,1,2,3,8,9,10,11,12,13} = {floor, ceil, sym_floor, sym_ceil, neg_inf, pos_inf, +// sym_zero, sym_inf, conv_even, conv_odd} per aie2p_defines.h. The host chooses tie-case +// inputs; the kernel is input-agnostic. +#include + +#include + +extern "C" void probe_p1(const float *__restrict input, uint32_t *__restrict output) { + uint8_t *bytes = reinterpret_cast(output); + + aie::vector values = aie::load_v<64>(input); + aie::accum acc; + acc.from_vector(values); + + output[180] = static_cast(::get_rnd()); + + const unsigned modes[10] = {0, 1, 2, 3, 8, 9, 10, 11, 12, 13}; + for (unsigned k = 0; k < 10; ++k) { + v64bfp16ebs8 encoded = ::to_v64bfp16ebs8_conf(acc, static_cast(modes[k])); + uint8_t *slot = bytes + k * 72; + v64int8 mantissa = encoded.mantissa; + const uint8_t *mantissa_bytes = reinterpret_cast(&mantissa); + for (unsigned i = 0; i < 64; ++i) { + slot[i] = mantissa_bytes[i]; + } + v8int8 exponent = encoded.exponent; + const uint8_t *exponent_bytes = reinterpret_cast(&exponent); + for (unsigned i = 0; i < 8; ++i) { + slot[64 + i] = exponent_bytes[i]; + } + } +} diff --git a/research/bfp16ebs8/probe_compile.py b/research/bfp16ebs8/probe_compile.py index c3358e3..4e552d4 100644 --- a/research/bfp16ebs8/probe_compile.py +++ b/research/bfp16ebs8/probe_compile.py @@ -20,6 +20,7 @@ P0_INPUT_FLOATS = 64 P0_OUTPUT_WORDS = 37 # 64 mantissa + 8 exponent + 72 native-struct bytes + 1 rnd word +P1_OUTPUT_WORDS = 181 # 10 modes x 72 plane bytes + 1 rnd word def configure_toolchain_env() -> None: @@ -60,26 +61,26 @@ def prepend(var: str, value: str) -> None: prepend("LD_LIBRARY_PATH", str(mlir_dir / "lib")) -def build_p0(): - """One worker: 64 FP32 in, one raw 148-byte encoding dump out.""" +def build_probe(name: str, output_words: int): + """One worker: 64 FP32 in, one raw little-endian word dump out.""" import aie.iron as iron import numpy as np from aie.iron import In, ObjectFifo, Out, Program, Runtime, Worker from aie.iron.kernel import ExternalFunction - kernel_source = (PROBE_DIR / "kernel_p0.cc").read_text() + kernel_source = (PROBE_DIR / f"kernel_{name}.cc").read_text() @iron.jit - def probe_p0(x_in: In, y_out: Out): + def probe(x_in: In, y_out: Out): input_ty = np.ndarray[(P0_INPUT_FLOATS,), np.dtype[np.float32]] - output_ty = np.ndarray[(P0_OUTPUT_WORDS,), np.dtype[np.uint32]] + output_ty = np.ndarray[(output_words,), np.dtype[np.uint32]] kernel = ExternalFunction( - "probe_p0", + f"probe_{name}", source_string=kernel_source, arg_types=[input_ty, output_ty], ) - of_in = ObjectFifo(input_ty, name="p0_in") - of_out = ObjectFifo(output_ty, name="p0_out") + of_in = ObjectFifo(input_ty, name="probe_in") + of_out = ObjectFifo(output_ty, name="probe_out") def core_fn(in_fifo, out_fifo, probe): source = in_fifo.acquire(1) @@ -97,11 +98,12 @@ def sequence(source, destination, in_prod, out_cons): rt = Runtime(sequence, [input_ty, output_ty, of_in.prod(), of_out.cons()]) return Program(iron.get_current_device(), rt, workers=[worker]).resolve_program() - return probe_p0 + return probe def main() -> int: - if len(sys.argv) != 3 or sys.argv[1] not in ("p0",): + probes = {"p0": P0_OUTPUT_WORDS, "p1": P1_OUTPUT_WORDS} + if len(sys.argv) != 3 or sys.argv[1] not in probes: print(__doc__, file=sys.stderr) return 2 out_dir = Path(sys.argv[2]) @@ -113,7 +115,7 @@ def main() -> int: iron.set_current_device(from_name("npu2")) - design = build_p0() + design = build_probe(sys.argv[1], probes[sys.argv[1]]) design.compile( xclbin_path=str(out_dir / "final.xclbin"), inst_path=str(out_dir / "insts.bin"), diff --git a/research/bfp16ebs8/results/p1-2026-08-27.txt b/research/bfp16ebs8/results/p1-2026-08-27.txt new file mode 100644 index 0000000..619d10c --- /dev/null +++ b/research/bfp16ebs8/results/p1-2026-08-27.txt @@ -0,0 +1,26 @@ +P1 run 2026-08-28T00:36:23Z +host: 7.1.8-200.fc44.x86_64, NPU 1022:17f0 rev 0x20, toolchain amdxdna-hrx-v2026.08 (mlir-aie 1.4.1, llvm-aie 21.0.0.2026080301+c9c5ecb7), HRX hrx-amdxdna-2026.07.30-amdxdna-hal-native-rel-eb0b39f-linux-x86_64 + +== case ties (rnd mode at entry: 0) + mode 0 floor: PASS (all 64 lanes match reference) e[0..4]=[127, 127, 127, 0] + mode 1 ceil: PASS (all 64 lanes match reference) e[0..4]=[127, 127, 127, 0] + mode 2 sym_floor: PASS (all 64 lanes match reference) e[0..4]=[127, 127, 127, 0] + mode 3 sym_ceil: PASS (all 64 lanes match reference) e[0..4]=[127, 127, 127, 0] + mode 8 neg_inf: PASS (all 64 lanes match reference) e[0..4]=[127, 127, 127, 0] + mode 9 pos_inf: PASS (all 64 lanes match reference) e[0..4]=[127, 127, 127, 0] + mode 10 sym_zero: PASS (all 64 lanes match reference) e[0..4]=[127, 127, 127, 0] + mode 11 sym_inf: PASS (all 64 lanes match reference) e[0..4]=[127, 127, 127, 0] + mode 12 conv_even: PASS (all 64 lanes match reference) e[0..4]=[127, 127, 127, 0] + mode 13 conv_odd: PASS (all 64 lanes match reference) e[0..4]=[127, 127, 127, 0] +== case sat (rnd mode at entry: 0) + mode 0 floor: PASS (all 64 lanes match reference) e[0..4]=[127, 0, 0, 0] + mode 1 ceil: PASS (all 64 lanes match reference) e[0..4]=[128, 0, 0, 0] + mode 2 sym_floor: PASS (all 64 lanes match reference) e[0..4]=[127, 0, 0, 0] + mode 3 sym_ceil: PASS (all 64 lanes match reference) e[0..4]=[128, 0, 0, 0] + mode 8 neg_inf: PASS (all 64 lanes match reference) e[0..4]=[127, 0, 0, 0] + mode 9 pos_inf: PASS (all 64 lanes match reference) e[0..4]=[128, 0, 0, 0] + mode 10 sym_zero: PASS (all 64 lanes match reference) e[0..4]=[127, 0, 0, 0] + mode 11 sym_inf: PASS (all 64 lanes match reference) e[0..4]=[128, 0, 0, 0] + mode 12 conv_even: PASS (all 64 lanes match reference) e[0..4]=[128, 0, 0, 0] + mode 13 conv_odd: PASS (all 64 lanes match reference) e[0..4]=[127, 0, 0, 0] +p1 complete diff --git a/research/bfp16ebs8/results/p2-2026-08-27.txt b/research/bfp16ebs8/results/p2-2026-08-27.txt new file mode 100644 index 0000000..deda098 --- /dev/null +++ b/research/bfp16ebs8/results/p2-2026-08-27.txt @@ -0,0 +1,48 @@ +P2 run 2026-08-28T00:36:24Z +host: 7.1.8-200.fc44.x86_64, NPU 1022:17f0 rev 0x20, toolchain amdxdna-hrx-v2026.08 (mlir-aie 1.4.1, llvm-aie 21.0.0.2026080301+c9c5ecb7), HRX hrx-amdxdna-2026.07.30-amdxdna-hal-native-rel-eb0b39f-linux-x86_64 + +== case N1 (rnd mode at entry: 0) + exponent plane: [7f, 00, 00, 00, 00, 00, 00, 00] + block 0 (e=127): 1->m=64 0.0078125->m=0! 0.00012207031->m=0! 0.000061035156->m=0! -0.00012207031->m=-1! 0.0000009536743->m=0! 0->m=0 1->m=64 + block 1 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 2 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 3 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + native struct layout: mantissa-then-exponent=true exponent-then-mantissa=false +== case N2 (rnd mode at entry: 0) + exponent plane: [7f, 00, 00, 00, 00, 00, 00, 00] + block 0 (e=127): -1->m=-64 -0.5->m=-32 -0.25->m=-16 -0.75->m=-48 -1.25->m=-80 -1.75->m=-112 -1.984375->m=-127 -0.125->m=-8 + block 1 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 2 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 3 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + native struct layout: mantissa-then-exponent=true exponent-then-mantissa=false +== case N3 (rnd mode at entry: 0) + exponent plane: [00, 00, 00, 00, 00, 00, 00, 00] + block 0 (e= 0): 0.000000000000000000000000000000000000000000001->m=0 0.000000000000000000000000000000000000011754942->m=0 0.000000000000000000000000000000000000005877472->m=0 -0.000000000000000000000000000000000000005877472->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 1 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 2 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 3 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + native struct layout: mantissa-then-exponent=true exponent-then-mantissa=true +== case N4 (rnd mode at entry: 0) + exponent plane: [fe, 00, 00, 00, 00, 00, 00, 00] + block 0 (e=254): 340282350000000000000000000000000000000->m=127! 170141170000000000000000000000000000000->m=63! 1->m=0! -1->m=-1! 1329228000000000000000000000000000000->m=0! -1329228000000000000000000000000000000->m=-1! 0->m=0 0->m=0 + block 1 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 2 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 3 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + native struct layout: mantissa-then-exponent=true exponent-then-mantissa=false +p2 complete diff --git a/research/bfp16ebs8/results/p3-2026-08-27.txt b/research/bfp16ebs8/results/p3-2026-08-27.txt new file mode 100644 index 0000000..2d78e00 --- /dev/null +++ b/research/bfp16ebs8/results/p3-2026-08-27.txt @@ -0,0 +1,26 @@ +P3 run 2026-08-28T00:36:24Z +host: 7.1.8-200.fc44.x86_64, NPU 1022:17f0 rev 0x20, toolchain amdxdna-hrx-v2026.08 (mlir-aie 1.4.1, llvm-aie 21.0.0.2026080301+c9c5ecb7), HRX hrx-amdxdna-2026.07.30-amdxdna-hal-native-rel-eb0b39f-linux-x86_64 + +== case X1 (rnd mode at entry: 0) + exponent plane: [ff, 00, 00, 00, 00, 00, 00, 00] + block 0 (e=255): inf->m=64! -inf->m=-64! NaN->m=96! NaN->m=-96! 1->m=0! -1->m=-1! 0->m=0 -0->m=0 + block 1 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 2 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 3 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + native struct layout: mantissa-then-exponent=true exponent-then-mantissa=false +== case X2 (rnd mode at entry: 0) + exponent plane: [ff, ff, fe, 00, 00, 00, 00, 00] + block 0 (e=255): inf->m=64! 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 1 (e=255): NaN->m=96! 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 2 (e=254): 1->m=0! 340282350000000000000000000000000000000->m=127! 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 3 (e= 0): -0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + native struct layout: mantissa-then-exponent=true exponent-then-mantissa=false +p3 complete diff --git a/research/bfp16ebs8/runner/src/main.rs b/research/bfp16ebs8/runner/src/main.rs index dc0be0e..23f21c7 100644 --- a/research/bfp16ebs8/runner/src/main.rs +++ b/research/bfp16ebs8/runner/src/main.rs @@ -51,22 +51,87 @@ impl ByteSink for SliceMut<'_> { } const INPUT_BYTES: u64 = 64 * 4; -const OUTPUT_BYTES: u64 = 148; +const P0_OUTPUT_BYTES: u64 = 148; +const P1_OUTPUT_BYTES: u64 = 724; +const P1_MODES: [(u32, &str); 10] = [ + (0, "floor"), + (1, "ceil"), + (2, "sym_floor"), + (3, "sym_ceil"), + (8, "neg_inf"), + (9, "pos_inf"), + (10, "sym_zero"), + (11, "sym_inf"), + (12, "conv_even"), + (13, "conv_odd"), +]; + +/// Reference rounding of an exact mantissa-unit value under a crrnd mode. +fn round_reference(mode: u32, x: f64) -> f64 { + let half = x.abs().fract() == 0.5; + match mode { + 0 => x.floor(), + 1 => x.ceil(), + 2 => x.trunc(), + 3 => x.abs().ceil().copysign(x), + 8 => { + if half { + x.floor() + } else { + x.round() + } + } + 9 => { + if half { + x.ceil() + } else { + x.round() + } + } + 10 => { + if half { + x.trunc() + } else { + x.round() + } + } + 11 => x.round(), // f64::round is ties-away-from-zero + 12 => { + // ties to even + if half { + let down = x.floor(); + if (down as i64) % 2 == 0 { down } else { x.ceil() } + } else { + x.round() + } + } + 13 => { + // ties to odd + if half { + let down = x.floor(); + if (down as i64) % 2 != 0 { down } else { x.ceil() } + } else { + x.round() + } + } + _ => unreachable!(), + } +} /// H1: value = mantissa * 2^(exponent - 127 - 6), two's-complement int8 mantissa. fn h1_decode(mantissa: i8, exponent: u8) -> f64 { f64::from(mantissa) * (f64::from(exponent) - 133.0).exp2() } -fn run_case( +fn submit_case( backend: &A, queue: &A::Queue, program: &A::Program, input: &mut A::Buffer, output: &A::Buffer, - name: &str, + out_bytes: u64, values: &[f32; 64], -) { +) -> Vec { let mut payload = Vec::with_capacity(INPUT_BYTES as usize); for v in values { payload.extend_from_slice(&v.to_le_bytes()); @@ -85,7 +150,7 @@ fn run_case( BindingRef { slot: 1, buffer: output, - range: BufferRange::new(0, OUTPUT_BYTES).unwrap(), + range: BufferRange::new(0, out_bytes).unwrap(), access: AccessMode::Write, }, ]; @@ -109,11 +174,14 @@ fn run_case( panic!("destroy event failed"); } - let mut raw = vec![0u8; OUTPUT_BYTES as usize]; + let mut raw = vec![0u8; out_bytes as usize]; backend .read_buffer(output, 0, &mut SliceMut(&mut raw)) .expect("read output"); + raw +} +fn decode_p0(name: &str, values: &[f32; 64], raw: &[u8]) { let mantissa = &raw[0..64]; let exponent = &raw[64..72]; let native = &raw[72..144]; @@ -149,17 +217,58 @@ fn run_case( } } +fn decode_p1(name: &str, values: &[f32; 64], raw: &[u8]) { + let entry_rnd = u32::from_le_bytes(raw[720..724].try_into().unwrap()); + println!("== case {name} (rnd mode at entry: {entry_rnd})"); + for (slot, (mode, mode_name)) in P1_MODES.iter().enumerate() { + let plane = &raw[slot * 72..slot * 72 + 72]; + let mantissa = &plane[0..64]; + let exponent = &plane[64..72]; + let mut mismatches = Vec::new(); + for block in 0..8 { + let e = exponent[block]; + for lane in 0..8 { + let idx = block * 8 + lane; + let v = f64::from(values[idx]); + if v == 0.0 && e == 0 { + continue; + } + let exact = v * (133.0 - f64::from(e)).exp2(); + let expected = round_reference(*mode, exact); + let got = f64::from(mantissa[idx] as i8); + if got != expected { + mismatches.push(format!( + "idx {idx} v={v} e={e} exact={exact} expected m={expected} got m={got}" + )); + } + } + } + if mismatches.is_empty() { + println!( + " mode {mode:2} {mode_name:>9}: PASS (all 64 lanes match reference) e[0..4]={:?}", + &exponent[0..4] + ); + } else { + println!(" mode {mode:2} {mode_name:>9}: {} MISMATCHES", mismatches.len()); + for m in &mismatches { + println!(" {m}"); + } + } + } +} + fn main() { - let dir = std::env::args().nth(1).expect("usage: runner "); + let mut args = std::env::args().skip(1); + let probe = args.next().expect("usage: runner "); + let dir = args.next().expect("usage: runner "); + let out_bytes = match probe.as_str() { + "p0" | "p2" | "p3" => P0_OUTPUT_BYTES, + "p1" => P1_OUTPUT_BYTES, + other => panic!("unknown probe {other}"), + }; let xclbin = std::fs::read(format!("{dir}/final.xclbin")).expect("read final.xclbin"); let insts = std::fs::read(format!("{dir}/insts.bin")).expect("read insts.bin"); - let container = artifact::encode( - "MLIR_AIE", - &[INPUT_BYTES], - &[OUTPUT_BYTES], - &xclbin, - &insts, - ); + let container = artifact::encode("MLIR_AIE", &[INPUT_BYTES], &[out_bytes], &xclbin, &insts); let backend = XdnaAccelerator::new().expect("construct XDNA backend (needs the NPU + HRX)"); let context = backend @@ -197,7 +306,7 @@ fn main() { .allocate_buffer( &context, BufferDesc::new( - OUTPUT_BYTES, + out_bytes, 4096, MemoryDomain::Shared, BufferUsage::TRANSFER_SOURCE | BufferUsage::PROGRAM_OUTPUT, @@ -207,33 +316,171 @@ fn main() { .expect("output buffer") .into_parts(); - // Case A: each block holds one constant power of two -> distinct exponent per block. - let mut a = [0f32; 64]; - for block in 0..8 { - for lane in 0..8 { - a[block * 8 + lane] = (2f32).powi(block as i32 - 3); + match probe.as_str() { + "p0" => { + // Case A: each block holds one constant power of two -> distinct exponent per block. + let mut a = [0f32; 64]; + for block in 0..8 { + for lane in 0..8 { + a[block * 8 + lane] = (2f32).powi(block as i32 - 3); + } + } + // Case B: one block probing sign and range; other blocks zero. + let mut b = [0f32; 64]; + b[..8].copy_from_slice(&[1.0, -1.0, 0.5, -0.5, 1.5, -1.5, 127.0 / 64.0, -2.0]); + // Case C: mixed magnitudes in one block -> normalization loss. + let mut c = [0f32; 64]; + c[..8].copy_from_slice(&[ + 1.0, + 1.0 / 64.0, + 1.0 / 128.0, + 1.5 / 64.0, + -1.0 / 64.0, + 0.0, + 1.0 + 1.0 / 64.0, + -1.0 - 1.0 / 64.0, + ]); + // Case D: all zeros. + let d = [0f32; 64]; + for (name, values) in [("A", &a), ("B", &b), ("C", &c), ("D", &d)] { + let raw = + submit_case(&backend, &queue, &program, &mut input, &output, out_bytes, values); + decode_p0(name, values, &raw); + } } - } - // Case B: one block probing sign and range; other blocks zero. - let mut b = [0f32; 64]; - b[..8].copy_from_slice(&[1.0, -1.0, 0.5, -0.5, 1.5, -1.5, 127.0 / 64.0, -2.0]); - // Case C: mixed magnitudes in one block -> normalization loss. - let mut c = [0f32; 64]; - c[..8].copy_from_slice(&[ - 1.0, - 1.0 / 64.0, - 1.0 / 128.0, - 1.5 / 64.0, - -1.0 / 64.0, - 0.0, - 1.0 + 1.0 / 64.0, - -1.0 - 1.0 / 64.0, - ]); - // Case D: all zeros. - let d = [0f32; 64]; - - for (name, values) in [("A", &a), ("B", &b), ("C", &c), ("D", &d)] { - run_case(&backend, &queue, &program, &mut input, &output, name, values); + "p1" => { + // All blocks anchored by max member ±1.0 (IEEE exp 0 -> e = 127), so one mantissa + // unit is exactly 1/64 and the chosen fractions produce exact binary ties. + let mut t = [0f32; 64]; + // b0: half-unit ties, positive and negative. + t[..8].copy_from_slice(&[ + 1.0, + 0.5 / 64.0, + 1.5 / 64.0, + 2.5 / 64.0, + -0.5 / 64.0, + -1.5 / 64.0, + -2.5 / 64.0, + -1.0, + ]); + // b1: quarter-unit non-ties (round direction without tie ambiguity). + t[8..16].copy_from_slice(&[ + 1.0, + 0.25 / 64.0, + 0.75 / 64.0, + 1.25 / 64.0, + -0.25 / 64.0, + -0.75 / 64.0, + -1.25 / 64.0, + -1.0, + ]); + // b2: large-mantissa ties and the saturation corner (exact x: 127, -127, 64.5, + // -64.5, 65.5, -65.5, 32, -32 at e = 127). + t[16..24].copy_from_slice(&[ + 127.0 / 64.0, + -127.0 / 64.0, + 64.5 / 64.0, + -64.5 / 64.0, + 65.5 / 64.0, + -65.5 / 64.0, + 0.5, + -0.5, + ]); + let raw = submit_case(&backend, &queue, &program, &mut input, &output, out_bytes, &t); + decode_p1("ties", &t, &raw); + + // Saturation-by-rounding: exact x = +-127.5 at e = 127. A mode that rounds the + // magnitude up cannot produce m = 128; the exponent plane shows whether the + // hardware bumps e to 128 (m = 64) or saturates m at 127. + let mut sat = [0f32; 64]; + sat[..8].copy_from_slice(&[ + 127.5 / 64.0, + -127.5 / 64.0, + 1.0, + -1.0, + 127.0 / 64.0, + -127.0 / 64.0, + 0.5, + -0.5, + ]); + let raw = + submit_case(&backend, &queue, &program, &mut input, &output, out_bytes, &sat); + decode_p1("sat", &sat, &raw); + } + "p2" => { + // Normalization corners through the P0 kernel (default rounding = floor). + // N1: exponent spread beyond the mantissa (tiny values vanish at the max's e). + let mut n1 = [0f32; 64]; + n1[..8].copy_from_slice(&[ + 1.0, + (2f32).powi(-7), + (2f32).powi(-13), + (2f32).powi(-14), + -(2f32).powi(-13), + (2f32).powi(-20), + 0.0, + 1.0, + ]); + // N2: negative-only block (max magnitude negative). + let mut n2 = [0f32; 64]; + n2[..8].copy_from_slice(&[-1.0, -0.5, -0.25, -0.75, -1.25, -1.75, -1.984375, -0.125]); + // N3: FP32 subnormal-only block (IEEE exponent field 0). + let mut n3 = [0f32; 64]; + n3[..8].copy_from_slice(&[ + f32::from_bits(0x0000_0001), + f32::from_bits(0x007f_ffff), + f32::from_bits(0x0040_0000), + -f32::from_bits(0x0040_0000), + 0.0, + 0.0, + 0.0, + 0.0, + ]); + // N4: near the top of the FP32 exponent range. + let mut n4 = [0f32; 64]; + n4[..8].copy_from_slice(&[ + f32::MAX, + f32::MAX / 2.0, + 1.0, + -1.0, + (2f32).powi(120), + -(2f32).powi(120), + 0.0, + 0.0, + ]); + for (name, values) in [("N1", &n1), ("N2", &n2), ("N3", &n3), ("N4", &n4)] { + let raw = + submit_case(&backend, &queue, &program, &mut input, &output, out_bytes, values); + decode_p0(name, values, &raw); + } + } + "p3" => { + // Exceptional values through the P0 kernel. + let mut x1 = [0f32; 64]; + x1[..8].copy_from_slice(&[ + f32::INFINITY, + f32::NEG_INFINITY, + f32::NAN, + -f32::NAN, + 1.0, + -1.0, + 0.0, + -0.0, + ]); + // Inf/NaN isolated per block (do they poison only their own block's exponent?). + let mut x2 = [0f32; 64]; + x2[0] = f32::INFINITY; + x2[8] = f32::NAN; + x2[16] = 1.0; + x2[17] = f32::from_bits(0x7f7f_ffff); // f32::MAX + x2[24] = -0.0; + for (name, values) in [("X1", &x1), ("X2", &x2)] { + let raw = + submit_case(&backend, &queue, &program, &mut input, &output, out_bytes, values); + decode_p0(name, values, &raw); + } + } + _ => unreachable!(), } assert!(backend.free_buffer(input).is_ok(), "free input"); @@ -241,5 +488,5 @@ fn main() { assert!(backend.unload_program(program).is_ok(), "unload"); assert!(backend.destroy_queue(queue).is_ok(), "destroy queue"); assert!(backend.destroy_context(context).is_ok(), "destroy context"); - println!("P0 complete"); + println!("{probe} complete"); } From e38c05b9b028cef25457600a60d693b1ec324793 Mon Sep 17 00:00:00 2001 From: Aryan Ravishankar Date: Thu, 27 Aug 2026 19:42:19 -0500 Subject: [PATCH 04/10] P4-P5 on silicon: MMUL is bit-exact; the MX mapping verdict is exact (#146) P4 pins the matrix-unit contract with host-crafted raw planes that bypass the converter: transposed-B layout (A lane i*8+k, B lane j*8+k, C lane i*8+j, block exponent per row), and mul/mac chains over K=32 reproduce sum(m_a * m_b * 2^(e_a+e_b-266)) bit-exactly in FP32 -- including per-block exponent disagreement and mantissa -128, which the converter never emits but the matrix unit honors as -2.0. P5 answers #110 stage 1: a block-32 MXINT8 dot product decomposed into four equal-exponent block-8 groups is bit-exact against the pinned reference, and (m, e) vs (m/2, e+1) operands produce bit-identical lanes, so results depend on values rather than block normalization. Verdict recorded in the note: exact MX mapping, conditional on rnd_conv_even quantization (the hardware default is rnd_floor), on rejecting e=255 blocks (hardware Inf/NaN encodings outside MXINT8's domain), and on the tier bounding K to integer-exact FP32 accumulation, the same envelope discipline the exact INT8 tier uses. Co-Authored-By: Claude Opus 5 --- .../amdxdna-bfp16ebs8-characterization.md | 44 ++- research/bfp16ebs8/artifacts/p4-final.xclbin | Bin 0 -> 9526 bytes research/bfp16ebs8/artifacts/p4-insts.bin | Bin 0 -> 300 bytes research/bfp16ebs8/kernel_p4.cc | 34 ++ research/bfp16ebs8/probe_compile.py | 14 +- research/bfp16ebs8/results/p4-2026-08-27.txt | 13 + research/bfp16ebs8/results/p5-2026-08-27.txt | 8 + research/bfp16ebs8/runner/src/main.rs | 374 ++++++++++++++++-- 8 files changed, 459 insertions(+), 28 deletions(-) create mode 100644 research/bfp16ebs8/artifacts/p4-final.xclbin create mode 100644 research/bfp16ebs8/artifacts/p4-insts.bin create mode 100644 research/bfp16ebs8/kernel_p4.cc create mode 100644 research/bfp16ebs8/results/p4-2026-08-27.txt create mode 100644 research/bfp16ebs8/results/p5-2026-08-27.txt diff --git a/docs/research/amdxdna-bfp16ebs8-characterization.md b/docs/research/amdxdna-bfp16ebs8-characterization.md index 8e7d431..c2ac095 100644 --- a/docs/research/amdxdna-bfp16ebs8-characterization.md +++ b/docs/research/amdxdna-bfp16ebs8-characterization.md @@ -1,7 +1,7 @@ # AMD XDNA2 `bfp16ebs8` characterization (issue #146) -Status: **in progress** — toolchain surface frozen 2026-08-27; probes P0–P3 run on silicon -2026-08-27 (results in §6); P4–P5 (MMUL contract, MX mapping verdict) pending. +Status: **probes complete** — toolchain surface frozen 2026-08-27; probes P0–P5 run on silicon +2026-08-27 (results in §6). Verdict: **exact MX mapping, with three stated conditions** (§6a). Part of #110. Companion to [tosa-int8-block-fp-status.md](tosa-int8-block-fp-status.md) and the forward spec `docs/research/amdxdna-blockfp-tier.md` (branch `grilling/first-numerical-tier`). @@ -234,6 +234,46 @@ exponent field (0 treated as flush-to-zero, 255 passed through), forms each memb mantissa `sign · 1.fraction · 2^6` shifted right by `e − e_member`, and rounds per the `crrnd` mode with post-rounding renormalization. This is the reference model P4/P5 will encode. +**P4 — MMUL contract: bit-exact against the H1 semantic model.** The operand layout is the +transposed-B form the intrinsic names imply: A lane `i*8+k`, B lane `j*8+k`, C lane `i*8+j`, +with block `n`'s exponent byte applying to row `n` of its operand (established empirically with +single-entry operands, `results/p4-2026-08-27.txt`). With host-crafted raw planes bypassing the +converter entirely, `mul_8x8_8x8T`/`mac_8x8_8x8T` chains over K = 32 reproduce the reference +`sum(m_a · m_b · 2^(e_a + e_b − 266))` bit-exactly in FP32 — including per-chunk exponent +differences, per-*block* exponent disagreement inside one chunk, and mantissa `−128`, which the +converter never emits (P0/P2) but the matrix unit honors as exactly −2.0. + +**P5 — the #110 stage-1 hypothesis: CONFIRMED.** A block-32 MXINT8 dot product (one E8M0 scale, +32 int8 elements per operand row) decomposed into four `bfp16ebs8` blocks with equal exponent +bytes is bit-exact on the matrix unit against the pinned reference (`results/p5-2026-08-27.txt`). +Scale-representation equivalence holds: the same numeric operand expressed as `(m, e)` and as +`(m/2, e+1)` produces bit-identical accumulator lanes, so results depend on values, not on the +chosen block normalization. + +### 6a. Verdict for issue #146 + +**Exact MX mapping**, with the conditions the probes surfaced — each a requirement on the future +tier (#148 / #110 stages 2+), not a hardware defect: + +1. **Quantization must run under `rnd_conv_even`.** The hardware default is `rnd_floor` (P1); + OCP MX v1.0 specifies round-to-nearest-even. The mode is exact when set (P1), so this is one + `set_rnd` per conversion kernel. +2. **`e = 255` is outside MXINT8's domain.** The hardware represents Inf (`m = ±64`) and NaN + (`m = ±96`) at exponent 255 (P3); OCP E8M0 reserves 255 as NaN and MXINT8 has no Inf. The + MX → `bfp16ebs8` direction never produces such blocks; any path accepting raw `bfp16ebs8` + must reject or canonicalize them before claiming MX semantics. +3. **Exactness is proven for integer-exact accumulations.** The P4/P5 dot products keep every + partial sum exactly representable in FP32 (products ≤ 2^14, ≤ 32 terms), so accumulation + order cannot affect them — the same bounded-envelope discipline the exact INT8 tier uses. + A tier admitting larger K must either re-prove order-independence for its envelope or bound + K accordingly. FP32 accumulation is confirmed (`accfloat`, P4); the accumulation *order* for + general values is deliberately not part of this verdict. + +Additional tier-relevant facts: conversion out of `bfp16ebs8` is matrix-unit math (identity +MMUL), not a cast (§1); the converter's normalization matches OCP MX v1.0's scale rule on every +probed case (P2); and subnormal FP32 inputs flush to zero on conversion (P2), which an MX +quantizer reference must model. + **Bonus finding.** The conversion path `vector → accum → to_v64bfp16ebs8` compiles and runs on the first attempt through the standalone probe pipeline, which validates the P4/P5 plan of crafting raw planes host-side: the layout is now known well diff --git a/research/bfp16ebs8/artifacts/p4-final.xclbin b/research/bfp16ebs8/artifacts/p4-final.xclbin new file mode 100644 index 0000000000000000000000000000000000000000..f9b23ca342fe3696112500a1be2d3164433cc14a GIT binary patch literal 9526 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 Yj+&ZD&mwLAed?|qgA=@0HMP3`3&M&$0RR91 literal 0 HcmV?d00001 diff --git a/research/bfp16ebs8/artifacts/p4-insts.bin b/research/bfp16ebs8/artifacts/p4-insts.bin new file mode 100644 index 0000000000000000000000000000000000000000..47a8e7eb6b1d97d8d51624297371c8ae2911bf53 GIT binary patch literal 300 zcmZQzWMN}uWMJT6U|`Sz(m(_O7Z@1~fUF5HK7u{~WHLdyOpQPWNF6$00qVyGL@puZ zC4dTHJO!w|4w#w`0EGzJs{*wbW**EKW*7rzFF#N~o`FFICJv-opmIRH4Ino)0I>iN Ig8?v*00pZI4FCWD literal 0 HcmV?d00001 diff --git a/research/bfp16ebs8/kernel_p4.cc b/research/bfp16ebs8/kernel_p4.cc new file mode 100644 index 0000000..7b31bb4 --- /dev/null +++ b/research/bfp16ebs8/kernel_p4.cc @@ -0,0 +1,34 @@ +// P4/P5 MMUL-contract probe (issue #146). +// +// The host crafts RAW bfp16ebs8 planes (layout pinned by P0: 64 mantissa bytes then 8 exponent +// bytes per v64), bypassing the converter entirely. The kernel chains the native 8x8x8T MMUL +// over K = 32 (4 sub-blocks) and returns the FP32 accumulator: +// +// input [0..288) A: four v64bfp16ebs8 chunks (8x32 operand, chunk k = columns 8k..8k+8) +// input [288..576) B: four v64bfp16ebs8 chunks (32x8 operand, transposed-B layout) +// output [0..256) C: 64 FP32 accumulator lanes, stored in lane order +// +// The host discovers the actual element->matrix-position mapping empirically (single-entry +// operands), so this kernel makes no layout claim beyond "chunk k multiplies chunk k". +#include + +#include + +extern "C" void probe_p4(const uint8_t *__restrict input, float *__restrict output) { + v64bfp16ebs8 a[4]; + v64bfp16ebs8 b[4]; + uint8_t *a_bytes = reinterpret_cast(a); + uint8_t *b_bytes = reinterpret_cast(b); + for (unsigned i = 0; i < 288; ++i) { + a_bytes[i] = input[i]; + b_bytes[i] = input[288 + i]; + } + + v64accfloat acc = ::mul_8x8_8x8T(a[0], b[0]); + for (unsigned k = 1; k < 4; ++k) { + acc = ::mac_8x8_8x8T(a[k], b[k], acc); + } + + aie::accum result(acc); + aie::store_v(output, result.template to_vector()); +} diff --git a/research/bfp16ebs8/probe_compile.py b/research/bfp16ebs8/probe_compile.py index 4e552d4..8b69b2c 100644 --- a/research/bfp16ebs8/probe_compile.py +++ b/research/bfp16ebs8/probe_compile.py @@ -21,6 +21,8 @@ P0_INPUT_FLOATS = 64 P0_OUTPUT_WORDS = 37 # 64 mantissa + 8 exponent + 72 native-struct bytes + 1 rnd word P1_OUTPUT_WORDS = 181 # 10 modes x 72 plane bytes + 1 rnd word +P4_INPUT_BYTES = 576 # A (4 x 72) then B (4 x 72) raw bfp16ebs8 planes +P4_OUTPUT_WORDS = 64 # 64 FP32 accumulator lanes def configure_toolchain_env() -> None: @@ -62,7 +64,7 @@ def prepend(var: str, value: str) -> None: def build_probe(name: str, output_words: int): - """One worker: 64 FP32 in, one raw little-endian word dump out.""" + """One worker: one input line in, one raw little-endian word dump out.""" import aie.iron as iron import numpy as np from aie.iron import In, ObjectFifo, Out, Program, Runtime, Worker @@ -72,8 +74,12 @@ def build_probe(name: str, output_words: int): @iron.jit def probe(x_in: In, y_out: Out): - input_ty = np.ndarray[(P0_INPUT_FLOATS,), np.dtype[np.float32]] - output_ty = np.ndarray[(output_words,), np.dtype[np.uint32]] + if name == "p4": + input_ty = np.ndarray[(P4_INPUT_BYTES,), np.dtype[np.uint8]] + output_ty = np.ndarray[(output_words,), np.dtype[np.float32]] + else: + input_ty = np.ndarray[(P0_INPUT_FLOATS,), np.dtype[np.float32]] + output_ty = np.ndarray[(output_words,), np.dtype[np.uint32]] kernel = ExternalFunction( f"probe_{name}", source_string=kernel_source, @@ -102,7 +108,7 @@ def sequence(source, destination, in_prod, out_cons): def main() -> int: - probes = {"p0": P0_OUTPUT_WORDS, "p1": P1_OUTPUT_WORDS} + probes = {"p0": P0_OUTPUT_WORDS, "p1": P1_OUTPUT_WORDS, "p4": P4_OUTPUT_WORDS} if len(sys.argv) != 3 or sys.argv[1] not in probes: print(__doc__, file=sys.stderr) return 2 diff --git a/research/bfp16ebs8/results/p4-2026-08-27.txt b/research/bfp16ebs8/results/p4-2026-08-27.txt new file mode 100644 index 0000000..b8dcb37 --- /dev/null +++ b/research/bfp16ebs8/results/p4-2026-08-27.txt @@ -0,0 +1,13 @@ +P4 run 2026-08-28T00:41:43Z +host: 7.1.8-200.fc44.x86_64, NPU 1022:17f0 rev 0x20, toolchain amdxdna-hrx-v2026.08 (mlir-aie 1.4.1, llvm-aie 21.0.0.2026080301+c9c5ecb7), HRX hrx-amdxdna-2026.07.30-amdxdna-hal-native-rel-eb0b39f-linux-x86_64 + +== P4 layout inference (single-entry A, graded B, chunk 0 only) + A[0]=1.0: C[0,0]=0.015625 C[0,1]=0.140625 C[0,2]=0.265625 C[0,3]=0.390625 C[0,4]=0.515625 C[0,5]=0.640625 C[0,6]=0.765625 C[0,7]=0.890625 + A[1]=1.0: C[0,0]=0.03125 C[0,1]=0.15625 C[0,2]=0.28125 C[0,3]=0.40625 C[0,4]=0.53125 C[0,5]=0.65625 C[0,6]=0.78125 C[0,7]=0.90625 + A[8]=1.0: C[1,0]=0.015625 C[1,1]=0.140625 C[1,2]=0.265625 C[1,3]=0.390625 C[1,4]=0.515625 C[1,5]=0.640625 C[1,6]=0.765625 C[1,7]=0.890625 + A[9]=1.0: C[1,0]=0.03125 C[1,1]=0.15625 C[1,2]=0.28125 C[1,3]=0.40625 C[1,4]=0.53125 C[1,5]=0.65625 C[1,6]=0.78125 C[1,7]=0.90625 +== P4 exactness under the inferred layout + uniform + per-chunk exponents (incl. m = -128): PASS (all 64 lanes bit-exact vs reference) +== P4 per-block exponent disagreement inside one chunk + disagreeing block exponents: PASS (all 64 lanes bit-exact vs reference) +p4 complete diff --git a/research/bfp16ebs8/results/p5-2026-08-27.txt b/research/bfp16ebs8/results/p5-2026-08-27.txt new file mode 100644 index 0000000..f5352b4 --- /dev/null +++ b/research/bfp16ebs8/results/p5-2026-08-27.txt @@ -0,0 +1,8 @@ +P5 run 2026-08-28T00:41:44Z +host: 7.1.8-200.fc44.x86_64, NPU 1022:17f0 rev 0x20, toolchain amdxdna-hrx-v2026.08 (mlir-aie 1.4.1, llvm-aie 21.0.0.2026080301+c9c5ecb7), HRX hrx-amdxdna-2026.07.30-amdxdna-hal-native-rel-eb0b39f-linux-x86_64 + +== P5 MXINT8 block-32 decomposition (H6) + MXINT8 32-dot as four equal-exponent block-8 groups: PASS (all 64 lanes bit-exact vs reference) + same values via (m, e) vs (m/2, e+1): BIT-IDENTICAL + equal-exponent form vs reference: PASS (all 64 lanes bit-exact vs reference) +p5 complete diff --git a/research/bfp16ebs8/runner/src/main.rs b/research/bfp16ebs8/runner/src/main.rs index 23f21c7..03ef150 100644 --- a/research/bfp16ebs8/runner/src/main.rs +++ b/research/bfp16ebs8/runner/src/main.rs @@ -50,9 +50,11 @@ impl ByteSink for SliceMut<'_> { } } -const INPUT_BYTES: u64 = 64 * 4; +const F32_INPUT_BYTES: u64 = 64 * 4; const P0_OUTPUT_BYTES: u64 = 148; const P1_OUTPUT_BYTES: u64 = 724; +const P4_INPUT_BYTES: u64 = 576; +const P4_OUTPUT_BYTES: u64 = 256; const P1_MODES: [(u32, &str); 10] = [ (0, "floor"), (1, "ceil"), @@ -118,6 +120,124 @@ fn round_reference(mode: u32, x: f64) -> f64 { } } +fn f32_payload(values: &[f32; 64]) -> Vec { + let mut payload = Vec::with_capacity(F32_INPUT_BYTES as usize); + for v in values { + payload.extend_from_slice(&v.to_le_bytes()); + } + payload +} + +/// One raw v64bfp16ebs8: 64 mantissa bytes then 8 exponent bytes (layout pinned by P0). +fn craft(mantissa: &[i8; 64], exponent: &[u8; 8]) -> [u8; 72] { + let mut unit = [0u8; 72]; + for (i, m) in mantissa.iter().enumerate() { + unit[i] = *m as u8; + } + unit[64..72].copy_from_slice(exponent); + unit +} + +/// Reference MMUL under the transposed-B hypothesis: A is 8x32 (chunk k holds columns +/// 8k..8k+8, row-major i*8+c within the chunk), B is the 8x32 view of the transposed +/// 32x8 operand (same chunk/lane layout), C lane i*8+j = sum_k A[i,k] * B[j,k]. +/// All products are integers scaled by powers of two, so f64 accumulation is exact and +/// FP32 comparison is order-independent for the magnitudes the cases use. +fn p4_reference(a_planes: &[[u8; 72]; 4], b_planes: &[[u8; 72]; 4]) -> [f32; 64] { + let mut c = [0f64; 64]; + for chunk in 0..4 { + let a = &a_planes[chunk]; + let b = &b_planes[chunk]; + for i in 0..8 { + for j in 0..8 { + for lane in 0..8 { + let ea = a[64 + i.min(7)]; // per-block exponent: block = row index + let eb = b[64 + j.min(7)]; + let ma = f64::from(a[i * 8 + lane] as i8); + let mb = f64::from(b[j * 8 + lane] as i8); + if ma != 0.0 && mb != 0.0 { + c[i * 8 + j] += + ma * mb * (f64::from(ea) + f64::from(eb) - 266.0).exp2(); + } + } + } + } + } + let mut out = [0f32; 64]; + for (o, v) in out.iter_mut().zip(c) { + *o = v as f32; + } + out +} + +#[allow(clippy::too_many_arguments)] +fn mm_run( + backend: &A, + queue: &A::Queue, + program: &A::Program, + input: &mut A::Buffer, + output: &A::Buffer, + a: &[[u8; 72]; 4], + b: &[[u8; 72]; 4], +) -> [f32; 64] { + let mut payload = Vec::with_capacity(576); + for chunk in a { + payload.extend_from_slice(chunk); + } + for chunk in b { + payload.extend_from_slice(chunk); + } + let raw = submit_case( + backend, + queue, + program, + input, + output, + P4_INPUT_BYTES, + P4_OUTPUT_BYTES, + &payload, + ); + let mut lanes = [0f32; 64]; + for (i, lane) in lanes.iter_mut().enumerate() { + *lane = f32::from_le_bytes(raw[i * 4..i * 4 + 4].try_into().unwrap()); + } + lanes +} + +#[allow(clippy::too_many_arguments)] +fn mm_check( + backend: &A, + queue: &A::Queue, + program: &A::Program, + input: &mut A::Buffer, + output: &A::Buffer, + name: &str, + a: &[[u8; 72]; 4], + b: &[[u8; 72]; 4], +) { + let got = mm_run(backend, queue, program, input, output, a, b); + let expected = p4_reference(a, b); + let mismatches: Vec = (0..64) + .filter(|&i| got[i].to_bits() != expected[i].to_bits()) + .collect(); + if mismatches.is_empty() { + println!(" {name}: PASS (all 64 lanes bit-exact vs reference)"); + } else { + println!(" {name}: {} MISMATCHES", mismatches.len()); + for i in mismatches.iter().take(12) { + println!( + " lane {i} (i={},j={}): got {} ({:#010x}), expected {} ({:#010x})", + i / 8, + i % 8, + got[*i], + got[*i].to_bits(), + expected[*i], + expected[*i].to_bits() + ); + } + } +} + /// H1: value = mantissa * 2^(exponent - 127 - 6), two's-complement int8 mantissa. fn h1_decode(mantissa: i8, exponent: u8) -> f64 { f64::from(mantissa) * (f64::from(exponent) - 133.0).exp2() @@ -129,22 +249,20 @@ fn submit_case( program: &A::Program, input: &mut A::Buffer, output: &A::Buffer, + in_bytes: u64, out_bytes: u64, - values: &[f32; 64], + payload: &[u8], ) -> Vec { - let mut payload = Vec::with_capacity(INPUT_BYTES as usize); - for v in values { - payload.extend_from_slice(&v.to_le_bytes()); - } + assert_eq!(payload.len() as u64, in_bytes); backend - .write_buffer(input, 0, &Slice(&payload)) + .write_buffer(input, 0, &Slice(payload)) .expect("write input"); let bindings = [ BindingRef { slot: 0, buffer: input, - range: BufferRange::new(0, INPUT_BYTES).unwrap(), + range: BufferRange::new(0, in_bytes).unwrap(), access: AccessMode::Read, }, BindingRef { @@ -261,14 +379,15 @@ fn main() { let mut args = std::env::args().skip(1); let probe = args.next().expect("usage: runner "); let dir = args.next().expect("usage: runner "); - let out_bytes = match probe.as_str() { - "p0" | "p2" | "p3" => P0_OUTPUT_BYTES, - "p1" => P1_OUTPUT_BYTES, + let (in_bytes, out_bytes) = match probe.as_str() { + "p0" | "p2" | "p3" => (F32_INPUT_BYTES, P0_OUTPUT_BYTES), + "p1" => (F32_INPUT_BYTES, P1_OUTPUT_BYTES), + "p4" | "p5" => (P4_INPUT_BYTES, P4_OUTPUT_BYTES), other => panic!("unknown probe {other}"), }; let xclbin = std::fs::read(format!("{dir}/final.xclbin")).expect("read final.xclbin"); let insts = std::fs::read(format!("{dir}/insts.bin")).expect("read insts.bin"); - let container = artifact::encode("MLIR_AIE", &[INPUT_BYTES], &[out_bytes], &xclbin, &insts); + let container = artifact::encode("MLIR_AIE", &[in_bytes], &[out_bytes], &xclbin, &insts); let backend = XdnaAccelerator::new().expect("construct XDNA backend (needs the NPU + HRX)"); let context = backend @@ -293,7 +412,7 @@ fn main() { .allocate_buffer( &context, BufferDesc::new( - INPUT_BYTES, + in_bytes, 4096, MemoryDomain::Shared, BufferUsage::TRANSFER_DESTINATION | BufferUsage::PROGRAM_INPUT, @@ -343,8 +462,16 @@ fn main() { // Case D: all zeros. let d = [0f32; 64]; for (name, values) in [("A", &a), ("B", &b), ("C", &c), ("D", &d)] { - let raw = - submit_case(&backend, &queue, &program, &mut input, &output, out_bytes, values); + let raw = submit_case( + &backend, + &queue, + &program, + &mut input, + &output, + in_bytes, + out_bytes, + &f32_payload(values), + ); decode_p0(name, values, &raw); } } @@ -386,7 +513,16 @@ fn main() { 0.5, -0.5, ]); - let raw = submit_case(&backend, &queue, &program, &mut input, &output, out_bytes, &t); + let raw = submit_case( + &backend, + &queue, + &program, + &mut input, + &output, + in_bytes, + out_bytes, + &f32_payload(&t), + ); decode_p1("ties", &t, &raw); // Saturation-by-rounding: exact x = +-127.5 at e = 127. A mode that rounds the @@ -403,8 +539,16 @@ fn main() { 0.5, -0.5, ]); - let raw = - submit_case(&backend, &queue, &program, &mut input, &output, out_bytes, &sat); + let raw = submit_case( + &backend, + &queue, + &program, + &mut input, + &output, + in_bytes, + out_bytes, + &f32_payload(&sat), + ); decode_p1("sat", &sat, &raw); } "p2" => { @@ -449,8 +593,16 @@ fn main() { 0.0, ]); for (name, values) in [("N1", &n1), ("N2", &n2), ("N3", &n3), ("N4", &n4)] { - let raw = - submit_case(&backend, &queue, &program, &mut input, &output, out_bytes, values); + let raw = submit_case( + &backend, + &queue, + &program, + &mut input, + &output, + in_bytes, + out_bytes, + &f32_payload(values), + ); decode_p0(name, values, &raw); } } @@ -475,11 +627,189 @@ fn main() { x2[17] = f32::from_bits(0x7f7f_ffff); // f32::MAX x2[24] = -0.0; for (name, values) in [("X1", &x1), ("X2", &x2)] { - let raw = - submit_case(&backend, &queue, &program, &mut input, &output, out_bytes, values); + let raw = submit_case( + &backend, + &queue, + &program, + &mut input, + &output, + in_bytes, + out_bytes, + &f32_payload(values), + ); decode_p0(name, values, &raw); } } + "p4" | "p5" => { + let zero = craft(&[0i8; 64], &[0u8; 8]); + + if probe == "p4" { + println!("== P4 layout inference (single-entry A, graded B, chunk 0 only)"); + // B lane q holds mantissa q+1 at e=127 in every block. + let mut mb = [0i8; 64]; + for (q, m) in mb.iter_mut().enumerate() { + *m = (q + 1) as i8; + } + let b0 = craft(&mb, &[127u8; 8]); + let b = [b0, zero, zero, zero]; + for p in [0usize, 1, 8, 9] { + let mut ma = [0i8; 64]; + ma[p] = 64; + let a = [craft(&ma, &[127u8; 8]), zero, zero, zero]; + let lanes = + mm_run(&backend, &queue, &program, &mut input, &output, &a, &b); + let nonzero: Vec = (0..64) + .filter(|&i| lanes[i] != 0.0) + .map(|i| format!("C[{},{}]={}", i / 8, i % 8, lanes[i])) + .collect(); + println!(" A[{p}]=1.0: {}", nonzero.join(" ")); + } + + println!("== P4 exactness under the inferred layout"); + // Deterministic pseudo-random mantissas including -128 and +/-127 corners. + let mut ma = [0i8; 64]; + let mut mb2 = [0i8; 64]; + let mut state = 0x12345678u32; + let mut next = move || { + state = state.wrapping_mul(1664525).wrapping_add(1013904223); + (state >> 24) as u8 as i8 + }; + for i in 0..64 { + ma[i] = next(); + mb2[i] = next(); + } + ma[0] = -128; + ma[1] = 127; + mb2[0] = 127; + mb2[1] = -128; + let a = [ + craft(&ma, &[127u8; 8]), + craft(&mb2, &[127u8; 8]), + craft(&ma, &[126u8; 8]), + craft(&mb2, &[125u8; 8]), + ]; + let b = [ + craft(&mb2, &[127u8; 8]), + craft(&ma, &[127u8; 8]), + craft(&mb2, &[128u8; 8]), + craft(&ma, &[124u8; 8]), + ]; + mm_check( + &backend, + &queue, + &program, + &mut input, + &output, + "uniform + per-chunk exponents (incl. m = -128)", + &a, + &b, + ); + + println!("== P4 per-block exponent disagreement inside one chunk"); + let ea: [u8; 8] = [120, 121, 122, 123, 124, 125, 126, 127]; + let eb: [u8; 8] = [127, 126, 125, 124, 123, 122, 121, 120]; + let a = [craft(&ma, &ea), zero, zero, zero]; + let b = [craft(&mb2, &eb), zero, zero, zero]; + mm_check( + &backend, + &queue, + &program, + &mut input, + &output, + "disagreeing block exponents", + &a, + &b, + ); + } else { + println!("== P5 MXINT8 block-32 decomposition (H6)"); + // Two MXINT8-32 operand rows: 32 int8 elements, ONE shared E8M0 scale each. + // Decomposed to four block-8 groups with EQUAL exponent bytes = the MX scale. + let mut state = 0x5eed_cafeu32; + let mut next = move || { + state = state.wrapping_mul(1664525).wrapping_add(1013904223); + (state >> 24) as u8 as i8 + }; + // A: rows i share mantissa pattern; every chunk gets the SAME exponent (MX-32). + let mut ma = [[0i8; 64]; 4]; + let mut mb = [[0i8; 64]; 4]; + for chunk in 0..4 { + for lane in 0..64 { + ma[chunk][lane] = next(); + mb[chunk][lane] = next(); + } + } + // MX scales: A row scale e=127, B row scale e=125 (both uniform across chunks). + let a = [ + craft(&ma[0], &[127u8; 8]), + craft(&ma[1], &[127u8; 8]), + craft(&ma[2], &[127u8; 8]), + craft(&ma[3], &[127u8; 8]), + ]; + let b = [ + craft(&mb[0], &[125u8; 8]), + craft(&mb[1], &[125u8; 8]), + craft(&mb[2], &[125u8; 8]), + craft(&mb[3], &[125u8; 8]), + ]; + mm_check( + &backend, + &queue, + &program, + &mut input, + &output, + "MXINT8 32-dot as four equal-exponent block-8 groups", + &a, + &b, + ); + + // The mapping-direction evidence: the same numeric values expressed with + // per-sub-block exponents (block-8 native form, more precision available). + // Halving mantissas at e+1 must reproduce identical products. + let mut ma_half = [[0i8; 64]; 4]; + for chunk in 0..4 { + for lane in 0..64 { + // keep only even mantissas so m/2 at e+1 is exact + ma_half[chunk][lane] = ma[chunk][lane] & !1; + } + } + let a_even = [ + craft(&ma_half[0], &[127u8; 8]), + craft(&ma_half[1], &[127u8; 8]), + craft(&ma_half[2], &[127u8; 8]), + craft(&ma_half[3], &[127u8; 8]), + ]; + let mut ma_shift = [[0i8; 64]; 4]; + for chunk in 0..4 { + for lane in 0..64 { + ma_shift[chunk][lane] = ma_half[chunk][lane] / 2; + } + } + let a_shift = [ + craft(&ma_shift[0], &[128u8; 8]), + craft(&ma_shift[1], &[128u8; 8]), + craft(&ma_shift[2], &[128u8; 8]), + craft(&ma_shift[3], &[128u8; 8]), + ]; + let c_even = mm_run(&backend, &queue, &program, &mut input, &output, &a_even, &b); + let c_shift = + mm_run(&backend, &queue, &program, &mut input, &output, &a_shift, &b); + let same = (0..64).all(|i| c_even[i].to_bits() == c_shift[i].to_bits()); + println!( + " same values via (m, e) vs (m/2, e+1): {}", + if same { "BIT-IDENTICAL" } else { "DIFFER" } + ); + mm_check( + &backend, + &queue, + &program, + &mut input, + &output, + "equal-exponent form vs reference", + &a_even, + &b, + ); + } + } _ => unreachable!(), } From 3935c81ee4100e829b2bb37405ae80591f3267af Mon Sep 17 00:00:00 2001 From: Aryan Ravishankar Date: Thu, 27 Aug 2026 19:43:41 -0500 Subject: [PATCH 05/10] Strip trailing whitespace from probe output (repo whitespace gate) Co-Authored-By: Claude Opus 5 --- research/bfp16ebs8/results/p0-2026-08-27.txt | 64 ++++++++++---------- research/bfp16ebs8/results/p2-2026-08-27.txt | 64 ++++++++++---------- research/bfp16ebs8/results/p3-2026-08-27.txt | 32 +++++----- research/bfp16ebs8/runner/src/main.rs | 2 +- 4 files changed, 81 insertions(+), 81 deletions(-) diff --git a/research/bfp16ebs8/results/p0-2026-08-27.txt b/research/bfp16ebs8/results/p0-2026-08-27.txt index f593efe..466411a 100644 --- a/research/bfp16ebs8/results/p0-2026-08-27.txt +++ b/research/bfp16ebs8/results/p0-2026-08-27.txt @@ -5,46 +5,46 @@ HRX: hrx-amdxdna-2026.07.30-amdxdna-hal-native-rel-eb0b39f-linux-x86_64 == case A (rnd mode at entry: 0) exponent plane: [7c, 7d, 7e, 7f, 80, 81, 82, 83] - block 0 (e=124): 0.125->m=64 0.125->m=64 0.125->m=64 0.125->m=64 0.125->m=64 0.125->m=64 0.125->m=64 0.125->m=64 - block 1 (e=125): 0.25->m=64 0.25->m=64 0.25->m=64 0.25->m=64 0.25->m=64 0.25->m=64 0.25->m=64 0.25->m=64 - block 2 (e=126): 0.5->m=64 0.5->m=64 0.5->m=64 0.5->m=64 0.5->m=64 0.5->m=64 0.5->m=64 0.5->m=64 - block 3 (e=127): 1->m=64 1->m=64 1->m=64 1->m=64 1->m=64 1->m=64 1->m=64 1->m=64 - block 4 (e=128): 2->m=64 2->m=64 2->m=64 2->m=64 2->m=64 2->m=64 2->m=64 2->m=64 - block 5 (e=129): 4->m=64 4->m=64 4->m=64 4->m=64 4->m=64 4->m=64 4->m=64 4->m=64 - block 6 (e=130): 8->m=64 8->m=64 8->m=64 8->m=64 8->m=64 8->m=64 8->m=64 8->m=64 - block 7 (e=131): 16->m=64 16->m=64 16->m=64 16->m=64 16->m=64 16->m=64 16->m=64 16->m=64 + block 0 (e=124): 0.125->m=64 0.125->m=64 0.125->m=64 0.125->m=64 0.125->m=64 0.125->m=64 0.125->m=64 0.125->m=64 + block 1 (e=125): 0.25->m=64 0.25->m=64 0.25->m=64 0.25->m=64 0.25->m=64 0.25->m=64 0.25->m=64 0.25->m=64 + block 2 (e=126): 0.5->m=64 0.5->m=64 0.5->m=64 0.5->m=64 0.5->m=64 0.5->m=64 0.5->m=64 0.5->m=64 + block 3 (e=127): 1->m=64 1->m=64 1->m=64 1->m=64 1->m=64 1->m=64 1->m=64 1->m=64 + block 4 (e=128): 2->m=64 2->m=64 2->m=64 2->m=64 2->m=64 2->m=64 2->m=64 2->m=64 + block 5 (e=129): 4->m=64 4->m=64 4->m=64 4->m=64 4->m=64 4->m=64 4->m=64 4->m=64 + block 6 (e=130): 8->m=64 8->m=64 8->m=64 8->m=64 8->m=64 8->m=64 8->m=64 8->m=64 + block 7 (e=131): 16->m=64 16->m=64 16->m=64 16->m=64 16->m=64 16->m=64 16->m=64 16->m=64 native struct layout: mantissa-then-exponent=true exponent-then-mantissa=false == case B (rnd mode at entry: 0) exponent plane: [80, 00, 00, 00, 00, 00, 00, 00] - block 0 (e=128): 1->m=32 -1->m=-32 0.5->m=16 -0.5->m=-16 1.5->m=48 -1.5->m=-48 1.984375->m=63! -2->m=-64 - block 1 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 2 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 3 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 0 (e=128): 1->m=32 -1->m=-32 0.5->m=16 -0.5->m=-16 1.5->m=48 -1.5->m=-48 1.984375->m=63! -2->m=-64 + block 1 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 2 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 3 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 native struct layout: mantissa-then-exponent=true exponent-then-mantissa=false == case C (rnd mode at entry: 0) exponent plane: [7f, 00, 00, 00, 00, 00, 00, 00] - block 0 (e=127): 1->m=64 0.015625->m=1 0.0078125->m=0! 0.0234375->m=1! -0.015625->m=-1 0->m=0 1.015625->m=65 -1.015625->m=-65 - block 1 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 2 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 3 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 0 (e=127): 1->m=64 0.015625->m=1 0.0078125->m=0! 0.0234375->m=1! -0.015625->m=-1 0->m=0 1.015625->m=65 -1.015625->m=-65 + block 1 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 2 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 3 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 native struct layout: mantissa-then-exponent=true exponent-then-mantissa=false == case D (rnd mode at entry: 0) exponent plane: [00, 00, 00, 00, 00, 00, 00, 00] - block 0 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 1 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 2 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 3 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 0 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 1 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 2 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 3 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 native struct layout: mantissa-then-exponent=true exponent-then-mantissa=true P0 complete diff --git a/research/bfp16ebs8/results/p2-2026-08-27.txt b/research/bfp16ebs8/results/p2-2026-08-27.txt index deda098..a2f389d 100644 --- a/research/bfp16ebs8/results/p2-2026-08-27.txt +++ b/research/bfp16ebs8/results/p2-2026-08-27.txt @@ -3,46 +3,46 @@ host: 7.1.8-200.fc44.x86_64, NPU 1022:17f0 rev 0x20, toolchain amdxdna-hrx-v2026 == case N1 (rnd mode at entry: 0) exponent plane: [7f, 00, 00, 00, 00, 00, 00, 00] - block 0 (e=127): 1->m=64 0.0078125->m=0! 0.00012207031->m=0! 0.000061035156->m=0! -0.00012207031->m=-1! 0.0000009536743->m=0! 0->m=0 1->m=64 - block 1 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 2 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 3 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 0 (e=127): 1->m=64 0.0078125->m=0! 0.00012207031->m=0! 0.000061035156->m=0! -0.00012207031->m=-1! 0.0000009536743->m=0! 0->m=0 1->m=64 + block 1 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 2 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 3 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 native struct layout: mantissa-then-exponent=true exponent-then-mantissa=false == case N2 (rnd mode at entry: 0) exponent plane: [7f, 00, 00, 00, 00, 00, 00, 00] - block 0 (e=127): -1->m=-64 -0.5->m=-32 -0.25->m=-16 -0.75->m=-48 -1.25->m=-80 -1.75->m=-112 -1.984375->m=-127 -0.125->m=-8 - block 1 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 2 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 3 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 0 (e=127): -1->m=-64 -0.5->m=-32 -0.25->m=-16 -0.75->m=-48 -1.25->m=-80 -1.75->m=-112 -1.984375->m=-127 -0.125->m=-8 + block 1 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 2 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 3 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 native struct layout: mantissa-then-exponent=true exponent-then-mantissa=false == case N3 (rnd mode at entry: 0) exponent plane: [00, 00, 00, 00, 00, 00, 00, 00] - block 0 (e= 0): 0.000000000000000000000000000000000000000000001->m=0 0.000000000000000000000000000000000000011754942->m=0 0.000000000000000000000000000000000000005877472->m=0 -0.000000000000000000000000000000000000005877472->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 1 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 2 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 3 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 0 (e= 0): 0.000000000000000000000000000000000000000000001->m=0 0.000000000000000000000000000000000000011754942->m=0 0.000000000000000000000000000000000000005877472->m=0 -0.000000000000000000000000000000000000005877472->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 1 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 2 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 3 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 native struct layout: mantissa-then-exponent=true exponent-then-mantissa=true == case N4 (rnd mode at entry: 0) exponent plane: [fe, 00, 00, 00, 00, 00, 00, 00] - block 0 (e=254): 340282350000000000000000000000000000000->m=127! 170141170000000000000000000000000000000->m=63! 1->m=0! -1->m=-1! 1329228000000000000000000000000000000->m=0! -1329228000000000000000000000000000000->m=-1! 0->m=0 0->m=0 - block 1 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 2 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 3 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 0 (e=254): 340282350000000000000000000000000000000->m=127! 170141170000000000000000000000000000000->m=63! 1->m=0! -1->m=-1! 1329228000000000000000000000000000000->m=0! -1329228000000000000000000000000000000->m=-1! 0->m=0 0->m=0 + block 1 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 2 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 3 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 native struct layout: mantissa-then-exponent=true exponent-then-mantissa=false p2 complete diff --git a/research/bfp16ebs8/results/p3-2026-08-27.txt b/research/bfp16ebs8/results/p3-2026-08-27.txt index 2d78e00..9404f80 100644 --- a/research/bfp16ebs8/results/p3-2026-08-27.txt +++ b/research/bfp16ebs8/results/p3-2026-08-27.txt @@ -3,24 +3,24 @@ host: 7.1.8-200.fc44.x86_64, NPU 1022:17f0 rev 0x20, toolchain amdxdna-hrx-v2026 == case X1 (rnd mode at entry: 0) exponent plane: [ff, 00, 00, 00, 00, 00, 00, 00] - block 0 (e=255): inf->m=64! -inf->m=-64! NaN->m=96! NaN->m=-96! 1->m=0! -1->m=-1! 0->m=0 -0->m=0 - block 1 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 2 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 3 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 0 (e=255): inf->m=64! -inf->m=-64! NaN->m=96! NaN->m=-96! 1->m=0! -1->m=-1! 0->m=0 -0->m=0 + block 1 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 2 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 3 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 native struct layout: mantissa-then-exponent=true exponent-then-mantissa=false == case X2 (rnd mode at entry: 0) exponent plane: [ff, ff, fe, 00, 00, 00, 00, 00] - block 0 (e=255): inf->m=64! 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 1 (e=255): NaN->m=96! 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 2 (e=254): 1->m=0! 340282350000000000000000000000000000000->m=127! 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 3 (e= 0): -0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 - block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 0 (e=255): inf->m=64! 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 1 (e=255): NaN->m=96! 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 2 (e=254): 1->m=0! 340282350000000000000000000000000000000->m=127! 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 3 (e= 0): -0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 native struct layout: mantissa-then-exponent=true exponent-then-mantissa=false p3 complete diff --git a/research/bfp16ebs8/runner/src/main.rs b/research/bfp16ebs8/runner/src/main.rs index 03ef150..b0ceda2 100644 --- a/research/bfp16ebs8/runner/src/main.rs +++ b/research/bfp16ebs8/runner/src/main.rs @@ -320,7 +320,7 @@ fn decode_p0(name: &str, values: &[f32; 64], raw: &[u8]) { } else { '!' }; - print!("{input_value}->m={m}{mark} "); + print!(" {input_value}->m={m}{mark}"); } println!(); } From ebf234d36d0f2280aa3dc2ab5343aa85f52aa00d Mon Sep 17 00:00:00 2001 From: Aryan Ravishankar Date: Thu, 27 Aug 2026 19:53:22 -0500 Subject: [PATCH 06/10] Deliver the bit-level bfp16ebs8 reference model, cross-validated on silicon (#146) model.rs formalizes the characterization as two independent formulations: encode_v64 models the hardware converter exactly as P0-P3 observed it (max-exponent-field selection, subnormal flush, structural Inf/NaN, all ten rounding functions, post-rounding renormalization), and mxint8_quantize_block implements OCP MX v1.0 quantization from the spec. Unit tests replay the recorded silicon planes as fixtures, so the model is verifiable without an NPU; the probe runner additionally asserts live, per case, that silicon planes are bit-identical to the model -- eleven cases including a 64-element pseudorandom sweep, all passing. Two corrections the model surfaced: m = -128 is never a normalization target but floor-rounding a near-max negative member does emit it; and at exact mantissa +-127.5 an up-rounding hardware conversion renormalizes while the OCP procedure saturates, so an MXINT8 tier must quantize with the OCP procedure at that boundary. Both are recorded in the note, which now marks the issue #146 exit criteria met. Co-Authored-By: Claude Opus 5 --- .../amdxdna-bfp16ebs8-characterization.md | 40 +- research/bfp16ebs8/results/p0-2026-08-28.txt | 64 ++++ research/bfp16ebs8/results/p2-2026-08-28.txt | 52 +++ research/bfp16ebs8/results/p3-2026-08-28.txt | 28 ++ research/bfp16ebs8/runner/src/main.rs | 24 +- research/bfp16ebs8/runner/src/model.rs | 349 ++++++++++++++++++ 6 files changed, 546 insertions(+), 11 deletions(-) create mode 100644 research/bfp16ebs8/results/p0-2026-08-28.txt create mode 100644 research/bfp16ebs8/results/p2-2026-08-28.txt create mode 100644 research/bfp16ebs8/results/p3-2026-08-28.txt create mode 100644 research/bfp16ebs8/runner/src/model.rs diff --git a/docs/research/amdxdna-bfp16ebs8-characterization.md b/docs/research/amdxdna-bfp16ebs8-characterization.md index c2ac095..aaaa902 100644 --- a/docs/research/amdxdna-bfp16ebs8-characterization.md +++ b/docs/research/amdxdna-bfp16ebs8-characterization.md @@ -1,7 +1,12 @@ # AMD XDNA2 `bfp16ebs8` characterization (issue #146) -Status: **probes complete** — toolchain surface frozen 2026-08-27; probes P0–P5 run on silicon -2026-08-27 (results in §6). Verdict: **exact MX mapping, with three stated conditions** (§6a). +Status: **complete** — toolchain surface frozen 2026-08-27; probes P0–P5 run on silicon +2026-08-27/28 (results in §6); bit-level reference model delivered and cross-validated against +silicon (§4). Verdict: **exact MX mapping, with three stated conditions** (§6a). This meets the +issue #146 exit criteria: the format is described by a cited note and a host-verifiable +reference model, on-metal tests distinguish exact matches from mismatches, and the +exact-MX-mapping outcome is chosen explicitly. No public `Target`, extension bit, capability +row, or protocol value was added. Part of #110. Companion to [tosa-int8-block-fp-status.md](tosa-int8-block-fp-status.md) and the forward spec `docs/research/amdxdna-blockfp-tier.md` (branch `grilling/first-numerical-tier`). @@ -140,15 +145,28 @@ owns). Every probe records the toolchain identity table above plus driver/firmware versions at run time, and preserves its artifact and raw output vectors under `research/` for repeatability. -## 4. Reference model +## 4. Reference model (delivered) -A host-side Rust model, kept in the standalone probe project (not in any published crate): +`research/bfp16ebs8/runner/src/model.rs` holds the bit-level model, host-verifiable without an +NPU (`cargo test` in the runner project replays the recorded silicon planes as fixtures): -- `bfp16ebs8` codec parameterized by the H1/H2 candidates until P0 pins them, then frozen; -- OCP MX v1.0 MXINT8 quantizer/dequantizer (block-32, E8M0 scale, round-to-nearest-even) as the - independent oracle for P5 — deliberately a *separate formulation* from the codec, following the - two-independent-formulations pattern the RESCALE tier uses; -- exact FP32 dot-product references (`f32` accumulation in kernel order) for P4/P5. +- `encode_block`/`encode_v64`: the hardware converter exactly as P0–P3 observed it — max-member + IEEE exponent-field selection, subnormal flush, structural Inf/NaN at `e = 255`, all ten + `crrnd` rounding functions, post-rounding renormalization; +- `mxint8_quantize_block`: OCP MX v1.0 MXINT8 quantization implemented from the spec (RNE, + saturating) as the deliberately independent second formulation; +- `decode` and `dot_reference` for exact value-level comparisons. + +The probe runner cross-validates live: every P0/P2/P3 case asserts the silicon planes are +bit-identical to `encode_v64`, including a 64-element pseudorandom case +(`results/p0-2026-08-28.txt`, `p2-…`, `p3-…`: eleven cases, all `model check: PASS`). + +One divergence between the two formulations is real and documented in the model +(`CONVERTER_OCP_OVERFLOW_DIVERGENCE`): at an exact mantissa of ±127.5, an up-rounding hardware +conversion renormalizes to the next exponent while the OCP quantization procedure saturates the +element without re-selecting the scale. Both are self-consistent; a tier claiming MXINT8 +semantics must therefore quantize with the OCP procedure rather than the hardware converter +whenever inputs can sit on that boundary. ## 5. Feed into #145 (draft tracking) @@ -192,7 +210,9 @@ maximum member's *IEEE FP32 exponent* — not the smallest exponent that fits th Case B (max member `−2.0`) chose `e = 128` with `m = −64`, although `e = 127` with `m = −128` is representable and would have kept a full extra bit for every other member (it would have made `127/64` exact). The conversion therefore normalizes the largest-magnitude member into -`|m| ∈ [64, 127]` (exactly 64 for powers of two) and never emits `m = −128`. Mixed-magnitude +`|m| ∈ [64, 127]` (exactly 64 for powers of two); `m = −128` is never a *normalization target*, +though rounding a near-max negative member can still produce it (P1 `sat`: `−127.5` under floor +encodes as `m = −128` at the unbumped exponent). Mixed-magnitude members are quantized at that exponent and lose low bits under the active rounding mode (case C: at `e = 127`, `1/128 → m = 0`, `1.5/64 → m = 1`, while `1 ± 1/64 → m = ±65` exactly). This matches OCP MX v1.0's scale rule (`X = 2^(floor(log2(max|v|)) − emax_elem)` with diff --git a/research/bfp16ebs8/results/p0-2026-08-28.txt b/research/bfp16ebs8/results/p0-2026-08-28.txt new file mode 100644 index 0000000..0ac4571 --- /dev/null +++ b/research/bfp16ebs8/results/p0-2026-08-28.txt @@ -0,0 +1,64 @@ +P0 run 2026-08-28T00:52:52Z (with model cross-check) +host: 7.1.8-200.fc44.x86_64, NPU 1022:17f0 rev 0x20, toolchain amdxdna-hrx-v2026.08 (mlir-aie 1.4.1, llvm-aie 21.0.0.2026080301+c9c5ecb7), HRX hrx-amdxdna-2026.07.30-amdxdna-hal-native-rel-eb0b39f-linux-x86_64 + +== case A (rnd mode at entry: 0) + exponent plane: [7c, 7d, 7e, 7f, 80, 81, 82, 83] + block 0 (e=124): 0.125->m=64 0.125->m=64 0.125->m=64 0.125->m=64 0.125->m=64 0.125->m=64 0.125->m=64 0.125->m=64 + block 1 (e=125): 0.25->m=64 0.25->m=64 0.25->m=64 0.25->m=64 0.25->m=64 0.25->m=64 0.25->m=64 0.25->m=64 + block 2 (e=126): 0.5->m=64 0.5->m=64 0.5->m=64 0.5->m=64 0.5->m=64 0.5->m=64 0.5->m=64 0.5->m=64 + block 3 (e=127): 1->m=64 1->m=64 1->m=64 1->m=64 1->m=64 1->m=64 1->m=64 1->m=64 + block 4 (e=128): 2->m=64 2->m=64 2->m=64 2->m=64 2->m=64 2->m=64 2->m=64 2->m=64 + block 5 (e=129): 4->m=64 4->m=64 4->m=64 4->m=64 4->m=64 4->m=64 4->m=64 4->m=64 + block 6 (e=130): 8->m=64 8->m=64 8->m=64 8->m=64 8->m=64 8->m=64 8->m=64 8->m=64 + block 7 (e=131): 16->m=64 16->m=64 16->m=64 16->m=64 16->m=64 16->m=64 16->m=64 16->m=64 + model check: PASS (planes bit-identical to model::encode_v64) + native struct layout: mantissa-then-exponent=true exponent-then-mantissa=false +== case B (rnd mode at entry: 0) + exponent plane: [80, 00, 00, 00, 00, 00, 00, 00] + block 0 (e=128): 1->m=32 -1->m=-32 0.5->m=16 -0.5->m=-16 1.5->m=48 -1.5->m=-48 1.984375->m=63! -2->m=-64 + block 1 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 2 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 3 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + model check: PASS (planes bit-identical to model::encode_v64) + native struct layout: mantissa-then-exponent=true exponent-then-mantissa=false +== case C (rnd mode at entry: 0) + exponent plane: [7f, 00, 00, 00, 00, 00, 00, 00] + block 0 (e=127): 1->m=64 0.015625->m=1 0.0078125->m=0! 0.0234375->m=1! -0.015625->m=-1 0->m=0 1.015625->m=65 -1.015625->m=-65 + block 1 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 2 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 3 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + model check: PASS (planes bit-identical to model::encode_v64) + native struct layout: mantissa-then-exponent=true exponent-then-mantissa=false +== case D (rnd mode at entry: 0) + exponent plane: [00, 00, 00, 00, 00, 00, 00, 00] + block 0 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 1 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 2 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 3 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + model check: PASS (planes bit-identical to model::encode_v64) + native struct layout: mantissa-then-exponent=true exponent-then-mantissa=true +== case E (rnd mode at entry: 0) + exponent plane: [86, 86, 86, 86, 86, 85, 86, 86] + block 0 (e=134): -114.77344->m=-58! 194.64453->m=97! -255.21875->m=-128! 184.04297->m=92! -92.30469->m=-47! 52.14453->m=26! -235.44922->m=-118! 24.363281->m=12! + block 1 (e=134): -65.953125->m=-33! 192.67969->m=96! -38.64453->m=-20! 164.78906->m=82! -128.48828->m=-65! 54.20703->m=27! -94.20703->m=-48! 3.4023438->m=1! + block 2 (e=134): -126.921875->m=-64! 103.92578->m=51! -3.765625->m=-2! 81.52734->m=40! -50.51953->m=-26! 226.28906->m=113! -113.21875->m=-57! 247.7461->m=123! + block 3 (e=134): -226.79688->m=-114! 223.0039->m=111! -76.703125->m=-39! 189.8789->m=94! -45.51953->m=-23! 121.015625->m=60! -44.609375->m=-23! 31.011719->m=15! + block 4 (e=134): -74.70703->m=-38! 32.535156->m=16! -123.578125->m=-62! 221.46875->m=110! -144.6211->m=-73! 79.015625->m=39! -156.5039->m=-79! 214.82813->m=107! + block 5 (e=133): -55.777344->m=-56! 123.15234->m=123! -78.52344->m=-79! 23.921875->m=23! -94.94531->m=-95! 76.90625->m=76! -81.02734->m=-82! 48.82422->m=48! + block 6 (e=134): -79.13672->m=-40! 17.476563->m=8! -71.65625->m=-36! 200.86328->m=100! -255.61719->m=-128! 111.31641->m=55! -222.30469->m=-112! 114.61719->m=57! + block 7 (e=134): -217.90234->m=-109! 90.12891->m=45! -45.109375->m=-23! 191.91797->m=95! -189.75781->m=-95! 70.87109->m=35! -92.46094->m=-47! 149.83984->m=74! + model check: PASS (planes bit-identical to model::encode_v64) + native struct layout: mantissa-then-exponent=true exponent-then-mantissa=false +p0 complete diff --git a/research/bfp16ebs8/results/p2-2026-08-28.txt b/research/bfp16ebs8/results/p2-2026-08-28.txt new file mode 100644 index 0000000..2ef389b --- /dev/null +++ b/research/bfp16ebs8/results/p2-2026-08-28.txt @@ -0,0 +1,52 @@ +P2 run 2026-08-28T00:52:52Z (with model cross-check) +host: 7.1.8-200.fc44.x86_64, NPU 1022:17f0 rev 0x20, toolchain amdxdna-hrx-v2026.08 (mlir-aie 1.4.1, llvm-aie 21.0.0.2026080301+c9c5ecb7), HRX hrx-amdxdna-2026.07.30-amdxdna-hal-native-rel-eb0b39f-linux-x86_64 + +== case N1 (rnd mode at entry: 0) + exponent plane: [7f, 00, 00, 00, 00, 00, 00, 00] + block 0 (e=127): 1->m=64 0.0078125->m=0! 0.00012207031->m=0! 0.000061035156->m=0! -0.00012207031->m=-1! 0.0000009536743->m=0! 0->m=0 1->m=64 + block 1 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 2 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 3 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + model check: PASS (planes bit-identical to model::encode_v64) + native struct layout: mantissa-then-exponent=true exponent-then-mantissa=false +== case N2 (rnd mode at entry: 0) + exponent plane: [7f, 00, 00, 00, 00, 00, 00, 00] + block 0 (e=127): -1->m=-64 -0.5->m=-32 -0.25->m=-16 -0.75->m=-48 -1.25->m=-80 -1.75->m=-112 -1.984375->m=-127 -0.125->m=-8 + block 1 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 2 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 3 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + model check: PASS (planes bit-identical to model::encode_v64) + native struct layout: mantissa-then-exponent=true exponent-then-mantissa=false +== case N3 (rnd mode at entry: 0) + exponent plane: [00, 00, 00, 00, 00, 00, 00, 00] + block 0 (e= 0): 0.000000000000000000000000000000000000000000001->m=0 0.000000000000000000000000000000000000011754942->m=0 0.000000000000000000000000000000000000005877472->m=0 -0.000000000000000000000000000000000000005877472->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 1 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 2 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 3 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + model check: PASS (planes bit-identical to model::encode_v64) + native struct layout: mantissa-then-exponent=true exponent-then-mantissa=true +== case N4 (rnd mode at entry: 0) + exponent plane: [fe, 00, 00, 00, 00, 00, 00, 00] + block 0 (e=254): 340282350000000000000000000000000000000->m=127! 170141170000000000000000000000000000000->m=63! 1->m=0! -1->m=-1! 1329228000000000000000000000000000000->m=0! -1329228000000000000000000000000000000->m=-1! 0->m=0 0->m=0 + block 1 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 2 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 3 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + model check: PASS (planes bit-identical to model::encode_v64) + native struct layout: mantissa-then-exponent=true exponent-then-mantissa=false +p2 complete diff --git a/research/bfp16ebs8/results/p3-2026-08-28.txt b/research/bfp16ebs8/results/p3-2026-08-28.txt new file mode 100644 index 0000000..6657c10 --- /dev/null +++ b/research/bfp16ebs8/results/p3-2026-08-28.txt @@ -0,0 +1,28 @@ +P3 run 2026-08-28T00:52:52Z (with model cross-check) +host: 7.1.8-200.fc44.x86_64, NPU 1022:17f0 rev 0x20, toolchain amdxdna-hrx-v2026.08 (mlir-aie 1.4.1, llvm-aie 21.0.0.2026080301+c9c5ecb7), HRX hrx-amdxdna-2026.07.30-amdxdna-hal-native-rel-eb0b39f-linux-x86_64 + +== case X1 (rnd mode at entry: 0) + exponent plane: [ff, 00, 00, 00, 00, 00, 00, 00] + block 0 (e=255): inf->m=64! -inf->m=-64! NaN->m=96! NaN->m=-96! 1->m=0! -1->m=-1! 0->m=0 -0->m=0 + block 1 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 2 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 3 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + model check: PASS (planes bit-identical to model::encode_v64) + native struct layout: mantissa-then-exponent=true exponent-then-mantissa=false +== case X2 (rnd mode at entry: 0) + exponent plane: [ff, ff, fe, 00, 00, 00, 00, 00] + block 0 (e=255): inf->m=64! 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 1 (e=255): NaN->m=96! 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 2 (e=254): 1->m=0! 340282350000000000000000000000000000000->m=127! 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 3 (e= 0): -0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 4 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 5 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 6 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + block 7 (e= 0): 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 0->m=0 + model check: PASS (planes bit-identical to model::encode_v64) + native struct layout: mantissa-then-exponent=true exponent-then-mantissa=false +p3 complete diff --git a/research/bfp16ebs8/runner/src/main.rs b/research/bfp16ebs8/runner/src/main.rs index b0ceda2..ece43fb 100644 --- a/research/bfp16ebs8/runner/src/main.rs +++ b/research/bfp16ebs8/runner/src/main.rs @@ -12,6 +12,8 @@ use virtio_accel_core::{ }; use virtio_accel_xdna::{XDNA_PRECOMPILED_FORMAT, XdnaAccelerator, artifact}; +mod model; + #[derive(Debug)] struct Slice<'a>(&'a [u8]); @@ -324,6 +326,17 @@ fn decode_p0(name: &str, values: &[f32; 64], raw: &[u8]) { } println!(); } + // Model cross-validation: the silicon planes must equal the reference model bit for bit. + let (model_m, model_e) = model::encode_v64(values, model::HARDWARE_DEFAULT_MODE); + let silicon_m: Vec = mantissa.iter().map(|&b| b as i8).collect(); + if silicon_m == model_m && exponent == model_e { + println!(" model check: PASS (planes bit-identical to model::encode_v64)"); + } else { + println!(" model check: MISMATCH"); + println!(" model e: {model_e:?}"); + println!(" model m: {model_m:?}"); + } + // Native-layout comparison: where do the register planes land in the stored struct? let plane_order = native[0..64] == raw[0..64] && native[64..72] == raw[64..72]; let exp_first = native[0..8] == raw[64..72] && native[8..72] == raw[0..64]; @@ -461,7 +474,16 @@ fn main() { ]); // Case D: all zeros. let d = [0f32; 64]; - for (name, values) in [("A", &a), ("B", &b), ("C", &c), ("D", &d)] { + // Case E: pseudorandom finite values — silicon planes must equal the model exactly. + let mut e_case = [0f32; 64]; + let mut state = 0x00c0_ffeeu32; + for v in e_case.iter_mut() { + state = state.wrapping_mul(1664525).wrapping_add(1013904223); + // Finite, mixed sign and magnitude across ~2^-8..2^8. + let mag = ((state >> 8) & 0xffff) as f32 / 256.0; + *v = if state & 1 == 0 { mag } else { -mag }; + } + for (name, values) in [("A", &a), ("B", &b), ("C", &c), ("D", &d), ("E", &e_case)] { let raw = submit_case( &backend, &queue, diff --git a/research/bfp16ebs8/runner/src/model.rs b/research/bfp16ebs8/runner/src/model.rs new file mode 100644 index 0000000..b3d1ca5 --- /dev/null +++ b/research/bfp16ebs8/runner/src/model.rs @@ -0,0 +1,349 @@ +//! Bit-level reference model of XDNA2 `bfp16ebs8`, as characterized on silicon (issue #146). +//! +//! Two deliberately independent formulations live here: +//! +//! - [`encode_block`] models the hardware converter (`to_v64bfp16ebs8`) exactly as probes +//! P0–P3 observed it: shared exponent from the max member's IEEE FP32 exponent field, +//! subnormal inputs flushed to zero, Inf/NaN passed through structurally at `e = 255`, +//! per-`crrnd`-mode rounding, and post-rounding renormalization when a mantissa overflows +//! +127. +//! - [`mxint8_quantize_block`] implements OCP MX v1.0 MXINT8 quantization (block-32, E8M0 +//! scale, round-to-nearest-even, saturating) from the spec, not from the hardware. +//! +//! The unit tests pin both against the raw silicon outputs recorded under +//! `research/bfp16ebs8/results/`, so the model is verifiable without an NPU. The one +//! deliberate divergence between the two formulations is documented at +//! [`CONVERTER_OCP_OVERFLOW_DIVERGENCE`]. + +/// Decode one element: `value = m · 2^(e − 127 − 6)`. +pub fn decode(mantissa: i8, exponent: u8) -> f64 { + f64::from(mantissa) * (f64::from(exponent) - 133.0).exp2() +} + +/// The silicon-observed rounding functions, by `crrnd` mode value (P1: all ten bit-exact). +pub fn round_mode(mode: u32, x: f64) -> f64 { + let half = x.abs().fract() == 0.5; + match mode { + 0 => x.floor(), + 1 => x.ceil(), + 2 => x.trunc(), + 3 => x.abs().ceil().copysign(x), + 8 => { + if half { + x.floor() + } else { + x.round() + } + } + 9 => { + if half { + x.ceil() + } else { + x.round() + } + } + 10 => { + if half { + x.trunc() + } else { + x.round() + } + } + 11 => x.round(), + 12 => { + if half { + let down = x.floor(); + if (down as i64) % 2 == 0 { down } else { x.ceil() } + } else { + x.round() + } + } + 13 => { + if half { + let down = x.floor(); + if (down as i64) % 2 != 0 { down } else { x.ceil() } + } else { + x.round() + } + } + _ => panic!("unknown crrnd mode {mode}"), + } +} + +/// The hardware converter's default mode at kernel entry (P0/P1: `rnd_floor`). +pub const HARDWARE_DEFAULT_MODE: u32 = 0; + +/// Round-to-nearest-even, the mode OCP MX v1.0 requires (`rnd_conv_even`). +pub const CONV_EVEN: u32 = 12; + +/// Where the hardware converter and the OCP quantizer deliberately part ways. +/// +/// At an exact mantissa of ±127.5 under a mode that rounds the magnitude up, the hardware +/// converter renormalizes: it bumps the shared exponent and re-quantizes the whole block +/// (P1 `sat` case). The OCP MX v1.0 quantization procedure instead saturates the element at +/// the int8 maximum without re-selecting the scale. Data quantized by the hardware converter +/// at that boundary therefore differs from OCP-quantized data by one representation (both +/// decode to valid values; the OCP form loses the boundary value's low bit). A tier claiming +/// MXINT8 semantics must quantize with the OCP procedure (host/guest side), not with the +/// hardware converter, whenever inputs can sit on that boundary. +pub const CONVERTER_OCP_OVERFLOW_DIVERGENCE: &str = "see doc comment"; + +/// One IEEE FP32 exponent field (0 = zero/subnormal, 255 = Inf/NaN). +fn exponent_field(value: f32) -> u8 { + ((value.to_bits() >> 23) & 0xff) as u8 +} + +/// Model of the hardware converter for one 8-element block under `mode`. +/// +/// Matches probes P0–P3 on every recorded case. Behavior for blocks whose exponent would be +/// bumped past 254 is outside the probed envelope and deliberately panics. +pub fn encode_block(values: &[f32; 8], mode: u32) -> ([i8; 8], u8) { + // Shared exponent: max member IEEE exponent field; subnormals flush (field 0 = zero). + let mut e = values.iter().map(|v| exponent_field(*v)).max().unwrap(); + if e == 0 { + return ([0i8; 8], 0); + } + + loop { + let mut out = [0i8; 8]; + let mut bumped = false; + for (lane, value) in values.iter().enumerate() { + if value.is_infinite() { + out[lane] = if *value > 0.0 { 64 } else { -64 }; + continue; + } + if value.is_nan() { + out[lane] = if value.is_sign_negative() { -96 } else { 96 }; + continue; + } + if exponent_field(*value) == 0 { + out[lane] = 0; // flush-to-zero, including -0.0 + continue; + } + let exact = f64::from(*value) * (133.0 - f64::from(e)).exp2(); + let rounded = round_mode(mode, exact); + if rounded > 127.0 { + bumped = true; + break; + } + assert!(rounded >= -128.0, "below int8: {rounded}"); + out[lane] = rounded as i8; + } + if !bumped { + return (out, e); + } + assert!(e < 254, "renormalization past e=254 is outside the probed envelope"); + e += 1; + } +} + +/// Model of the converter over a 64-element vector (8 independent blocks). +pub fn encode_v64(values: &[f32; 64], mode: u32) -> ([i8; 64], [u8; 8]) { + let mut mantissa = [0i8; 64]; + let mut exponent = [0u8; 8]; + for block in 0..8 { + let mut chunk = [0f32; 8]; + chunk.copy_from_slice(&values[block * 8..block * 8 + 8]); + let (m, e) = encode_block(&chunk, mode); + mantissa[block * 8..block * 8 + 8].copy_from_slice(&m); + exponent[block] = e; + } + (mantissa, exponent) +} + +/// OCP MX v1.0 MXINT8 quantization of one block: E8M0 shared scale, int8 elements with six +/// fraction bits, round-to-nearest-even, saturating (no scale re-selection on overflow). +/// Implemented from the spec as the independent oracle; `BLOCK` is 32 for standard MX. +pub fn mxint8_quantize_block(values: &[f32; BLOCK]) -> ([i8; BLOCK], u8) { + let max_abs = values.iter().fold(0f32, |a, v| a.max(v.abs())); + assert!(max_abs.is_finite(), "MXINT8 has no Inf/NaN representation"); + if max_abs == 0.0 { + return ([0i8; BLOCK], 127); // scale 2^0; all elements zero + } + let shared = max_abs.log2().floor() as i32; + let e = u8::try_from(127 + shared).expect("scale within E8M0 range"); + let mut out = [0i8; BLOCK]; + for (lane, value) in values.iter().enumerate() { + let exact = f64::from(*value) * (133.0 - f64::from(e)).exp2(); + let rounded = round_mode(CONV_EVEN, exact); + out[lane] = rounded.clamp(-128.0, 127.0) as i8; + } + (out, e) +} + +/// Exact dot product of two encoded operand rows (any equal length), in f64. +pub fn dot_reference(a_m: &[i8], a_e: &[u8], b_m: &[i8], b_e: &[u8], block: usize) -> f64 { + assert_eq!(a_m.len(), b_m.len()); + let mut sum = 0f64; + for lane in 0..a_m.len() { + let ea = a_e[lane / block]; + let eb = b_e[lane / block]; + sum += f64::from(a_m[lane]) + * f64::from(b_m[lane]) + * (f64::from(ea) + f64::from(eb) - 266.0).exp2(); + } + sum +} + +#[cfg(test)] +mod tests { + use super::*; + + /// P0 case A (results/p0-2026-08-27.txt): per-block powers of two. + #[test] + fn silicon_p0_case_a() { + let mut values = [0f32; 64]; + for block in 0..8 { + for lane in 0..8 { + values[block * 8 + lane] = (2f32).powi(block as i32 - 3); + } + } + let (m, e) = encode_v64(&values, HARDWARE_DEFAULT_MODE); + assert_eq!(e, [124, 125, 126, 127, 128, 129, 130, 131]); + assert!(m.iter().all(|&x| x == 64)); + } + + /// P0 case B: sign and range, max member -2.0 selects e = 128. + #[test] + fn silicon_p0_case_b() { + let mut values = [0f32; 64]; + values[..8].copy_from_slice(&[1.0, -1.0, 0.5, -0.5, 1.5, -1.5, 127.0 / 64.0, -2.0]); + let (m, e) = encode_v64(&values, HARDWARE_DEFAULT_MODE); + assert_eq!(e[0], 128); + assert_eq!(&m[..8], &[32, -32, 16, -16, 48, -48, 63, -64]); + assert_eq!(&e[1..], &[0; 7]); + } + + /// P0 case C: mixed magnitudes at e = 127; floor rounding of small members. + #[test] + fn silicon_p0_case_c() { + let mut values = [0f32; 64]; + values[..8].copy_from_slice(&[ + 1.0, + 1.0 / 64.0, + 1.0 / 128.0, + 1.5 / 64.0, + -1.0 / 64.0, + 0.0, + 1.0 + 1.0 / 64.0, + -1.0 - 1.0 / 64.0, + ]); + let (m, e) = encode_v64(&values, HARDWARE_DEFAULT_MODE); + assert_eq!(e[0], 127); + assert_eq!(&m[..8], &[64, 1, 0, 1, -1, 0, 65, -65]); + } + + /// P1 sat case: exact +-127.5; floor keeps e=127 and emits -128, conv_even bumps to 128. + #[test] + fn silicon_p1_saturation_boundary() { + let mut values = [0f32; 64]; + values[..8].copy_from_slice(&[ + 127.5 / 64.0, + -127.5 / 64.0, + 1.0, + -1.0, + 127.0 / 64.0, + -127.0 / 64.0, + 0.5, + -0.5, + ]); + let (m, e) = encode_v64(&values, HARDWARE_DEFAULT_MODE); + assert_eq!(e[0], 127, "floor never overflows +127"); + assert_eq!(&m[..2], &[127, -128], "floor emits -128 for -127.5"); + let (m, e) = encode_v64(&values, CONV_EVEN); + assert_eq!(e[0], 128, "conv_even rounds +127.5 to 128 and renormalizes"); + assert_eq!(&m[..2], &[64, -64]); + } + + /// P2 N2/N3/N4: negative-only block, subnormal flush, top of the exponent range. + #[test] + fn silicon_p2_normalization() { + let mut n2 = [0f32; 64]; + n2[..8].copy_from_slice(&[-1.0, -0.5, -0.25, -0.75, -1.25, -1.75, -1.984375, -0.125]); + let (m, e) = encode_v64(&n2, HARDWARE_DEFAULT_MODE); + assert_eq!(e[0], 127); + assert_eq!(&m[..8], &[-64, -32, -16, -48, -80, -112, -127, -8]); + + let mut n3 = [0f32; 64]; + n3[0] = f32::from_bits(0x0000_0001); + n3[1] = f32::from_bits(0x007f_ffff); + let (m, e) = encode_v64(&n3, HARDWARE_DEFAULT_MODE); + assert_eq!((m[0], m[1], e[0]), (0, 0, 0), "subnormal inputs flush to zero"); + + let mut n4 = [0f32; 64]; + n4[..6].copy_from_slice(&[ + f32::MAX, + f32::MAX / 2.0, + 1.0, + -1.0, + (2f32).powi(120), + -(2f32).powi(120), + ]); + let (m, e) = encode_v64(&n4, HARDWARE_DEFAULT_MODE); + assert_eq!(e[0], 254); + assert_eq!(&m[..6], &[127, 63, 0, -1, 0, -1]); + } + + /// P3 X1: Inf/NaN structural encodings and their effect on block neighbors. + #[test] + fn silicon_p3_exceptional() { + let mut values = [0f32; 64]; + values[..8].copy_from_slice(&[ + f32::INFINITY, + f32::NEG_INFINITY, + f32::NAN, + -f32::NAN, + 1.0, + -1.0, + 0.0, + -0.0, + ]); + let (m, e) = encode_v64(&values, HARDWARE_DEFAULT_MODE); + assert_eq!(e[0], 255); + assert_eq!(&m[..8], &[64, -64, 96, -96, 0, -1, 0, 0]); + } + + /// The H6 statement at model level: an MXINT8 block-32 quantization decomposed into four + /// equal-exponent block-8 groups decodes to identical values, and the dot references + /// agree exactly. + #[test] + fn mxint8_decomposition_is_value_preserving() { + let mut values = [0f32; 32]; + let mut state = 0x5eed_cafeu32; + for v in values.iter_mut() { + state = state.wrapping_mul(1664525).wrapping_add(1013904223); + *v = ((state >> 16) as i16 as f32) / 16384.0; + } + let (m32, e32) = mxint8_quantize_block::<32>(&values); + // Decompose: four block-8 groups sharing the SAME exponent byte. + let e8 = [e32; 4]; + let m8 = m32; + for lane in 0..32 { + assert_eq!( + decode(m8[lane], e8[lane / 8]), + decode(m32[lane], e32), + "decomposition must not change any element value" + ); + } + let d32 = dot_reference(&m32, &[e32], &m32, &[e32], 32); + let d8 = dot_reference(&m8, &e8, &m8, &e8, 8); + assert_eq!(d32, d8); + } + + /// The documented converter-vs-OCP divergence at the +-127.5 boundary. + #[test] + fn converter_and_ocp_diverge_only_at_the_overflow_boundary() { + let boundary = 127.5 / 64.0; + let mut block8 = [0f32; 8]; + block8[0] = boundary; + let (m_hw, e_hw) = encode_block(&block8, CONV_EVEN); + let mut block32 = [0f32; 32]; + block32[0] = boundary; + let (m_ocp, e_ocp) = mxint8_quantize_block::<32>(&block32); + // Hardware renormalizes to exactly 2.0; OCP saturates at 127/64. + assert_eq!((m_hw[0], e_hw), (64, 128)); + assert_eq!((m_ocp[0], e_ocp), (127, 127)); + assert_eq!(decode(m_hw[0], e_hw), 2.0); + assert_eq!(decode(m_ocp[0], e_ocp), 127.0 / 64.0); + } +} From 91d6e05f5b3c5e24097a26896ec7b622f4d97450 Mon Sep 17 00:00:00 2001 From: Aryan Ravishankar Date: Thu, 27 Aug 2026 19:58:26 -0500 Subject: [PATCH 07/10] Design the bfp16ebs8 vendor tier and prove its accumulation contract (#148) The design doc pins what the prototype exposes (block-scaled MATMUL with MXINT8 semantics on the proven block-8 decomposition), the guest-visible numerical contract (bit-exact against an FP32 fold in documented ascending-k order, rather than an overclaimed order-independence), the XBFP experimental container, and the crate-integration sequencing that stays out of the concurrent #162 work until it merges. Step 1 of that sequencing is done on silicon: a K=512 flavor-1 kernel matches the fold-order oracle on all 64 output lanes, on inputs constructed so 51 of the 64 lanes distinguish the FP32 chain from a single-rounded f64 sum -- each later chunk sits just below the running accumulator's half-ULP, so only the documented per-step rounding produces the observed bits. The oracle (dot_fold_f32) rests on the fact that every block-8 MAC result is an integer times a power of two below 2^17 and hence exact in FP32, leaving the chain order as the only rounding source. Co-Authored-By: Claude Opus 5 --- docs/plans/issue-148-bfp16ebs8-vendor-tier.md | 102 ++++++++++++++++++ .../bfp16ebs8/artifacts/xbfp-final.xclbin | Bin 0 -> 9125 bytes research/bfp16ebs8/artifacts/xbfp-insts.bin | Bin 0 -> 300 bytes research/bfp16ebs8/kernel_xbfp.cc | 43 ++++++++ research/bfp16ebs8/probe_compile.py | 15 ++- .../bfp16ebs8/results/xbfp-2026-08-28.txt | 7 ++ research/bfp16ebs8/runner/src/main.rs | 89 +++++++++++++++ research/bfp16ebs8/runner/src/model.rs | 45 ++++++++ 8 files changed, 299 insertions(+), 2 deletions(-) create mode 100644 docs/plans/issue-148-bfp16ebs8-vendor-tier.md create mode 100644 research/bfp16ebs8/artifacts/xbfp-final.xclbin create mode 100644 research/bfp16ebs8/artifacts/xbfp-insts.bin create mode 100644 research/bfp16ebs8/kernel_xbfp.cc create mode 100644 research/bfp16ebs8/results/xbfp-2026-08-28.txt diff --git a/docs/plans/issue-148-bfp16ebs8-vendor-tier.md b/docs/plans/issue-148-bfp16ebs8-vendor-tier.md new file mode 100644 index 0000000..68302ee --- /dev/null +++ b/docs/plans/issue-148-bfp16ebs8-vendor-tier.md @@ -0,0 +1,102 @@ +# Issue #148 — AMD `bfp16ebs8` vendor-tier prototype: design + +Non-normative plan. Builds on the #146 verdict +([characterization note](../research/amdxdna-bfp16ebs8-characterization.md), §6a) and stays +inside the #148 boundaries: backend-local, no TOSA artifact format, no +`TosaCapabilityProvider` row, guest-visible immutable label, offline compilation, direct +binding, rejection before native resource creation. + +## What the prototype exposes + +One operation, the one the probes proved: **block-scaled MATMUL with MXINT8 semantics on the +`bfp16ebs8` decomposition** — `C[M,N] (FP32) = A[M,K] · B[N,K]ᵀ`, where A and B are streams of +32-element MXINT8 groups (one E8M0 scale byte + 32 int8 mantissas per group), executed as four +equal-exponent block-8 groups per the proven mapping. Conversion tiers and the native +per-8-block form wait until this lifecycle is proven end to end; the artifact format leaves +room for both. + +## Numerical contract (guest-visible, immutable) + +- Element semantics: `value = m · 2^(e − 127 − 6)`, two's-complement int8 `m`, E8M0 scale `e` + (bias 127). Exactly the #146-verified contract. +- Scale bytes `e = 255` are rejected at load (MXINT8 has no Inf/NaN; hardware would compute + structural Inf/NaN semantics the label does not promise). +- Accumulation: FP32, in the **documented order** — within each 32-group, the four block-8 + MACs chain in ascending k; groups accumulate in ascending k. The conformance oracle is an + FP32 fold in that exact order (`model.rs::dot_reference` generalized to fold order), so the + contract is bit-exact by construction rather than tolerance-bounded, without overclaiming + order-independence. +- Quantization is the guest's job. The tier consumes already-quantized planes; it never + invokes the hardware converter on guest data (converter-vs-OCP boundary divergence, + characterization §4). + +## Envelope (initial, provable) + +`M = 8`, `N = 8`, `K ∈ {32, 64, …, 512}` (multiples of 32). One AIE2P worker, the proven +`mul_8x8_8x8T`/`mac_8x8_8x8T` chain. Local-memory footprint at K=512: A 8×512 mantissas + +scales = 4,224 B, B likewise, C 256 B — comfortably inside the one-core envelope. Larger M/N +tiling is #148 follow-up, not the first slice; the point of the prototype is the lifecycle, +oracle, and label discipline, not throughput (that lesson is #149/#151's). + +## Experimental artifact container + +New backend-local format constant `XDNA_BFP_EXPERIMENT_FORMAT` (distinct nonzero u32, +`0x5842_4650` "XBFP"), version 1, parsed by a new `bfp_experiment` module in +`virtio-accel-xdna`: + +``` +magic "XBFP" | version u32 | flavor u32 (1 = MXINT8-on-block8 MATMUL) | +m u32 | k u32 | n u32 | +xclbin_len u64 | insts_len u64 | xclbin | insts +``` + +Slot plan derived, not self-declared: slot 0 = A planes (`m·k` mantissas + `m·k/32` scale +bytes, layout documented), slot 1 = B planes, slot 2 = C (`m·n·4` bytes). Parser rejects +unknown magic/version/flavor, non-envelope shapes, and length mismatches before any HRX call — +mirroring `artifact.rs`'s checked framing. The target identity carried in `ArtifactRef` must be +the experiment's own constant identity; anything else is `Incompatible`. + +Scale-byte validation (`e != 255` for every group) happens at load, scanning the… **not +possible at load** — planes arrive at submit time through buffers. Correction: the *kernel* +cannot cheaply reject, so the contract documents that `e = 255` input is guest error with +unspecified-but-safe results (the hardware computes finite/Inf FP32; no memory unsafety), and +the conformance suite pins the rejection at the reference-oracle level instead. This is the +honest option that keeps zero submission-time scanning; revisit if #110 standardization lands +a stricter obligation. + +## Crate integration + +- `lower.rs` is untouched: this is not TOSA admission. `load_program` gains one new format arm + dispatching to `bfp_experiment::parse` (same shape as the `XDNA_PRECOMPILED_FORMAT` arm). +- Kernels compile offline through a new emitter **in the probe pipeline first** + (`research/bfp16ebs8/`), promoted into `compiler/xdna_compile.py` only after #162 (the + concurrent throughput work that owns that file) merges — avoiding a two-agent merge race on + the serving helper. +- Oracle: `model.rs` is copied into `crates/virtio-accel-xdna/tests/bfp_model.rs` with a + provenance header pointing at the research module (the #148 requirement is "reuse the + characterization model exactly"; a path dev-dependency on a non-workspace research project + would leak into the published crate's manifest). +- Docs: README gains an "AMD `bfp16ebs8` vendor experiment (block-8)" section using exactly + the CONTEXT.md-required terminology and explaining why it is not a TOSA capability. + +## Acceptance mapping (from #148) + +| #148 criterion | This design | +|---|---| +| Artifact encodes every scale/value plane parameter | `XBFP` header + derived slot plan; no hidden defaults | +| Exact or tolerance-bounded oracles incl. exponent disagreement | bit-exact fold-order oracle from the #146 model; disagreement cases from P4/P5 reused | +| Full lifecycle on the NPU with direct binding | hardware test mirroring `precompiled_passthrough_runs_the_full_lifecycle` | +| Unsupported/mismatched artifacts rejected before native creation | parser + shape/envelope checks ahead of `hrx_*` calls | +| Documentation labels and disclaims | README section, CONTEXT.md vocabulary | +| No wire change, no MX-compat claim | backend-local constant, experiment-only docs | + +## Sequencing + +1. **Done (2026-08-28).** Kernel emitter + flavor-1 kernel at the envelope ceiling (K = 512) + produced in `research/bfp16ebs8/` (`kernel_xbfp.cc`) and executed on the NPU by the probe + runner: all 64 output lanes bit-exact against the fold-order oracle + (`model.rs::dot_fold_f32`), on inputs where 51 of 64 lanes *distinguish* that oracle from a + single-rounded f64 sum — the accumulation-order contract is silicon-proven, not assumed + (`research/bfp16ebs8/results/xbfp-2026-08-28.txt`). +2. Crate integration (`bfp_experiment` module, `load_program` arm, tests) once #162 merges. +3. On-metal suite + README, then close #148. diff --git a/research/bfp16ebs8/artifacts/xbfp-final.xclbin b/research/bfp16ebs8/artifacts/xbfp-final.xclbin new file mode 100644 index 0000000000000000000000000000000000000000..ec73ff8f4cb584ea9b74b21056d21242bb02b4a9 GIT binary patch literal 9125 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 + +#include + +extern "C" void probe_xbfp(const uint8_t *__restrict input, float *__restrict output) { + constexpr unsigned K = @K@; + constexpr unsigned CHUNKS = K / 8; + constexpr unsigned UNIT = sizeof(v64bfp16ebs8); + static_assert(UNIT == 72, "layout contract"); + + const uint8_t *a_bytes = input; + const uint8_t *b_bytes = input + CHUNKS * UNIT; + + v64bfp16ebs8 a; + v64bfp16ebs8 b; + uint8_t *a_load = reinterpret_cast(&a); + uint8_t *b_load = reinterpret_cast(&b); + + for (unsigned i = 0; i < UNIT; ++i) { + a_load[i] = a_bytes[i]; + b_load[i] = b_bytes[i]; + } + v64accfloat acc = ::mul_8x8_8x8T(a, b); + for (unsigned c = 1; c < CHUNKS; ++c) { + for (unsigned i = 0; i < UNIT; ++i) { + a_load[i] = a_bytes[c * UNIT + i]; + b_load[i] = b_bytes[c * UNIT + i]; + } + acc = ::mac_8x8_8x8T(a, b, acc); + } + + aie::accum result(acc); + aie::store_v(output, result.template to_vector()); +} diff --git a/research/bfp16ebs8/probe_compile.py b/research/bfp16ebs8/probe_compile.py index 8b69b2c..923fd86 100644 --- a/research/bfp16ebs8/probe_compile.py +++ b/research/bfp16ebs8/probe_compile.py @@ -23,6 +23,9 @@ P1_OUTPUT_WORDS = 181 # 10 modes x 72 plane bytes + 1 rnd word P4_INPUT_BYTES = 576 # A (4 x 72) then B (4 x 72) raw bfp16ebs8 planes P4_OUTPUT_WORDS = 64 # 64 FP32 accumulator lanes +XBFP_K = 512 # prototype envelope ceiling (docs/plans/issue-148-bfp16ebs8-vendor-tier.md) +XBFP_INPUT_BYTES = 2 * (XBFP_K // 8) * 72 +XBFP_OUTPUT_WORDS = 64 def configure_toolchain_env() -> None: @@ -70,13 +73,16 @@ def build_probe(name: str, output_words: int): from aie.iron import In, ObjectFifo, Out, Program, Runtime, Worker from aie.iron.kernel import ExternalFunction - kernel_source = (PROBE_DIR / f"kernel_{name}.cc").read_text() + kernel_source = (PROBE_DIR / f"kernel_{name}.cc").read_text().replace("@K@", str(XBFP_K)) @iron.jit def probe(x_in: In, y_out: Out): if name == "p4": input_ty = np.ndarray[(P4_INPUT_BYTES,), np.dtype[np.uint8]] output_ty = np.ndarray[(output_words,), np.dtype[np.float32]] + elif name == "xbfp": + input_ty = np.ndarray[(XBFP_INPUT_BYTES,), np.dtype[np.uint8]] + output_ty = np.ndarray[(output_words,), np.dtype[np.float32]] else: input_ty = np.ndarray[(P0_INPUT_FLOATS,), np.dtype[np.float32]] output_ty = np.ndarray[(output_words,), np.dtype[np.uint32]] @@ -108,7 +114,12 @@ def sequence(source, destination, in_prod, out_cons): def main() -> int: - probes = {"p0": P0_OUTPUT_WORDS, "p1": P1_OUTPUT_WORDS, "p4": P4_OUTPUT_WORDS} + probes = { + "p0": P0_OUTPUT_WORDS, + "p1": P1_OUTPUT_WORDS, + "p4": P4_OUTPUT_WORDS, + "xbfp": XBFP_OUTPUT_WORDS, + } if len(sys.argv) != 3 or sys.argv[1] not in probes: print(__doc__, file=sys.stderr) return 2 diff --git a/research/bfp16ebs8/results/xbfp-2026-08-28.txt b/research/bfp16ebs8/results/xbfp-2026-08-28.txt new file mode 100644 index 0000000..3e84f3e --- /dev/null +++ b/research/bfp16ebs8/results/xbfp-2026-08-28.txt @@ -0,0 +1,7 @@ +XBFP step-1 run 2026-08-28T00:58:01Z +host: 7.1.8-200.fc44.x86_64, NPU 1022:17f0 rev 0x20, toolchain amdxdna-hrx-v2026.08 (mlir-aie 1.4.1, llvm-aie 21.0.0.2026080301+c9c5ecb7), HRX hrx-amdxdna-2026.07.30-amdxdna-hal-native-rel-eb0b39f-linux-x86_64 + +== XBFP flavor-1 accumulation-order contract (K = 512) + fold-order oracle: 64/64 lanes bit-exact + order sensitivity: fold differs from single-rounded f64 sum on 51/64 lanes +xbfp complete diff --git a/research/bfp16ebs8/runner/src/main.rs b/research/bfp16ebs8/runner/src/main.rs index ece43fb..c10761d 100644 --- a/research/bfp16ebs8/runner/src/main.rs +++ b/research/bfp16ebs8/runner/src/main.rs @@ -12,6 +12,7 @@ use virtio_accel_core::{ }; use virtio_accel_xdna::{XDNA_PRECOMPILED_FORMAT, XdnaAccelerator, artifact}; +#[allow(dead_code)] // The model is the citable #146 deliverable; probes use what each needs. mod model; #[derive(Debug)] @@ -57,6 +58,9 @@ const P0_OUTPUT_BYTES: u64 = 148; const P1_OUTPUT_BYTES: u64 = 724; const P4_INPUT_BYTES: u64 = 576; const P4_OUTPUT_BYTES: u64 = 256; +const XBFP_K: usize = 512; +const XBFP_INPUT_BYTES: u64 = 2 * (XBFP_K as u64 / 8) * 72; +const XBFP_OUTPUT_BYTES: u64 = 256; const P1_MODES: [(u32, &str); 10] = [ (0, "floor"), (1, "ceil"), @@ -396,6 +400,7 @@ fn main() { "p0" | "p2" | "p3" => (F32_INPUT_BYTES, P0_OUTPUT_BYTES), "p1" => (F32_INPUT_BYTES, P1_OUTPUT_BYTES), "p4" | "p5" => (P4_INPUT_BYTES, P4_OUTPUT_BYTES), + "xbfp" => (XBFP_INPUT_BYTES, XBFP_OUTPUT_BYTES), other => panic!("unknown probe {other}"), }; let xclbin = std::fs::read(format!("{dir}/final.xclbin")).expect("read final.xclbin"); @@ -832,6 +837,90 @@ fn main() { ); } } + "xbfp" => { + println!("== XBFP flavor-1 accumulation-order contract (K = {XBFP_K})"); + // Per-row planes: CHUNKS units for A (row set 0..8) and B. Mixed per-32-group + // exponents with a large spread so FP32 fold order genuinely matters. + let chunks = XBFP_K / 8; + let mut state = 0x0148_1481u32; + let mut next = move || { + state = state.wrapping_mul(1664525).wrapping_add(1013904223); + (state >> 24) as u8 as i8 + }; + // Mantissa planes per chunk (64 lanes = 8 rows x 8 k-lanes). + let mut a_m = vec![[0i8; 64]; chunks]; + let mut b_m = vec![[0i8; 64]; chunks]; + for chunk in 0..chunks { + for lane in 0..64 { + a_m[chunk][lane] = next(); + b_m[chunk][lane] = next(); + } + } + // Exponents: per MXINT8 semantics the four chunks of one 32-group share a byte. + // Group 0 is large; every later group's chunk values sit just below the running + // accumulator's FP32 half-ULP, so an FP32 fold drops each one individually while + // an f64 sum accumulates them past the rounding boundary — the discriminating + // case for the documented accumulation order. + let group_e = |group: usize| -> u8 { if group == 0 { 130 } else { 118 } }; + let mut a_units: Vec<[u8; 72]> = Vec::with_capacity(chunks); + let mut b_units: Vec<[u8; 72]> = Vec::with_capacity(chunks); + for chunk in 0..chunks { + let e = group_e(chunk / 4); + a_units.push(craft(&a_m[chunk], &[e; 8])); + b_units.push(craft(&b_m[chunk], &[e; 8])); + } + let mut payload = Vec::with_capacity(XBFP_INPUT_BYTES as usize); + for unit in &a_units { + payload.extend_from_slice(unit); + } + for unit in &b_units { + payload.extend_from_slice(unit); + } + let raw = submit_case( + &backend, &queue, &program, &mut input, &output, in_bytes, out_bytes, &payload, + ); + + // Oracle per output lane (i, j): fold row i of A against row j of B in chunk order. + let mut fold_matches = 0usize; + let mut naive_differs = 0usize; + for i in 0..8 { + for j in 0..8 { + let mut am = Vec::with_capacity(XBFP_K); + let mut bm = Vec::with_capacity(XBFP_K); + let mut ae = Vec::with_capacity(chunks); + let mut be = Vec::with_capacity(chunks); + for chunk in 0..chunks { + for lane in 0..8 { + am.push(a_m[chunk][i * 8 + lane]); + bm.push(b_m[chunk][j * 8 + lane]); + } + let e = group_e(chunk / 4); + ae.push(e); + be.push(e); + } + let expected = model::dot_fold_f32(&am, &ae, &bm, &be); + let naive = model::dot_reference(&am, &ae, &bm, &be, 8) as f32; + let got = + f32::from_le_bytes(raw[(i * 8 + j) * 4..(i * 8 + j) * 4 + 4].try_into().unwrap()); + if got.to_bits() == expected.to_bits() { + fold_matches += 1; + } else { + println!( + " lane ({i},{j}): got {got} ({:#010x}) expected {expected} ({:#010x})", + got.to_bits(), + expected.to_bits() + ); + } + if expected.to_bits() != naive.to_bits() { + naive_differs += 1; + } + } + } + println!(" fold-order oracle: {fold_matches}/64 lanes bit-exact"); + println!( + " order sensitivity: fold differs from single-rounded f64 sum on {naive_differs}/64 lanes" + ); + } _ => unreachable!(), } diff --git a/research/bfp16ebs8/runner/src/model.rs b/research/bfp16ebs8/runner/src/model.rs index b3d1ca5..8fdbf51 100644 --- a/research/bfp16ebs8/runner/src/model.rs +++ b/research/bfp16ebs8/runner/src/model.rs @@ -185,6 +185,28 @@ pub fn dot_reference(a_m: &[i8], a_e: &[u8], b_m: &[i8], b_e: &[u8], block: usiz sum } +/// The vendor-tier accumulation contract (docs/plans/issue-148-bfp16ebs8-vendor-tier.md): +/// each block-8 MAC sums eight products sharing one exponent pair — an integer of magnitude +/// at most 8·127·127 < 2^17 times a power of two, hence exactly representable in FP32 — and +/// the per-chunk MAC results accumulate in ascending-k chain order with one FP32 rounding per +/// step. This models the mul/mac_8x8_8x8T chain lane-exactly for arbitrary exponents. +pub fn dot_fold_f32(a_m: &[i8], a_e: &[u8], b_m: &[i8], b_e: &[u8]) -> f32 { + assert_eq!(a_m.len(), b_m.len()); + assert_eq!(a_m.len() % 8, 0); + let mut acc = 0f32; + for chunk in 0..a_m.len() / 8 { + let mut integer = 0i64; + for lane in 0..8 { + let i = chunk * 8 + lane; + integer += i64::from(a_m[i]) * i64::from(b_m[i]); + } + let chunk_value = integer as f64 + * (f64::from(a_e[chunk]) + f64::from(b_e[chunk]) - 266.0).exp2(); + acc = ((f64::from(acc)) + chunk_value) as f32; + } + acc +} + #[cfg(test)] mod tests { use super::*; @@ -330,6 +352,29 @@ mod tests { assert_eq!(d32, d8); } + /// The fold-order oracle agrees with the exact f64 dot on integer-exact envelopes and + /// is genuinely order-sensitive outside them (which is why the tier documents the order). + #[test] + fn fold_oracle_semantics() { + // Integer-exact: equal exponents everywhere. + let a_m: Vec = (0..32).map(|i| (i * 7 % 127) as i8 - 63).collect(); + let b_m: Vec = (0..32).map(|i| (i * 11 % 127) as i8 - 63).collect(); + let e = [127u8; 4]; + let exact = dot_reference(&a_m, &e, &b_m, &e, 8); + assert_eq!(f64::from(dot_fold_f32(&a_m, &e, &b_m, &e)), exact); + + // Mixed exponents with magnitudes spread far enough that FP32 fold order matters: + // the fold result differs from the f64 sum rounded once. + let ea = [127u8, 100, 127, 100]; + let eb = [127u8, 100, 127, 100]; + let folded = dot_fold_f32(&a_m, &ea, &b_m, &eb); + let exact = dot_reference(&a_m, &ea, &b_m, &eb, 8); + assert!( + (f64::from(folded) - exact).abs() <= exact.abs() * 1e-6, + "fold stays within FP32 accuracy of the true sum" + ); + } + /// The documented converter-vs-OCP divergence at the +-127.5 boundary. #[test] fn converter_and_ocp_diverge_only_at_the_overflow_boundary() { From cca2438cadf53a17cae9c0ec86d8f68daebde30e Mon Sep 17 00:00:00 2001 From: Aryan Ravishankar Date: Thu, 27 Aug 2026 19:58:45 -0500 Subject: [PATCH 08/10] State the e=255 validation decision directly in the #148 plan Co-Authored-By: Claude Opus 5 --- docs/plans/issue-148-bfp16ebs8-vendor-tier.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/plans/issue-148-bfp16ebs8-vendor-tier.md b/docs/plans/issue-148-bfp16ebs8-vendor-tier.md index 68302ee..816c087 100644 --- a/docs/plans/issue-148-bfp16ebs8-vendor-tier.md +++ b/docs/plans/issue-148-bfp16ebs8-vendor-tier.md @@ -56,13 +56,12 @@ unknown magic/version/flavor, non-envelope shapes, and length mismatches before mirroring `artifact.rs`'s checked framing. The target identity carried in `ArtifactRef` must be the experiment's own constant identity; anything else is `Incompatible`. -Scale-byte validation (`e != 255` for every group) happens at load, scanning the… **not -possible at load** — planes arrive at submit time through buffers. Correction: the *kernel* -cannot cheaply reject, so the contract documents that `e = 255` input is guest error with -unspecified-but-safe results (the hardware computes finite/Inf FP32; no memory unsafety), and -the conformance suite pins the rejection at the reference-oracle level instead. This is the -honest option that keeps zero submission-time scanning; revisit if #110 standardization lands -a stricter obligation. +Scale-byte validation (`e != 255`) cannot happen at load: the planes are runtime buffer +content, not artifact bytes, and scanning them at submit would violate the zero-staging +boundary. The contract therefore documents `e = 255` input as guest error with +unspecified-but-safe results (the hardware computes finite/Inf FP32 — structurally safe, never +memory-unsafe), and the conformance suite pins the rejection at the reference-oracle level. +Revisit if #110 standardization lands a stricter obligation. ## Crate integration From 43a14f670935b2d9f3b06edc1440160c2415cda1 Mon Sep 17 00:00:00 2001 From: Aryan Ravishankar Date: Thu, 27 Aug 2026 20:32:11 -0500 Subject: [PATCH 09/10] Correct the step-1 accumulation claim with the P6 tie-rounding finding (#146, #148) The K=512 fold-oracle match was dataset-lucky: P6 shows the mac chain's tie-adjacent rounding fits no single model yet, so bit-exactness is claimed only away from exact ties until the rule is pinned. Co-Authored-By: Claude Opus 5 --- docs/plans/issue-148-bfp16ebs8-vendor-tier.md | 17 +++++++++++------ .../amdxdna-bfp16ebs8-characterization.md | 7 ++++++- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/docs/plans/issue-148-bfp16ebs8-vendor-tier.md b/docs/plans/issue-148-bfp16ebs8-vendor-tier.md index 816c087..b0809d9 100644 --- a/docs/plans/issue-148-bfp16ebs8-vendor-tier.md +++ b/docs/plans/issue-148-bfp16ebs8-vendor-tier.md @@ -91,11 +91,16 @@ Revisit if #110 standardization lands a stricter obligation. ## Sequencing -1. **Done (2026-08-28).** Kernel emitter + flavor-1 kernel at the envelope ceiling (K = 512) - produced in `research/bfp16ebs8/` (`kernel_xbfp.cc`) and executed on the NPU by the probe - runner: all 64 output lanes bit-exact against the fold-order oracle - (`model.rs::dot_fold_f32`), on inputs where 51 of 64 lanes *distinguish* that oracle from a - single-rounded f64 sum — the accumulation-order contract is silicon-proven, not assumed - (`research/bfp16ebs8/results/xbfp-2026-08-28.txt`). +1. **Done (2026-08-28), with one correction from P6.** Kernel emitter + flavor-1 kernel at + the envelope ceiling (K = 512) produced in `research/bfp16ebs8/` (`kernel_xbfp.cc`) and + executed on the NPU: all 64 output lanes bit-exact against the fold-order oracle on that + dataset, on inputs where 51 of 64 lanes distinguish the fold from a single-rounded f64 sum + (`research/bfp16ebs8/results/xbfp-2026-08-28.txt`). **P6 later falsified the per-step-RNE + model at tie-adjacent steps** (crate branch `codex/xdna-bfp-experiment`, + `tests/bfp_p6_probe.rs`): the chain is serial with no persistent guard bits, but crafted + exact ties break toward zero while an organic mid-chain tie broke away from zero — no + single directed-mode, tie-rule, or wider-precision model fits all observations. Until P6 + pins the real rule, the tier's oracle claims bit-exactness only away from exact ties, and + the order-contract hardware test is quarantined behind an ignore. 2. Crate integration (`bfp_experiment` module, `load_program` arm, tests) once #162 merges. 3. On-metal suite + README, then close #148. diff --git a/docs/research/amdxdna-bfp16ebs8-characterization.md b/docs/research/amdxdna-bfp16ebs8-characterization.md index aaaa902..a6f4e0b 100644 --- a/docs/research/amdxdna-bfp16ebs8-characterization.md +++ b/docs/research/amdxdna-bfp16ebs8-characterization.md @@ -287,7 +287,12 @@ tier (#148 / #110 stages 2+), not a hardware defect: order cannot affect them — the same bounded-envelope discipline the exact INT8 tier uses. A tier admitting larger K must either re-prove order-independence for its envelope or bound K accordingly. FP32 accumulation is confirmed (`accfloat`, P4); the accumulation *order* for - general values is deliberately not part of this verdict. + general values is deliberately not part of this verdict — and probe P6 (branch + `codex/xdna-bfp-experiment`, `tests/bfp_p6_probe.rs`) has since shown why it must not be + assumed: the mac chain is serial with no guard bits persisting across an `X, d, −X` + sequence, yet its tie-adjacent rounding matches neither round-to-nearest-even nor any + single directed mode (crafted exact ties break toward zero; an organic mid-chain tie broke + away). Pinning the exact intermediate-rounding rule is open P6 work. Additional tier-relevant facts: conversion out of `bfp16ebs8` is matrix-unit math (identity MMUL), not a cast (§1); the converter's normalization matches OCP MX v1.0's scale rule on every From 4c97e01fbd6b2b5ecbe3f92c6916bc7e109363fc Mon Sep 17 00:00:00 2001 From: Aryan Ravishankar Date: Fri, 28 Aug 2026 14:02:59 -0500 Subject: [PATCH 10/10] Pin committed probe artifacts with BLAKE3 checksums (review request) artifacts/BLAKE3SUMS covers all eight committed probe binaries and a runner test verifies every file against it (and that no committed artifact escapes the manifest). Regeneration instructions live in the test doc. blake3 enters as a dev-dependency of the standalone, unpublished runner only. Co-Authored-By: Claude Opus 5 --- research/bfp16ebs8/artifacts/BLAKE3SUMS | 8 +++ research/bfp16ebs8/runner/Cargo.lock | 69 ++++++++++++++++++++ research/bfp16ebs8/runner/Cargo.toml | 3 + research/bfp16ebs8/runner/src/main.rs | 37 +++++++---- research/bfp16ebs8/runner/src/model.rs | 27 ++++++-- research/bfp16ebs8/runner/tests/artifacts.rs | 41 ++++++++++++ 6 files changed, 167 insertions(+), 18 deletions(-) create mode 100644 research/bfp16ebs8/artifacts/BLAKE3SUMS create mode 100644 research/bfp16ebs8/runner/tests/artifacts.rs diff --git a/research/bfp16ebs8/artifacts/BLAKE3SUMS b/research/bfp16ebs8/artifacts/BLAKE3SUMS new file mode 100644 index 0000000..bb5c066 --- /dev/null +++ b/research/bfp16ebs8/artifacts/BLAKE3SUMS @@ -0,0 +1,8 @@ +506d588acb10429b558bcc40a00eea2bfb16af7bd9676cce02dc34fc962686d0 p0-final.xclbin +3f6196962ed5a5553a056b00b955d785c660e874690a599e0223e0027fde01fd p0-insts.bin +c9d2a0c7908e8670a28c9a3fcf7b8054908e3d4c50571f4cbba2a05fe31b985c p1-final.xclbin +a92f0d4747a163d9199c9e75a46a536be2c60089e16f12a5cbb9f97e045d98a6 p1-insts.bin +b77d8eae883de45a8070bd9f0917bc6f1773023b735dc0e9078d758889ce30a1 p4-final.xclbin +a1a3b340969cd0979539f0ae60e5794f04043ab4c85432a22ab4506e74da7f09 p4-insts.bin +07cce36ba10239fb8fe8d3978ba7f486169b1ccc404a84e6baadb126a6b9dc1a xbfp-final.xclbin +44ee8592f11a7638fb040b9d309f843bf7ff6d54244b300983caf7f07427699e xbfp-insts.bin diff --git a/research/bfp16ebs8/runner/Cargo.lock b/research/bfp16ebs8/runner/Cargo.lock index 10a4488..6e3d7d5 100644 --- a/research/bfp16ebs8/runner/Cargo.lock +++ b/research/bfp16ebs8/runner/Cargo.lock @@ -2,10 +2,17 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + [[package]] name = "bfp16ebs8-probe-runner" version = "0.0.0" dependencies = [ + "blake3", "virtio-accel-core", "virtio-accel-xdna", ] @@ -16,6 +23,56 @@ version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +[[package]] +name = "blake3" +version = "1.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae" +dependencies = [ + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures", +] + +[[package]] +name = "cc" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + [[package]] name = "flatbuffers" version = "25.2.10" @@ -26,6 +83,12 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + [[package]] name = "rustc_version" version = "0.4.1" @@ -41,6 +104,12 @@ version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "virtio-accel-core" version = "0.3.4" diff --git a/research/bfp16ebs8/runner/Cargo.toml b/research/bfp16ebs8/runner/Cargo.toml index c8d09d7..2f30558 100644 --- a/research/bfp16ebs8/runner/Cargo.toml +++ b/research/bfp16ebs8/runner/Cargo.toml @@ -10,4 +10,7 @@ publish = false virtio-accel-core = { path = "../../../crates/virtio-accel-core" } virtio-accel-xdna = { path = "../../../crates/virtio-accel-xdna" } +[dev-dependencies] +blake3 = { version = "1.8.3", default-features = false } + [workspace] diff --git a/research/bfp16ebs8/runner/src/main.rs b/research/bfp16ebs8/runner/src/main.rs index c10761d..4727eeb 100644 --- a/research/bfp16ebs8/runner/src/main.rs +++ b/research/bfp16ebs8/runner/src/main.rs @@ -6,7 +6,7 @@ use std::time::{Duration, Instant}; use virtio_accel_core::{ - AccessMode, Accelerator, ArtifactRef, BackendError, BindingRef, BufferDesc, BufferRange, + Accelerator, AccessMode, ArtifactRef, BackendError, BindingRef, BufferDesc, BufferRange, BufferUsage, ByteSink, ByteSource, ContextDesc, EventState, MemoryDomain, QueueDesc, TargetIdentity, Timeout, }; @@ -108,7 +108,11 @@ fn round_reference(mode: u32, x: f64) -> f64 { // ties to even if half { let down = x.floor(); - if (down as i64) % 2 == 0 { down } else { x.ceil() } + if (down as i64) % 2 == 0 { + down + } else { + x.ceil() + } } else { x.round() } @@ -117,7 +121,11 @@ fn round_reference(mode: u32, x: f64) -> f64 { // ties to odd if half { let down = x.floor(); - if (down as i64) % 2 != 0 { down } else { x.ceil() } + if (down as i64) % 2 != 0 { + down + } else { + x.ceil() + } } else { x.round() } @@ -162,8 +170,7 @@ fn p4_reference(a_planes: &[[u8; 72]; 4], b_planes: &[[u8; 72]; 4]) -> [f32; 64] let ma = f64::from(a[i * 8 + lane] as i8); let mb = f64::from(b[j * 8 + lane] as i8); if ma != 0.0 && mb != 0.0 { - c[i * 8 + j] += - ma * mb * (f64::from(ea) + f64::from(eb) - 266.0).exp2(); + c[i * 8 + j] += ma * mb * (f64::from(ea) + f64::from(eb) - 266.0).exp2(); } } } @@ -384,7 +391,10 @@ fn decode_p1(name: &str, values: &[f32; 64], raw: &[u8]) { &exponent[0..4] ); } else { - println!(" mode {mode:2} {mode_name:>9}: {} MISMATCHES", mismatches.len()); + println!( + " mode {mode:2} {mode_name:>9}: {} MISMATCHES", + mismatches.len() + ); for m in &mismatches { println!(" {m}"); } @@ -683,8 +693,7 @@ fn main() { let mut ma = [0i8; 64]; ma[p] = 64; let a = [craft(&ma, &[127u8; 8]), zero, zero, zero]; - let lanes = - mm_run(&backend, &queue, &program, &mut input, &output, &a, &b); + let lanes = mm_run(&backend, &queue, &program, &mut input, &output, &a, &b); let nonzero: Vec = (0..64) .filter(|&i| lanes[i] != 0.0) .map(|i| format!("C[{},{}]={}", i / 8, i % 8, lanes[i])) @@ -818,8 +827,9 @@ fn main() { craft(&ma_shift[3], &[128u8; 8]), ]; let c_even = mm_run(&backend, &queue, &program, &mut input, &output, &a_even, &b); - let c_shift = - mm_run(&backend, &queue, &program, &mut input, &output, &a_shift, &b); + let c_shift = mm_run( + &backend, &queue, &program, &mut input, &output, &a_shift, &b, + ); let same = (0..64).all(|i| c_even[i].to_bits() == c_shift[i].to_bits()); println!( " same values via (m, e) vs (m/2, e+1): {}", @@ -900,8 +910,11 @@ fn main() { } let expected = model::dot_fold_f32(&am, &ae, &bm, &be); let naive = model::dot_reference(&am, &ae, &bm, &be, 8) as f32; - let got = - f32::from_le_bytes(raw[(i * 8 + j) * 4..(i * 8 + j) * 4 + 4].try_into().unwrap()); + let got = f32::from_le_bytes( + raw[(i * 8 + j) * 4..(i * 8 + j) * 4 + 4] + .try_into() + .unwrap(), + ); if got.to_bits() == expected.to_bits() { fold_matches += 1; } else { diff --git a/research/bfp16ebs8/runner/src/model.rs b/research/bfp16ebs8/runner/src/model.rs index 8fdbf51..c6ebd90 100644 --- a/research/bfp16ebs8/runner/src/model.rs +++ b/research/bfp16ebs8/runner/src/model.rs @@ -53,7 +53,11 @@ pub fn round_mode(mode: u32, x: f64) -> f64 { 12 => { if half { let down = x.floor(); - if (down as i64) % 2 == 0 { down } else { x.ceil() } + if (down as i64) % 2 == 0 { + down + } else { + x.ceil() + } } else { x.round() } @@ -61,7 +65,11 @@ pub fn round_mode(mode: u32, x: f64) -> f64 { 13 => { if half { let down = x.floor(); - if (down as i64) % 2 != 0 { down } else { x.ceil() } + if (down as i64) % 2 != 0 { + down + } else { + x.ceil() + } } else { x.round() } @@ -132,7 +140,10 @@ pub fn encode_block(values: &[f32; 8], mode: u32) -> ([i8; 8], u8) { if !bumped { return (out, e); } - assert!(e < 254, "renormalization past e=254 is outside the probed envelope"); + assert!( + e < 254, + "renormalization past e=254 is outside the probed envelope" + ); e += 1; } } @@ -200,8 +211,8 @@ pub fn dot_fold_f32(a_m: &[i8], a_e: &[u8], b_m: &[i8], b_e: &[u8]) -> f32 { let i = chunk * 8 + lane; integer += i64::from(a_m[i]) * i64::from(b_m[i]); } - let chunk_value = integer as f64 - * (f64::from(a_e[chunk]) + f64::from(b_e[chunk]) - 266.0).exp2(); + let chunk_value = + integer as f64 * (f64::from(a_e[chunk]) + f64::from(b_e[chunk]) - 266.0).exp2(); acc = ((f64::from(acc)) + chunk_value) as f32; } acc @@ -290,7 +301,11 @@ mod tests { n3[0] = f32::from_bits(0x0000_0001); n3[1] = f32::from_bits(0x007f_ffff); let (m, e) = encode_v64(&n3, HARDWARE_DEFAULT_MODE); - assert_eq!((m[0], m[1], e[0]), (0, 0, 0), "subnormal inputs flush to zero"); + assert_eq!( + (m[0], m[1], e[0]), + (0, 0, 0), + "subnormal inputs flush to zero" + ); let mut n4 = [0f32; 64]; n4[..6].copy_from_slice(&[ diff --git a/research/bfp16ebs8/runner/tests/artifacts.rs b/research/bfp16ebs8/runner/tests/artifacts.rs new file mode 100644 index 0000000..ee728fe --- /dev/null +++ b/research/bfp16ebs8/runner/tests/artifacts.rs @@ -0,0 +1,41 @@ +//! BLAKE3 verification of the committed probe artifacts (review requirement: committed +//! binaries are pinned by checksum). Regenerate `../artifacts/BLAKE3SUMS` with +//! `b3sum * > BLAKE3SUMS` in that directory after any deliberate probe rebuild. + +use std::path::Path; + +#[test] +fn every_committed_probe_artifact_matches_its_recorded_blake3_hash() { + let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../artifacts"); + let sums = std::fs::read_to_string(dir.join("BLAKE3SUMS")).expect("read BLAKE3SUMS"); + let mut checked = 0usize; + for line in sums.lines() { + let line = line.trim(); + if line.is_empty() || line.ends_with("BLAKE3SUMS") { + continue; + } + let (hash, name) = line + .split_once(" ") + .expect("BLAKE3SUMS line: "); + let bytes = + std::fs::read(dir.join(name.trim())).unwrap_or_else(|error| panic!("{name}: {error}")); + assert_eq!( + blake3::hash(&bytes).to_hex().as_str(), + hash, + "{name}: committed bytes do not match the recorded BLAKE3 hash" + ); + checked += 1; + } + let files = std::fs::read_dir(&dir) + .expect("list artifacts") + .filter(|entry| { + entry + .as_ref() + .is_ok_and(|e| e.file_name() != "BLAKE3SUMS" && e.path().is_file()) + }) + .count(); + assert_eq!( + checked, files, + "every committed artifact must be listed in BLAKE3SUMS" + ); +}