Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .cargo/config-v2.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# x86-64-v2 (SSE2 through SSE4.2, no AVX) — the scalar realization, which LLVM
# vectorizes to SSE.
#
# Usage:
# env -u RUSTFLAGS cargo --config .cargo/config-v2.toml <cmd>
# bash scripts/masking-parity.sh v2-qemu (runs under qemu -cpu Nehalem)
#
# Selects the arm `all(target_arch = "x86_64", target_feature = "sse4.2",
# not(target_feature = "avx"))`: Nehalem / Westmere, Silvermont-class Atom,
# VMs that hide AVX. `simd.rs` routes it to `simd_scalar.rs`. Plan:
# `.claude/plans/simd-sse-v2-tier-v1.md`. Overlay semantics as in
# `config-v3.toml`: only the target-cpu is set here; the crypto cfgs come from
# `.cargo/config.toml` through cargo's rustflags join. `env -u RUSTFLAGS` is
# load-bearing for the same reason.
[target.'cfg(target_arch = "x86_64")']
rustflags = ["-Ctarget-cpu=x86-64-v2"]
11 changes: 11 additions & 0 deletions .claude/blackboard.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
## 2026-10-10 — x86-64-v2 realization (SSE4.2 without AVX) via the scalar backend

- **What:** an eighth realization, selected at compile time by `all(target_arch = "x86_64", target_feature = "sse4.2", not(target_feature = "avx"))`. You get it with `target-cpu=native` on a v2-only CPU (Nehalem/Westmere, Silvermont-class Atom, VMs that hide AVX) or with `.cargo/config-v2.toml`. `simd.rs` routes this arm to `simd_scalar.rs`: the AVX2/AVX-512 types wrap 256/512-bit registers and cannot be the v2 realization, so plain arrays that LLVM compiles to SSE are used instead. Baseline x86-64 builds (no SSE4.2) are unchanged: they keep the AVX2 arm and the `cpu_guard` AVX2 floor, which now exempts `sse4.2 && !avx2` (the AVX and v2 arms together).
- **API gap closed in `simd_scalar.rs`:** `F32Mask16::to_bitmask` and `F32x16::gather` (unsafe, same contract as the AVX2 polyfill). sentinel-qa: `gather` CONDITIONAL, satisfied by the SAFETY comment added at the one call site in `hpc/cam_pq.rs`; `to_bitmask` PARITY-OK.
- **Evidence:**
- `scripts/masking-parity.sh v2-qemu` passes all 15 groups under `qemu-x86_64-static -cpu Nehalem`. The objdump witness finds no `%ymm`, `%zmm`, mask register, or VEX op on xmm. Disable run: with the `simd.rs` routing reverted, the binary SIGILLs under Nehalem.
- Per-test sweep of all 2541 v2 lib tests, each as its own process under Nehalem: 28 SIGILLs, all in `simd_avx2`'s own test modules (`tests`, `u8x32_tests`, `f16_precision_tests`), which call AVX2 intrinsics directly. No library path faulted. Those modules are gated off v2 (36 tests). Full v2 suite afterwards: 2474 passed, 0 failed, 31 ignored.
- Clippy `-D warnings` clean on v2, v3, v4, avx and baseline. Codegen witness avx2/avx512 PASS. fmt clean.
- New CI row `realization/v2` runs the parity arm, the objdump witness and the SIMD unit tests under Nehalem.
- **Open:** `crate::simd_avx2` stays public on v2 builds and nothing checks for AVX2 at startup there, so a direct caller SIGILLs. Documented in the `simd.rs` module doc; the safe path is the `crate::simd` re-exports.

## 2026-10-10 — `tests/splat3d_correctness.rs`: clippy 1.99 clean under `--features splat3d`

- Five lints, all in the test file: an unneeded `mut` on a closure, an over-precise f32 literal (`0.28209479177387814` → `0.282_094_8`; same bits, `0x3e906ebb`), and three `chunks_exact(3)` → `as_chunks::<3>().0.iter()`.
Expand Down
21 changes: 16 additions & 5 deletions .claude/plans/simd-sse-v2-tier-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,27 @@ goes through `crate::simd`, so the switch is confined to `simd.rs`.

## Work items

