Skip to content

Commit baeac24

Browse files
authored
Merge pull request #353 from AdaWorldAPI/ccr-9f3b8075-74u47l
simd: x86-64-v2 realization (SSE4.2 without AVX) via the scalar backend
2 parents db4daa8 + 6ca7efd commit baeac24

11 files changed

Lines changed: 239 additions & 41 deletions

File tree

‎.cargo/config-v2.toml‎

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

‎.claude/blackboard.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
## 2026-10-10 — x86-64-v2 realization (SSE4.2 without AVX) via the scalar backend
2+
3+
- **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).
4+
- **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.
5+
- **Evidence:**
6+
- `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.
7+
- 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.
8+
- Clippy `-D warnings` clean on v2, v3, v4, avx and baseline. Codegen witness avx2/avx512 PASS. fmt clean.
9+
- New CI row `realization/v2` runs the parity arm, the objdump witness and the SIMD unit tests under Nehalem.
10+
- **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.
11+
112
## 2026-10-10 — `tests/splat3d_correctness.rs`: clippy 1.99 clean under `--features splat3d`
213

314
- 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()`.

‎.claude/plans/simd-sse-v2-tier-v1.md‎

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,27 @@ goes through `crate::simd`, so the switch is confined to `simd.rs`.
3030

3131
## Work items
3232

33-
- [ ] P0 TESTS FIRST: `v2-qemu` parity arm (`--config .cargo/config-v2.toml`,
33+
- [x] P0 TESTS FIRST: `v2-qemu` parity arm (`--config .cargo/config-v2.toml`,
3434
run under `qemu-x86_64-static -cpu Nehalem`) in `scripts/masking-parity.sh`
3535
and a `realization/v2` row in `simd-matrix.yaml`, with an objdump witness
3636
(no AVX instruction at all: no `%ymm`, no VEX `v`-prefixed op). Expect it
3737
to fail before the arm exists.
38-
- [ ] P1 `.cargo/config-v2.toml`; `simd.rs` routing; `cpu_guard` floor gated off
38+
- [x] P1 `.cargo/config-v2.toml`; `simd.rs` routing; `cpu_guard` floor gated off
3939
the v2 arm.
40-
- [ ] P2 close the API gap: compile v2, list every missing item, add each to
40+
- [x] P2 close the API gap: compile v2, list every missing item, add each to
4141
`simd_scalar.rs` (types `U8x32`, `F32Mask16`, `F64Mask8` and the x86-only
4242
methods). Safe Rust only.
43-
- [ ] P3 gates: lib tests on v2 under qemu Nehalem; parity on every arm; clippy
43+
- [x] P3 gates: lib tests on v2 under qemu Nehalem; parity on every arm; clippy
4444
v2/v3/v4/avx/baseline; codegen witness avx2/avx512 unchanged.
45-
- [ ] P4 docs: CLAUDE.md realization list, blackboard, `simd.rs` dispatch comment.
45+
- [x] P4 docs: CLAUDE.md realization list, blackboard, `simd.rs` dispatch comment.
46+
47+
## Outcome (2026-10-10)
48+
49+
- Per-test sweep: all 2541 v2 lib tests, each run as its own process under
50+
`qemu-x86_64-static -cpu Nehalem`. 28 faulted with SIGILL, every one in
51+
`simd_avx2`'s own test modules (`tests`, `u8x32_tests`,
52+
`f16_precision_tests`), which call AVX2 intrinsics directly. No library
53+
path faulted. Those three modules are now gated off the v2 arm.
54+
- Open: `crate::simd_avx2` stays public on v2 builds and has no startup
55+
check there, so a direct caller faults with SIGILL. Documented in the
56+
`simd.rs` module doc; the safe path is the `crate::simd` re-exports.

‎.github/workflows/simd-matrix.yaml‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,48 @@ jobs:
266266
# the multithreaded harness while a test was panicking.
267267
qemu-x86_64-static -cpu SandyBridge "$bin" simd --test-threads=1
268268
269+
v2:
270+
# x86-64-v2 realization (SSE4.2, no AVX): `simd.rs` routes it to the
271+
# scalar backend, which LLVM vectorizes to SSE. Built for `x86-64-v2`
272+
# (`.cargo/config-v2.toml`) and RUN under `qemu-x86_64-static -cpu
273+
# Nehalem`, so any AVX instruction on the executed path faults. Three
274+
# rungs, as for `avx`: parity, an assembly witness that the parity binary
275+
# has no AVX encoding at all, and the SIMD unit tests under emulation.
276+
runs-on: ubuntu-latest
277+
name: realization/v2 × x86_64
278+
steps:
279+
- uses: actions/checkout@v4
280+
with:
281+
persist-credentials: false
282+
- uses: dtolnay/rust-toolchain@stable
283+
- uses: Swatinem/rust-cache@v2
284+
with:
285+
key: v2
286+
- name: install qemu-user
287+
run: sudo apt-get update && sudo apt-get install -y qemu-user-static
288+
- name: masking parity (x86-64-v2 build, qemu -cpu Nehalem)
289+
run: bash scripts/masking-parity.sh v2-qemu
290+
- name: no AVX instruction in the parity binary
291+
run: |
292+
bin=crates/simd-masking-parity/target/release/simd-masking-parity
293+
# Nehalem has no AVX: no ymm/zmm or mask register, and no VEX-encoded
294+
# (v-prefixed) op on xmm either. The parity binary has no
295+
# runtime-gated paths, so any hit came from an ungated intrinsic.
296+
bad=$(objdump -d --no-show-raw-insn "$bin" | grep -E '%ymm|%zmm|%k[0-7]|\sv[a-z0-9]+\s.*%xmm' || true)
297+
if [ -n "$bad" ]; then echo "$bad" | head -20; echo "::error::AVX instruction in the v2 parity binary"; exit 1; fi
298+
echo "no AVX instruction found"
299+
- name: SIMD unit tests under qemu -cpu Nehalem
300+
env:
301+
CARGO_PROFILE_DEV_DEBUG: "0"
302+
CARGO_PROFILE_TEST_DEBUG: "0"
303+
CARGO_PROFILE_DEV_OPT_LEVEL: "1"
304+
CARGO_PROFILE_TEST_OPT_LEVEL: "1"
305+
run: |
306+
bin=$(env -u RUSTFLAGS cargo --config .cargo/config-v2.toml test --lib --no-run --message-format=json \
307+
| jq -r 'select(.executable != null and .target.kind == ["lib"] and .target.name == "ndarray") | .executable')
308+
# One thread: see the `avx` job.
309+
qemu-x86_64-static -cpu Nehalem "$bin" simd --test-threads=1
310+
269311
neon:
270312
# aarch64 realization: cross-build on the x86 runner, run under qemu-user.
271313
# Three rungs: parity under qemu (bits), the codegen witness (opt-3 NEON

