-
Notifications
You must be signed in to change notification settings - Fork 0
Pillar-11 closure: constant guard, ndarray-substrate debt — and the A0/A1/A2 measurement that pays part of it #1140
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8788720
d0f6960
5df2d78
e87af51
3f347ba
7de3fbc
83b6539
3dcba98
2becf3c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| # AVX-512 build. Mirrors ndarray/.cargo/config-avx512.toml (`sapphirerapids`). | ||
| # cargo build --config .cargo/config-avx512.toml ... | ||
| # The `simd.rs` dispatch picks the `simd_avx512` backend (native __m512d). | ||
| # | ||
| # `sapphirerapids` is a SUPERSET of most AVX-512 silicon in the fleet. It | ||
| # implies x86-64-v4 PLUS avx512_bf16 / avx512_fp16 / AMX. On Sapphire / | ||
| # Emerald / Granite Rapids that is the right pick. On Cascade Lake (family 6 | ||
| # model 0x55 — what this session's host is) or Ice Lake, it is NOT: AMX is | ||
| # inline-asm and runtime-gated by `amx_available()`, so it stays inert, but | ||
| # anything compiled under `cfg(target_feature = "avx512bf16")` will SIGILL on | ||
| # silicon that lacks it. There, use one of: | ||
| # CARGO_BUILD_RUSTFLAGS='-Ctarget-cpu=x86-64-v4' # what the probe was run with | ||
| # cargo build --config .cargo/config-native.toml # host CPUID, non-portable | ||
| # Rule of thumb: `sapphirerapids` for a KNOWN SPR/EMR/GNR deployment target; | ||
| # `x86-64-v4` for portable AVX-512; `native` for this-machine-only. | ||
| [build] | ||
| [target.'cfg(target_arch = "x86_64")'] | ||
| rustflags = ["-Ctarget-cpu=sapphirerapids"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| # Build-machine-tuned. rustc resolves the host CPUID at compile time; the | ||
| # binary is NOT portable off this machine. Mirrors ndarray/.cargo/config-native.toml. | ||
| # cargo build --config .cargo/config-native.toml ... | ||
| [build] | ||
| [target.'cfg(target_arch = "x86_64")'] | ||
| rustflags = ["-Ctarget-cpu=native"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| # lance-graph cargo config — mirrors ndarray's `.cargo/config.toml` policy | ||
| # (operator ruling, 2026-09-02): GitHub needs v3; everything else v4 or native. | ||
| # | ||
| # ndarray is the MANDATORY execution substrate for every crate here, and its | ||
| # `simd.rs` dispatch is COMPILE-TIME (`cfg(target_feature = "avx512f")` / | ||
| # `"avx2"`). Whatever `target-cpu` this build runs at decides which backend | ||
| # `ndarray::simd::{F64x8, F32x16, I16x32}` resolves to. `.cargo/config.toml` | ||
| # is read from the INVOKING directory, not the dependency's — so ndarray's | ||
| # own config does NOT apply when a lance-graph crate is built from here. | ||
| # Without this file a local build compiles the substrate at generic x86-64 | ||
| # (SSE2), i.e. the scalar arm. | ||
| # | ||
| # Measured on the Goursat wavefront (crates/jc/examples/goursat_substrate_probe, | ||
| # 4097-point paths, release): under v4 `F64x8::mul_add` is 8.6x faster than | ||
| # the flat scalar loop; at generic x86-64 the SAME code is 4.5x SLOWER than | ||
| # scalar, because the polyfill's scalar arm is emulating eight lanes through | ||
| # arrays. A build with no target-cpu is a regression, not a no-op. | ||
| # | ||
| # Default here is v3 (AVX2) for the same reason ndarray chose it: portable | ||
| # across all x86_64 since Haswell, and what CI runs (`.github/workflows/*.yml` | ||
| # set `-C target-cpu=x86-64-v3` explicitly). Anything below v3 SIGILLs on the | ||
| # `simd_avx2` intrinsics. | ||
| # | ||
| # AVX-512 (v4): cargo build --config .cargo/config-avx512.toml ... | ||
| # or CARGO_BUILD_RUSTFLAGS='-Ctarget-cpu=x86-64-v4' | ||
| # Build machine: cargo build --config .cargo/config-native.toml ... | ||
| # | ||
| # The two crypto cfgs ndarray's config also carries (`curve25519_dalek_backend`, | ||
| # `poly1305_force_soft`) are deliberately NOT repeated: neither crate is in | ||
| # this workspace's Cargo.lock (verified 2026-09-02, 0 hits), so there is no | ||
| # AVX2 crypto surface here to compile out. Re-check if `encryption` or | ||
| # `ed25519-dalek` ever enters the tree. | ||
| [target.'cfg(target_arch = "x86_64")'] | ||
| rustflags = ["-Ctarget-cpu=x86-64-v3"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,162 @@ | ||
| ## TD-PILLAR11-SCIENTIFIC-LOOPS-BYPASS-NDARRAY-SIMD-1 (2026-09-02) — OPEN | ||
|
|
||
| **The debt is not missing SIMD support. It is scientific code bypassing the | ||
| already-complete `ndarray` execution substrate.** (Operator ruling, 2026-09-02.) | ||
|
|
||
| No violation was committed — the Pillar-11 legs contain zero raw intrinsics | ||
| (`core::arch` / `_mm*` / `target_feature`: 0 hits in `crates/sigker/src` and | ||
| `crates/jc/src`). They are scalar `f64`, hand-written beside a substrate that | ||
| already owns the machine vocabulary. | ||
|
|
||
| **The law, corrected from an earlier weaker reading.** There is no | ||
| "SIMD op exists -> polyfill, else -> scalar fallback" decision at consumer | ||
| level. `ndarray::simd` hides the architecture choice behind ONE bit-exact typed | ||
| surface, and **scalar is one BACKEND of that surface**, not a consumer-authored | ||
| alternative: `F64x8` resolves to AVX-512 (`__m512d`), AVX2 (`f64x4` x2), NEON | ||
| (`[float64x2_t; 4]`), wasm32+simd128 (`[v128; 4]`), and `scalar::F64x8` for | ||
| "other non-x86 targets ... full scalar fallback" (`simd.rs` dispatch arms, | ||
| verified 2026-09-02). So: | ||
|
|
||
| **Method-level parity CONFIRMED (operator, 2026-09-02):** *"every backend | ||
| implements every method with parity coverage."* The dispatch check above only | ||
| established that each backend EXPORTS the type; this closes the stronger | ||
| question a composing consumer actually depends on — that a method reached | ||
| through the typed surface is implemented, and bit-exact, on every arm. It is | ||
|
Comment on lines
+23
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Method availability does not make these floating-point operations bit-exact across backends. On wasm32+simd128 without relaxed SIMD, Useful? React with 👍 / 👎. |
||
| what makes "compose from polyfill methods" a safe instruction rather than a | ||
| per-method gamble, and it removes the last conditional from the law below. | ||
|
|
||
| ``` | ||
| named high-level algorithm exists -> call it | ||
| it does not exist -> COMPOSE it from ndarray polyfill methods | ||
| never -> a consumer-local scalar arithmetic path | ||
| never -> consumer-local intrinsics | ||
| ``` | ||
|
|
||
| JC tells us what is mathematically true. | ||
| ndarray tells the machine how to execute it. | ||
| Everything else is composition, never a second arithmetic implementation. | ||
|
|
||
| **For the Goursat kernel the arithmetic is entirely available; the only | ||
| unsolved part is algorithmic SCHEDULING.** `signature_kernel_pde` | ||
| (`sigker/src/kernel.rs:106-131`) computes | ||
|
|
||
| ``` | ||
| k[i+1][j+1] = k[i+1][j] + k[i][j+1] - k[i][j] + c_ij·k[i][j] | ||
| = mul_add(c_ij, diag, left + up - diag) | ||
| ``` | ||
|
|
||
| `mul_add`, `from_slice`, `copy_to_slice`, `reduce_sum`, `select` all ship on | ||
| `F64x8` today. What blocks it is that `k[i+1][j+1]` reads `k[i+1][j]` — a | ||
| strict serial recurrence along `j` in row-major order. Independence lives on | ||
| the **anti-diagonal**; the transformation needed is | ||
| `row-major serial recurrence -> anti-diagonal / rolling-wavefront formulation`. | ||
|
|
||
| **A1 determines A2's shape — the architectural reason not to jump from | ||
| `Vec<Vec>` straight to SIMD.** In a flat row-major buffer the anti-diagonal is | ||
| STRIDED (stride `m-1`), so a naive wavefront needs gather. **Three rolling | ||
| anti-diagonal buffers make every wavefront load contiguous and can eliminate | ||
| the gather entirely.** The storage decision therefore fixes which lane ops A2 | ||
| needs at all; it is not a warm-up measurement. | ||
|
|
||
| **W1.5 falsifier (do not run from a banking session):** | ||
|
|
||
| | arm | shape | relation | | ||
| |---|---|---| | ||
| | A0 | current `Vec<Vec<f64>>`, row-major | reference | | ||
| | A1 | flat / rolling storage, SAME recurrence and order | **A0 = A1 exactly** — only storage changes | | ||
| | A2 | rolling anti-diagonal traversal via `ndarray::simd::method()` | A1 <-> A2 gets a PREDECLARED solver tolerance: traversal changes evaluation order | | ||
|
|
||
| The A1<->A2 tolerance is stated in advance, never discovered after the fact. | ||
|
|
||
| **Inventory — the scalar surfaces in the same two crates.** Straightforwardly | ||
| canonical (`reduce_sum` over `mul_add`): `LogSignature::{dot, cosine}`, | ||
| `RandomizedSignature::{dot, cosine}`, `linear_path_kernel_closed_form`. | ||
| Accumulation-shaped: `signature_truncated`, `log_signature_truncated`, | ||
| `RandomizedSignature::encode`, `hydrate_signature`, `signature_kernel`, | ||
| `signature_kernel_normalized`. Combinatorial rather than arithmetic, and | ||
| plausibly staying scalar: `shuffle_product`, `enumerate_lyndon_words`, | ||
| `witt_component` / `witt_dimension`. | ||
|
|
||
| **Not shipped:** `signature_pde_sweep` and `shuffle_product_lift` return zero | ||
| hits in `ndarray/src/` — they remain the W1.5 catalogue's shopping list. The | ||
| missing thing is traversal/composition, NOT arithmetic; whether a composition | ||
| is later promoted to a named `signature_pde_sweep()` is purely an API/reuse | ||
| question, not a blocker. | ||
|
|
||
| **Pay by:** W1.5, gated on `jc Pillar 11` (green for the lattice leg, | ||
| `7751581f`). **Not W5** — W5 is workload-pressure machinery and stays HOLD | ||
| (4609 vs the 11 585-point 1 GiB threshold) regardless of any speedup here. | ||
|
|
||
| **A0/A1/A2 RUN (2026-09-02, same day — operator: "you didn't try the 25-26 | ||
| seconds with ndarray yet").** `crates/jc/examples/goursat_substrate_probe.rs`, | ||
| release, 4097-point paths (16.8M cells). Every A2 lane op is | ||
| `ndarray::simd::F64x8::mul_add` — the body is three FMAs | ||
| (`t = mul_add(1,left,up)`, `u = mul_add(-1,diag,t)`, `new = mul_add(c,diag,u)`); | ||
| no `Add`/`Sub` operator was needed, and none was minted. | ||
|
|
||
| | target-cpu | backend | A0 | A1 | A2 | A1/A2 | A0=A1 | \|A1-A2\|/A1 | | ||
| |---|---|---|---|---|---|---|---| | ||
| | generic x86-64 (no target-cpu) | scalar arm | 0.182 s | 0.169 s | **0.773 s** | **0.22x** | bit-exact | 2.431e-13 | | ||
| | x86-64-v3 (AVX2) | `f64x4` x2 | 0.163 s | 0.150 s | 0.0165 s | **9.12x** | bit-exact | 2.431e-13 | | ||
| | x86-64-v4 (AVX-512) | `__m512d` | 0.171 s | 0.157 s | 0.0182 s | **8.62x** | bit-exact | 2.431e-13 | | ||
|
|
||
| Four findings, each falsifiable and each measured: | ||
|
|
||
| 1. **Storage was NOT the wall; the recurrence was.** A1/A0 = 1.09x. The | ||
| hypothesis banked above — that a flat buffer alone would close most of the | ||
| gap — is **FALSIFIED**. The probe was built to answer that and it did. | ||
| 2. **A build with no `target-cpu` is a REGRESSION, not a no-op.** At generic x86-64 the | ||
| polyfill's scalar arm runs the wavefront 4.5x SLOWER than the flat scalar | ||
| loop: eight lanes emulated through arrays plus the wavefront bookkeeping. | ||
| Until this commit lance-graph had NO `.cargo/config.toml`, so every local | ||
| build landed there. `.cargo/config.toml` now pins v3 (CI already did via | ||
| `RUSTFLAGS` in `.github/workflows/*.yml`), with `config-avx512.toml` / | ||
| `config-native.toml` mirroring ndarray's. | ||
| 3. **Bit-exact across backends, as confirmed.** `|A1-A2|/A1` is identical to the | ||
| last digit on scalar, AVX2 and AVX-512 at every size. The delta itself | ||
| (~1e-13 at 4097) is the fused-vs-separate rounding of `c·diag` | ||
| accumulated over 16.8M cells — A2 is the MORE accurate arm. | ||
| 4. **AVX2 ~ AVX-512 here** (9.1x vs 8.6x): the wavefront is latency-bound on | ||
| the diagonal recurrence, not width-bound. Widening lanes buys nothing until | ||
| the dependency chain is restructured; that is a scheduling question, not a | ||
| substrate one. | ||
|
|
||
| **Correction (2026-09-02, same session):** an earlier revision of this entry | ||
| labelled the no-`target-cpu` row `"386"` and attributed the phrase to the | ||
| operator. That was a misreading of `x86-64-v4` in a terse message. The | ||
| measurement is unchanged; only the label was wrong and is removed above. The | ||
| commit message on `5df2d785` still carries it and is not rewritten (pushed). | ||
| Also recorded: `.cargo/config-avx512.toml`'s `sapphirerapids` is a SUPERSET of | ||
| Cascade Lake / Ice Lake silicon — this session's host is Cascade Lake (family 6 | ||
| model 0x55; `amx_report()`: `cpu_model()=OtherX86`, `expects_amx=false`). On | ||
| such hosts `x86-64-v4` or `native` is the correct pick; the probe was run with | ||
| `x86-64-v4`, so its numbers stand. | ||
|
|
||
| **SPR vs EMR — resolved (2026-09-02, operator refinement + ndarray git history):** | ||
| not the enablement, the DETECTION. Pre-PR-#217 (`src/simd_caps.rs` @ `bdf243cc`, | ||
| 2026-06-13) detected AMX by CPUID feature bits alone (`amx_tile`/`amx_int8`/ | ||
| `amx_bf16`/`amx_fp16`, EDX bits 24/25/22) — no XCR0 gate, no model table, and | ||
| the `arch_prctl` issued on syscall 157, so it always failed. PR #217 | ||
| (`e563fdcd`, 2026-06-14) replaced it with the four-gate detector (CPUID + | ||
| OSXSAVE + XCR0 + `arch_prctl` 158) PLUS the CPUID model table (`CpuModel`: | ||
| SPR 0x8F / EMR 0xCF / GNR 0xAD,0xAE / SRF 0xAF), added to tell "no silicon" | ||
| from "not OS-enabled". On SPR the old detector said *present* while nothing | ||
| ever executed (every tile test early-returned — Gotcha 9); on EMR the new | ||
| detector said *present AND enabled* and tiles ran. The `arch_prctl` grant is | ||
| the same on both; what differed was the detection code, and the change landed | ||
| on EMR silicon. `amx-enablement-and-kernel.md` §2 says this ("EMR was simply | ||
| the host where gate 4 got fixed first"); the operator's "detected differently" | ||
| is the same fact from the outside. Minor inconsistency noticed, not chased: | ||
| `cpu_ops.rs:186` says "Linux 5.19+", the doc says "5.16+". | ||
|
|
||
| Storage detail that held: with `dy` stored REVERSED, the anti-diagonal walk | ||
| is forward in `i`, so k-buffers, `dx` and `dy` are all contiguous slices — | ||
| **no gather**, exactly as predicted by "A1 determines A2's shape". | ||
|
|
||
| `jc` now depends on `ndarray` as a plain, non-optional `[dependencies]` | ||
| entry; its "zero external deps in production" header is retired (operator: | ||
| ndarray is mandatory everywhere). What stays standalone is the PROOF. | ||
|
|
||
| ## TD-GHOST-TIER-NAME-COLLISION-1 (2026-09-02) — OPEN, doc-only | ||
|
|
||
| `crates/lance-graph-contract/src/counterfactual.rs` calls the −6 minority-pole | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lattice_legchecks 4 + 12 + 36 = 52 reduced words for lengths 1 through 3;for_each_wordvisits only 4 + 16 + 64 = 84 candidates before filtering, so neither count is 484. Because this entry presents its figures as measured and uses the count to explain the runtime, the incorrect value makes the banked performance record misleading; replace it with 52 or identify what 484 is intended to count.Useful? React with 👍 / 👎.