- [ ] P0 TESTS FIRST: `v2-qemu` parity arm (`--config .cargo/config-v2.toml`,
- [x] P0 TESTS FIRST: `v2-qemu` parity arm (`--config .cargo/config-v2.toml`,
run under `qemu-x86_64-static -cpu Nehalem`) in `scripts/masking-parity.sh`
and a `realization/v2` row in `simd-matrix.yaml`, with an objdump witness
(no AVX instruction at all: no `%ymm`, no VEX `v`-prefixed op). Expect it
to fail before the arm exists.
- [ ] P1 `.cargo/config-v2.toml`; `simd.rs` routing; `cpu_guard` floor gated off
- [x] P1 `.cargo/config-v2.toml`; `simd.rs` routing; `cpu_guard` floor gated off
the v2 arm.
- [ ] P2 close the API gap: compile v2, list every missing item, add each to
- [x] P2 close the API gap: compile v2, list every missing item, add each to
`simd_scalar.rs` (types `U8x32`, `F32Mask16`, `F64Mask8` and the x86-only
methods). Safe Rust only.
- [ ] P3 gates: lib tests on v2 under qemu Nehalem; parity on every arm; clippy
- [x] P3 gates: lib tests on v2 under qemu Nehalem; parity on every arm; clippy
v2/v3/v4/avx/baseline; codegen witness avx2/avx512 unchanged.
- [ ] P4 docs: CLAUDE.md realization list, blackboard, `simd.rs` dispatch comment.
- [x] P4 docs: CLAUDE.md realization list, blackboard, `simd.rs` dispatch comment.

## Outcome (2026-10-10)

- Per-test sweep: all 2541 v2 lib tests, each run as its own process under
`qemu-x86_64-static -cpu Nehalem`. 28 faulted with SIGILL, every one in
`simd_avx2`'s own test modules (`tests`, `u8x32_tests`,
`f16_precision_tests`), which call AVX2 intrinsics directly. No library
path faulted. Those three modules are now gated off the v2 arm.
- Open: `crate::simd_avx2` stays public on v2 builds and has no startup
check there, so a direct caller faults with SIGILL. Documented in the
`simd.rs` module doc; the safe path is the `crate::simd` re-exports.
42 changes: 42 additions & 0 deletions .github/workflows/simd-matrix.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,48 @@ jobs:
# the multithreaded harness while a test was panicking.
qemu-x86_64-static -cpu SandyBridge "$bin" simd --test-threads=1

v2:
# x86-64-v2 realization (SSE4.2, no AVX): `simd.rs` routes it to the
# scalar backend, which LLVM vectorizes to SSE. Built for `x86-64-v2`
# (`.cargo/config-v2.toml`) and RUN under `qemu-x86_64-static -cpu
# Nehalem`, so any AVX instruction on the executed path faults. Three
# rungs, as for `avx`: parity, an assembly witness that the parity binary
# has no AVX encoding at all, and the SIMD unit tests under emulation.
runs-on: ubuntu-latest
name: realization/v2 × x86_64
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
key: v2
- name: install qemu-user
run: sudo apt-get update && sudo apt-get install -y qemu-user-static
- name: masking parity (x86-64-v2 build, qemu -cpu Nehalem)
run: bash scripts/masking-parity.sh v2-qemu
- name: no AVX instruction in the parity binary
run: |
bin=crates/simd-masking-parity/target/release/simd-masking-parity
# Nehalem has no AVX: no ymm/zmm or mask register, and no VEX-encoded
# (v-prefixed) op on xmm either. The parity binary has no
# runtime-gated paths, so any hit came from an ungated intrinsic.
bad=$(objdump -d --no-show-raw-insn "$bin" | grep -E '%ymm|%zmm|%k[0-7]|\sv[a-z0-9]+\s.*%xmm' || true)
if [ -n "$bad" ]; then echo "$bad" | head -20; echo "::error::AVX instruction in the v2 parity binary"; exit 1; fi
echo "no AVX instruction found"
- name: SIMD unit tests under qemu -cpu Nehalem
env:
CARGO_PROFILE_DEV_DEBUG: "0"
CARGO_PROFILE_TEST_DEBUG: "0"
CARGO_PROFILE_DEV_OPT_LEVEL: "1"
CARGO_PROFILE_TEST_OPT_LEVEL: "1"
run: |
bin=$(env -u RUSTFLAGS cargo --config .cargo/config-v2.toml test --lib --no-run --message-format=json \
| jq -r 'select(.executable != null and .target.kind == ["lib"] and .target.name == "ndarray") | .executable')
# One thread: see the `avx` job.
qemu-x86_64-static -cpu Nehalem "$bin" simd --test-threads=1

neon:
# aarch64 realization: cross-build on the x86 runner, run under qemu-user.
# Three rungs: parity under qemu (bits), the codegen witness (opt-3 NEON
Expand Down
16 changes: 9 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ src/
### The parity arms are ALL reachable here — never report one as blocked without apt

`scripts/masking-parity.sh` takes `native | nightly | wasm | wasm-scalar |
neon-qemu | avx-qemu`, and the cross arms are an `apt-get` away, not an environment
neon-qemu | avx-qemu | v2-qemu`, and the cross arms are an `apt-get` away, not an environment
limit. Measured 2026-09-16: `neon-qemu` failed with a bare
`No such file or directory (os error 2)` — which reads as "this target is not
available here" and is in fact a **missing linker**, then a missing
Expand Down Expand Up @@ -239,12 +239,14 @@ env -u RUSTFLAGS cargo --config ../../.cargo/config-v4.toml run --release

Read the program's own header line to confirm which arm you actually got
(`avx512f=true`, `neon=true`, …) — that line exists precisely because the
config can silently not apply. Six of the seven realizations are reachable
without nightly (AVX2, AVX-512, AVX-without-AVX2, NEON, wasm-simd128,
wasm-scalar); the AVX arm (`src/simd_avx.rs`, `.cargo/config-avx.toml`) runs
under `qemu-x86_64-static -cpu SandyBridge` (`apt-get install -y
qemu-user-static`); only
`nightly-simd` needs a toolchain this repo does not pin. The same lesson the
config can silently not apply. Seven of the eight realizations are reachable
without nightly (AVX2, AVX-512, AVX-without-AVX2, x86-64-v2, NEON,
wasm-simd128, wasm-scalar). The AVX arm (`src/simd_avx.rs`,
`.cargo/config-avx.toml`) runs under `qemu-x86_64-static -cpu SandyBridge`;
the v2 arm (SSE4.2 without AVX, routed to `simd_scalar.rs`,
`.cargo/config-v2.toml`) runs under `-cpu Nehalem` (both need `apt-get install
-y qemu-user-static`). Only `nightly-simd` needs a toolchain this repo does
not pin. The same lesson the
sibling `lance-graph-java` records for the JDK: **a stale index or a missing
helper binary reporting absence is not evidence of absence.**

Expand Down
11 changes: 10 additions & 1 deletion scripts/masking-parity.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@
# scripts/masking-parity.sh wasm-scalar wasm32 WITHOUT simd128 under node = the scalar arm
# scripts/masking-parity.sh neon-qemu aarch64 cross-build run under qemu-aarch64-static
# scripts/masking-parity.sh avx-qemu AVX-without-AVX2 build (config-avx) under qemu -cpu SandyBridge
# scripts/masking-parity.sh v2-qemu x86-64-v2 build (config-v2, scalar realization) under qemu -cpu Nehalem
#
# Extra cargo arguments (e.g. `--config .cargo/config-v4.toml` for the v4
# realization on the native row) pass through CARGO_ARGS, see
# scripts/codegen-witness.sh for why that form and not the env var.
set -euo pipefail
ARM="${1:?native|nightly|wasm|wasm-scalar|neon-qemu|avx-qemu}"
ARM="${1:?native|nightly|wasm|wasm-scalar|neon-qemu|avx-qemu|v2-qemu}"
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
MANIFEST="$ROOT/crates/simd-masking-parity/Cargo.toml"
TD="${CARGO_TARGET_DIR:-$ROOT/crates/simd-masking-parity/target}"
Expand Down Expand Up @@ -70,6 +71,14 @@ case "$ARM" in
env -u RUSTFLAGS cargo ${CARGO_ARGS:-} --config "$ROOT/.cargo/config-avx.toml" build --release --manifest-path "$MANIFEST" --bin simd-masking-parity
"$QEMU" -cpu SandyBridge "$TD/release/simd-masking-parity"
;;
v2-qemu)
# The x86-64-v2 arm: SSE4.2, no AVX, routed to the scalar realization.
# Built for x86-64-v2 and run on an emulated Nehalem, so an AVX
# instruction anywhere in the binary faults instead of passing silently.
QEMU="${QEMU_X86_64:-qemu-x86_64-static}"
env -u RUSTFLAGS cargo ${CARGO_ARGS:-} --config "$ROOT/.cargo/config-v2.toml" build --release --manifest-path "$MANIFEST" --bin simd-masking-parity
"$QEMU" -cpu Nehalem "$TD/release/simd-masking-parity"
;;
*) echo "unknown arm: $ARM"; exit 2 ;;
esac
echo "masking parity ($ARM): PASS"
25 changes: 15 additions & 10 deletions src/cpu_guard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ impl fmt::Display for BuildCpuMismatch {
f,
" This build uses ndarray's AVX2 SIMD backend. For a CPU with AVX but \
not AVX2, rebuild with `target-cpu=native` on it or with \
`--config .cargo/config-avx.toml`; a CPU without AVX is not supported."
`--config .cargo/config-avx.toml`; for one with SSE4.2 but not AVX, \
`--config .cargo/config-v2.toml`. A CPU without SSE4.2 is not supported."
)
} else {
write!(
Expand Down Expand Up @@ -274,12 +275,14 @@ const FEATURES: &[FeatureRow] = {

/// The floor of `crate::simd`'s AVX2 realization (see the module docs):
/// required on every x86_64 build that compiles that realization in, whatever
/// its target features. The one x86_64 build that does not is the
/// AVX-without-AVX2 arm (`simd_avx.rs`, `.cargo/config-avx.toml`), whose
/// `avx` requirement the compiled-feature table already enforces.
/// its target features. Two x86_64 builds do not, and the compiled-feature
/// table already enforces what each needs: the AVX-without-AVX2 arm
/// (`simd_avx.rs`, `.cargo/config-avx.toml`) and the x86-64-v2 arm
/// (SSE4.2 without AVX, the scalar realization, `.cargo/config-v2.toml`).
/// Together they are exactly `sse4.2` without `avx2`.
#[cfg(target_arch = "x86_64")]
const BACKEND_FLOOR: &[FeatureRow] =
&[("avx2", cfg!(not(all(target_feature = "avx", not(target_feature = "avx2")))), || {
&[("avx2", cfg!(not(all(target_feature = "sse4.2", not(target_feature = "avx2")))), || {
cpuid::bit(7, 0, 'b', 5) && cpuid::os_avx()
})];

Expand Down Expand Up @@ -448,21 +451,23 @@ mod tests {

#[test]
#[cfg(target_arch = "x86_64")]
fn avx2_is_required_unless_the_avx_arm_is_compiled_in() {
// A baseline build (no `avx2` compiled in) still reaches the AVX2 SIMD
// backend, so the floor applies to it; only the AVX-without-AVX2 arm
// is exempt, because it never selects that backend.
fn avx2_is_required_unless_the_avx_or_v2_arm_is_compiled_in() {
// A baseline build (no `sse4.2` compiled in) still reaches the AVX2
// SIMD backend, so the floor applies to it; the AVX-without-AVX2 arm
// and the x86-64-v2 arm are exempt, because neither selects it.
let avx_arm = cfg!(all(target_feature = "avx", not(target_feature = "avx2")));
let v2_arm = cfg!(all(target_feature = "sse4.2", not(target_feature = "avx")));
assert!(BACKEND_FLOOR
.iter()
.any(|&(n, compiled, _)| n == "avx2" && compiled == !avx_arm));
.any(|&(n, compiled, _)| n == "avx2" && compiled == !(avx_arm || v2_arm)));
}

#[test]
fn the_message_explains_the_avx2_floor_only_when_avx2_is_missing() {
let floor = BuildCpuMismatch { missing: vec!["avx2"] }.to_string();
assert!(floor.contains("AVX2 SIMD backend"), "{floor}");
assert!(floor.contains("config-avx.toml"), "{floor}");
assert!(floor.contains("config-v2.toml"), "{floor}");
let other = BuildCpuMismatch {
missing: vec!["avx512f"],
}
Expand Down
5 changes: 5 additions & 0 deletions src/hpc/cam_pq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,11 @@ impl DistanceTables {

// Gather distances from precomputed table
let base_ptr = self.tables[s].as_ptr();
// SAFETY: every index is a `u8` code (`CamFingerprint = [u8; 6]`),
// so 0..=255 < NUM_CENTROIDS, and `base_ptr` starts the
// `[f32; NUM_CENTROIDS]` table `self.tables[s]` (`s <
// NUM_SUBSPACES`): each offset lands on an initialised, aligned
// `f32` in that one array.
let distances = F32x16::gather(indices, base_ptr);

acc = acc + distances;
Expand Down
Loading
Loading