Skip to content

Route the AVX2 search kernel through the ndarray::simd polyfill - #4

Merged
AdaWorldAPI merged 9 commits into
mainfrom
squash/our-work-on-upstream-2026-10-29
Oct 29, 2026
Merged

AdaWorldAPI merged 9 commits into
mainfrom
squash/our-work-on-upstream-2026-10-29

Conversation

@AdaWorldAPI

@AdaWorldAPI AdaWorldAPI commented Oct 29, 2026 •

Copy link
Copy Markdown
Owner

Related issue

None. This re-applies the fork's own work after main was reset to upstream 1.1.2 (the old history is kept on backup/main-pre-upstream-sync-2026-10-29 and -b).

Summary

  • search_multi_query_avx2 now uses only the ndarray::simd facade and no raw intrinsics. It works 64 bytes wide:
    • One U8x64 load covers two adjacent byte groups, for both the codes and the LUT. In the blocked layout the two groups are contiguous, so nothing is copied.
    • shuffle_bytes does the lookups.
    • U16x32::from_u8x64_lo/hi widen into the u16 accumulators, which replaces the SUB trick.
    • Each FLUSH_EVERY batch is flushed with U16x32::to_f32x16_lo/hi and F32x16::mul_add.
    • F32x16::simd_gt prunes against the heap minimum.
  • FORCE_AVX2_PATH forces the AVX2 kernel on AVX-512 hosts. It is available in tests and under the new bench-internals feature.
  • examples/kernel_speed.rs times the native, AVX2 and scalar paths on one index.
  • Adds the ndarray fork as a dependency: x86_64 only, default-features = false, no BLAS.
  • Moves to toolchain 1.99.0, which the ndarray fork requires:
    • rust-toolchain.toml;
    • rust-version in both crates (the MSRV job requires the two to match);
    • the CI clippy pin.
    • Clippy 1.99 adds two lint classes that fire in existing code (chunks_exact_to_as_chunks, manual_isolate_lowest_one), so both are added to the allow-list, as the CI comment asks for a pin bump.
  • The old commits' BLAS/build.rs changes are dropped because upstream no longer depends on ndarray or BLAS.

Motivation

The AVX2 body is the slot where the polyfill replaces hand-written intrinsics. Which realization runs is left to ndarray::simd.

Test plan

  • avx2_kernel_reference drives search_multi_query_avx2 directly on synthetic codes and LUTs. It checks the results bit for bit against a scalar reference that reads the blocked layout independently. The cases cover:

    • two flush batches and a partial block;
    • a slot mask with four queries;
    • a heap that never fills.

    It does not depend on which code layout the host picks.

  • avx2_ndarray_simd_parity (with TURBOVEC_NO_VECTOR_MAJOR=1) compares forced-AVX2 search with the default kernel. It checks top-k ids and scores bit for bit at dim 256 and at dim 1024 with n=1000. A test-only call counter asserts that the forced search really reached the AVX2 kernel and that the default one did not. Otherwise the test could compare the AVX-512 kernel with itself.

  • A disable run (writing scores in byte order instead of slot order) turns the parity test red.

  • cargo mutants --in-diff locally: both dispatch mutants (!force_avx2 &&) are caught. They survived before the call counter was added. cargo-mutants does not mutate the kernel itself, since it is an unsafe fn. The other 16 mutants replace the whole scan_with_luts body, which any search test catches.

  • cargo test -p turbovec --release: every lib test passes except v7_delta_tests::a_failed_sync_recovers_via_full_write, both with and without TURBOVEC_NO_VECTOR_MAJOR. That test relies on a read-only file refusing writes, which does not happen when running as root, as in this sandbox. It passes in CI.

  • The CI clippy command with its flags passes on x86_64 (--workspace) and aarch64. It also passes on x86_64 with --features bench-internals,turbovec9-lab.

  • CI is green, including:

    • Rust and Python on all three OSes;
    • MSRV, Clippy, cargo-deny and the changelog check;
    • the mutants job.

    The review workflow is the exception. It needs CLAUDE_BOT_TOKEN and CLAUDE_CODE_OAUTH_TOKEN, which this fork does not have.

Speed, from kernel_speed (n=20000, dim=512, nq=256, k=10, 4-bit, AVX-512 host, ns/query):

