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..b0809d9 --- /dev/null +++ b/docs/plans/issue-148-bfp16ebs8-vendor-tier.md @@ -0,0 +1,106 @@ +# 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`) 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 + +- `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), 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 new file mode 100644 index 0000000..a6f4e0b --- /dev/null +++ b/docs/research/amdxdna-bfp16ebs8-characterization.md @@ -0,0 +1,311 @@ +# AMD XDNA2 `bfp16ebs8` characterization (issue #146) + +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`). + +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 (delivered) + +`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): + +- `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) + +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. 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); `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 +`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. + +**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 — 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 +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 +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 +verdict). 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/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/artifacts/p0-final.xclbin b/research/bfp16ebs8/artifacts/p0-final.xclbin new file mode 100644 index 0000000..b1f4889 Binary files /dev/null and b/research/bfp16ebs8/artifacts/p0-final.xclbin differ diff --git a/research/bfp16ebs8/artifacts/p0-insts.bin b/research/bfp16ebs8/artifacts/p0-insts.bin new file mode 100644 index 0000000..fd21924 Binary files /dev/null and b/research/bfp16ebs8/artifacts/p0-insts.bin differ diff --git a/research/bfp16ebs8/artifacts/p1-final.xclbin b/research/bfp16ebs8/artifacts/p1-final.xclbin new file mode 100644 index 0000000..639f22f Binary files /dev/null and b/research/bfp16ebs8/artifacts/p1-final.xclbin differ diff --git a/research/bfp16ebs8/artifacts/p1-insts.bin b/research/bfp16ebs8/artifacts/p1-insts.bin new file mode 100644 index 0000000..d526e88 Binary files /dev/null and b/research/bfp16ebs8/artifacts/p1-insts.bin differ diff --git a/research/bfp16ebs8/artifacts/p4-final.xclbin b/research/bfp16ebs8/artifacts/p4-final.xclbin new file mode 100644 index 0000000..f9b23ca Binary files /dev/null and b/research/bfp16ebs8/artifacts/p4-final.xclbin differ diff --git a/research/bfp16ebs8/artifacts/p4-insts.bin b/research/bfp16ebs8/artifacts/p4-insts.bin new file mode 100644 index 0000000..47a8e7e Binary files /dev/null and b/research/bfp16ebs8/artifacts/p4-insts.bin differ diff --git a/research/bfp16ebs8/artifacts/xbfp-final.xclbin b/research/bfp16ebs8/artifacts/xbfp-final.xclbin new file mode 100644 index 0000000..ec73ff8 Binary files /dev/null and b/research/bfp16ebs8/artifacts/xbfp-final.xclbin differ diff --git a/research/bfp16ebs8/artifacts/xbfp-insts.bin b/research/bfp16ebs8/artifacts/xbfp-insts.bin new file mode 100644 index 0000000..0f00bda Binary files /dev/null and b/research/bfp16ebs8/artifacts/xbfp-insts.bin differ 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/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/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/kernel_xbfp.cc b/research/bfp16ebs8/kernel_xbfp.cc new file mode 100644 index 0000000..5c0206b --- /dev/null +++ b/research/bfp16ebs8/kernel_xbfp.cc @@ -0,0 +1,43 @@ +// XBFP flavor-1 prototype kernel (issue #148, design step 1). +// +// C[8,8] (FP32) = A[8,K] . B[8,K]^T on bfp16ebs8 planes, K = @K@ (multiple of 8). +// Chunk c holds columns 8c..8c+8 as one v64bfp16ebs8 unit (64 mantissas then 8 exponents, +// the P0-pinned layout). Accumulation contract: ascending-c mul/mac chain, FP32. +// +// input [0 .. 9*K) A: K/8 units of 72 bytes +// input [9*K .. 18*K) B: K/8 units of 72 bytes +// output [0..256) C: 64 FP32 lanes, lane i*8+j +#include + +#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 new file mode 100644 index 0000000..923fd86 --- /dev/null +++ b/research/bfp16ebs8/probe_compile.py @@ -0,0 +1,145 @@ +#!/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 +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: + 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_probe(name: str, output_words: int): + """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 + from aie.iron.kernel import ExternalFunction + + 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]] + kernel = ExternalFunction( + f"probe_{name}", + source_string=kernel_source, + arg_types=[input_ty, output_ty], + ) + 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) + 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 + + +def main() -> int: + 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 + 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_probe(sys.argv[1], probes[sys.argv[1]]) + 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..466411a --- /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/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/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..a2f389d --- /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/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-27.txt b/research/bfp16ebs8/results/p3-2026-08-27.txt new file mode 100644 index 0000000..9404f80 --- /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/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/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/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/Cargo.lock b/research/bfp16ebs8/runner/Cargo.lock new file mode 100644 index 0000000..6e3d7d5 --- /dev/null +++ b/research/bfp16ebs8/runner/Cargo.lock @@ -0,0 +1,139 @@ +# This file is automatically @generated by Cargo. +# 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", +] + +[[package]] +name = "bitflags" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1045398c1bfd89168b5fd3f1fc11f6e70b34f6f66300c87d44d3de849463abf1" +dependencies = [ + "bitflags", + "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" +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 = "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" +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..2f30558 --- /dev/null +++ b/research/bfp16ebs8/runner/Cargo.toml @@ -0,0 +1,16 @@ +# 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" } + +[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 new file mode 100644 index 0000000..4727eeb --- /dev/null +++ b/research/bfp16ebs8/runner/src/main.rs @@ -0,0 +1,946 @@ +//! 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::{ + Accelerator, AccessMode, ArtifactRef, BackendError, BindingRef, BufferDesc, BufferRange, + BufferUsage, ByteSink, ByteSource, ContextDesc, EventState, MemoryDomain, QueueDesc, + TargetIdentity, Timeout, +}; +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)] +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 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 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"), + (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!(), + } +} + +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() +} + +fn submit_case( + backend: &A, + queue: &A::Queue, + program: &A::Program, + input: &mut A::Buffer, + output: &A::Buffer, + in_bytes: u64, + out_bytes: u64, + payload: &[u8], +) -> Vec { + assert_eq!(payload.len() as u64, in_bytes); + backend + .write_buffer(input, 0, &Slice(payload)) + .expect("write input"); + + let bindings = [ + BindingRef { + slot: 0, + buffer: input, + range: BufferRange::new(0, in_bytes).unwrap(), + access: AccessMode::Read, + }, + BindingRef { + slot: 1, + buffer: output, + range: BufferRange::new(0, out_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; 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]; + 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!(); + } + // 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]; + 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 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 mut args = std::env::args().skip(1); + let probe = args.next().expect("usage: runner "); + let dir = args.next().expect("usage: runner "); + 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), + "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"); + let insts = std::fs::read(format!("{dir}/insts.bin")).expect("read insts.bin"); + 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 + .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( + in_bytes, + 4096, + MemoryDomain::Shared, + BufferUsage::TRANSFER_DESTINATION | BufferUsage::PROGRAM_INPUT, + ) + .unwrap(), + ) + .expect("input buffer") + .into_parts(); + let (output, _) = backend + .allocate_buffer( + &context, + BufferDesc::new( + out_bytes, + 4096, + MemoryDomain::Shared, + BufferUsage::TRANSFER_SOURCE | BufferUsage::PROGRAM_OUTPUT, + ) + .unwrap(), + ) + .expect("output buffer") + .into_parts(); + + 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]; + // 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, + &program, + &mut input, + &output, + in_bytes, + out_bytes, + &f32_payload(values), + ); + decode_p0(name, values, &raw); + } + } + "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, + 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 + // 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, + in_bytes, + out_bytes, + &f32_payload(&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, + in_bytes, + out_bytes, + &f32_payload(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, + 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, + ); + } + } + "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!(), + } + + 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!("{probe} complete"); +} diff --git a/research/bfp16ebs8/runner/src/model.rs b/research/bfp16ebs8/runner/src/model.rs new file mode 100644 index 0000000..c6ebd90 --- /dev/null +++ b/research/bfp16ebs8/runner/src/model.rs @@ -0,0 +1,409 @@ +//! 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 +} + +/// 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::*; + + /// 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 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() { + 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); + } +} 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" + ); +}