‎CLAUDE.md‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ src/
196196
### The parity arms are ALL reachable here — never report one as blocked without apt
197197

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

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

‎scripts/masking-parity.sh‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@
99
# scripts/masking-parity.sh wasm-scalar wasm32 WITHOUT simd128 under node = the scalar arm
1010
# scripts/masking-parity.sh neon-qemu aarch64 cross-build run under qemu-aarch64-static
1111
# scripts/masking-parity.sh avx-qemu AVX-without-AVX2 build (config-avx) under qemu -cpu SandyBridge
12+
# scripts/masking-parity.sh v2-qemu x86-64-v2 build (config-v2, scalar realization) under qemu -cpu Nehalem
1213
#
1314
# Extra cargo arguments (e.g. `--config .cargo/config-v4.toml` for the v4
1415
# realization on the native row) pass through CARGO_ARGS, see
1516
# scripts/codegen-witness.sh for why that form and not the env var.
1617
set -euo pipefail
17-
ARM="${1:?native|nightly|wasm|wasm-scalar|neon-qemu|avx-qemu}"
18+
ARM="${1:?native|nightly|wasm|wasm-scalar|neon-qemu|avx-qemu|v2-qemu}"
1819
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
1920
MANIFEST="$ROOT/crates/simd-masking-parity/Cargo.toml"
2021
TD="${CARGO_TARGET_DIR:-$ROOT/crates/simd-masking-parity/target}"
@@ -70,6 +71,14 @@ case "$ARM" in
7071
env -u RUSTFLAGS cargo ${CARGO_ARGS:-} --config "$ROOT/.cargo/config-avx.toml" build --release --manifest-path "$MANIFEST" --bin simd-masking-parity
7172
"$QEMU" -cpu SandyBridge "$TD/release/simd-masking-parity"
7273
;;
74+
v2-qemu)
75+
# The x86-64-v2 arm: SSE4.2, no AVX, routed to the scalar realization.
76+
# Built for x86-64-v2 and run on an emulated Nehalem, so an AVX
77+
# instruction anywhere in the binary faults instead of passing silently.
78+
QEMU="${QEMU_X86_64:-qemu-x86_64-static}"
79+
env -u RUSTFLAGS cargo ${CARGO_ARGS:-} --config "$ROOT/.cargo/config-v2.toml" build --release --manifest-path "$MANIFEST" --bin simd-masking-parity
80+
"$QEMU" -cpu Nehalem "$TD/release/simd-masking-parity"
81+
;;
7382
*) echo "unknown arm: $ARM"; exit 2 ;;
7483
esac
7584
echo "masking parity ($ARM): PASS"

‎src/cpu_guard.rs‎

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@ impl fmt::Display for BuildCpuMismatch {
9898
f,
9999
" This build uses ndarray's AVX2 SIMD backend. For a CPU with AVX but \
100100
not AVX2, rebuild with `target-cpu=native` on it or with \
101-
`--config .cargo/config-avx.toml`; a CPU without AVX is not supported."
101+
`--config .cargo/config-avx.toml`; for one with SSE4.2 but not AVX, \
102+
`--config .cargo/config-v2.toml`. A CPU without SSE4.2 is not supported."
102103
)
103104
} else {
104105
write!(
@@ -274,12 +275,14 @@ const FEATURES: &[FeatureRow] = {
274275

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

@@ -448,21 +451,23 @@ mod tests {
448451

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

461465
#[test]
462466
fn the_message_explains_the_avx2_floor_only_when_avx2_is_missing() {
463467
let floor = BuildCpuMismatch { missing: vec!["avx2"] }.to_string();
464468
assert!(floor.contains("AVX2 SIMD backend"), "{floor}");
465469
assert!(floor.contains("config-avx.toml"), "{floor}");
470+
assert!(floor.contains("config-v2.toml"), "{floor}");
466471
let other = BuildCpuMismatch {
467472
missing: vec!["avx512f"],
468473
}

‎src/hpc/cam_pq.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,11 @@ impl DistanceTables {
248248

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

253258
acc = acc + distances;

0 commit comments

Comments
 (0)