build native AVX-512 AVX2 kernel
upstream raw-intrinsics AVX2 kernel, default build (x86-64-v2) 40 449 71 516
this PR, default build (x86-64-v2) 38 782 862 599
this PR, -Ctarget-cpu=x86-64-v3 38 379 852 745
this PR, -Ctarget-cpu=x86-64-v4 39 830 82 390

Recall@10 is 0.6434 on every path. The AVX-512 kernel, which production uses on AVX-512 hosts, is unchanged. On a default or v3 build, the polyfilled AVX2 kernel is about 12× slower than the raw kernel it replaces. With AVX-512 enabled it is about 15% slower. The difference comes from how the facade's 64-byte types are realized below AVX-512, which is ndarray::simd's concern. This PR changes nothing there.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XAqKMMjKnig6MXTd41Lf2v

Re-applies this fork's work on top of upstream 1.1.2, as one commit. The
backup branches backup/main-pre-upstream-sync-2026-10-29{,-b} keep the
original history.

search_multi_query_avx2 now uses only the ndarray::simd facade, 64 bytes
wide: one U8x64 load covers two adjacent byte groups (codes and LUT),
shuffle_bytes does the lookups, U16x32::from_u8x64_lo/hi widen into the
u16 accumulators (replacing the SUB trick), U16x32::to_f32x16_lo/hi +
F32x16::mul_add flush each FLUSH_EVERY batch, and F32x16::simd_gt prunes
against the heap minimum. Scores are bit-identical to the AVX-512 kernel.

Also:
- FORCE_AVX2_PATH forces the AVX2 kernel on AVX-512 hosts (tests and the
  bench-internals feature); avx2_ndarray_simd_parity compares it with the
  default kernel bit for bit, including a two-batch dim and a partial
  block.
- examples/kernel_speed.rs times the native, AVX2 and scalar paths.
- ndarray fork dependency (x86_64 only, no BLAS), currently on the
  claude/u16x32-facade-gaps branch until AdaWorldAPI/ndarray#385 merges.
- Toolchain 1.99.0, which the ndarray fork requires: rust-toolchain.toml,
  rust-version in both crates, and the CI clippy pin, with the two lint
  classes 1.99 adds to existing code allow-listed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XAqKMMjKnig6MXTd41Lf2v
…he changelog

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XAqKMMjKnig6MXTd41Lf2v
@AdaWorldAPI
AdaWorldAPI force-pushed the squash/our-work-on-upstream-2026-10-29 branch from c0c2dad to b447edc Compare October 29, 2026 09:33
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XAqKMMjKnig6MXTd41Lf2v
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XAqKMMjKnig6MXTd41Lf2v
The index-level parity test only runs where the classic layout is picked,
so on hosts that take the vector-major path it never reaches the kernel.
This drives search_multi_query_avx2 on synthetic codes and LUTs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XAqKMMjKnig6MXTd41Lf2v
cargo-mutants only recognises a bare #[cfg(test)]; under
cfg(all(test, target_arch = "x86_64")) it mutated the test helpers
themselves (about 110 extra mutants).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XAqKMMjKnig6MXTd41Lf2v
Both kernels return bit-identical scores, so the parity test could not
tell a forced AVX2 search from a second AVX-512 one. A test-only call
counter in search_multi_query_avx2 makes the routing observable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XAqKMMjKnig6MXTd41Lf2v
…ing-free

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XAqKMMjKnig6MXTd41Lf2v
@AdaWorldAPI
AdaWorldAPI marked this pull request as ready for review October 29, 2026 12:10
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-29T12:12:23.343642Z 0d957d1 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Copy link
Copy Markdown
Owner Author

The review check (pr-review.yml) fails on its first step with gh: To use GitHub CLI in a GitHub Actions workflow, set the GH_TOKEN environment variable. That step reads secrets.CLAUDE_BOT_TOKEN, and the action reads CLAUDE_CODE_OAUTH_TOKEN; neither secret is set on this fork, so GH_TOKEN is empty. The failure comes from the fork's repository configuration, not from this PR. It goes away once those two secrets are added, or once that workflow is disabled here. The "Agent code review" status depends on that workflow, so it stays red for the same reason. Every other check on 0d957d1 is green.


Generated by Claude Code


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0d957d1dae

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread turbovec/Cargo.toml Resolved
… is merged

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XAqKMMjKnig6MXTd41Lf2v
@AdaWorldAPI
AdaWorldAPI merged commit ec05a52 into main Oct 29, 2026
23 of 27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants