Skip to content

Route Hamming code sizes to the fastest kernel, and accept ragged sizes - #5520

Open
mnorris11 wants to merge 1 commit into
facebookresearch:mainfrom
mnorris11:export-D115982541
Open

Route Hamming code sizes to the fastest kernel, and accept ragged sizes#5520
mnorris11 wants to merge 1 commit into
facebookresearch:mainfrom
mnorris11:export-D115982541

Conversation

@mnorris11

@mnorris11 mnorris11 commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary:
faiss::hammings() threw unless the code size was a whole number of 64-bit words, even though the other Hamming entry points accept ragged sizes. And for aligned sizes it always used the bit-level hammings_impl<nbits> kernels, never the hand-written HammingComputer family, which is faster at some sizes.

Callers needing either case had to reach past hammings() into with_simd_level_a0_spr and with_HammingComputer and hand-roll the sweep plus a routing rule. Laser does this today in KnnFaissBinaryIndex::measureAllCentroids (D115840549) and can drop it.

hammings_fixSL() now takes the HammingComputer path when the size is ragged, or when prefer_hamming_computer() says the computer wins there. That predicate is constexpr over THE_SIMD_LEVEL; sizes it does not name keep the kernels they use today.

Which sizes

Measured on Intel Cooper Lake (Xeon 8339HC) and AMD Genoa (EPYC, Zen 4), mode/opt, interleaved A/B, one code size per process, 250-600 samples per cell. Speedup from rerouting:

ncodes AVX2 Intel AVX2 AMD AVX512 Intel AVX512 AMD
16 -25% -3.0% word kernel wins, not rerouted same
32 -25% -12.6% word kernel wins, not rerouted see below
64 -47% -40.4% -83% -63%

ncodes 64 prefers the computer everywhere on x86: hamming<512> has no SIMD specialization above 256 bits, so it is a plain scalar popcount loop, and AVX-512 flags make it worse rather than better (15.0 -> 2.5 ns/pair Intel, 5.7 -> 2.1 AMD).

ncodes 8 and the runtime-nwords sizes (24, 40, 48, 56, 128, 160) keep the word kernels, which beat the computer by 46-218%.

Known regressions

ncodes 10 and 33 lose 3-5% on AMD (10: +3.2% AVX2, +4.6% AVX512; 33: +4.3% AVX512). Both are ragged sizes whose routing is unchanged — shrinking the switch shifts the layout of the code around them. Reproducible at 600 samples on two cores. Everything else is within noise or faster.

ncodes 32 at AVX512 differs by vendor

Intel prefers the word kernel there by 27%; AMD prefers the computer by 23%. The table follows Intel, so AMD gives up ~23% at that one size.

Fixing it requires a runtime predicate, which was measured and rejected: routing 32 on SIMDConfig::avx512_split gains 23% at ncodes 32 but costs 15-30% at ncodes 24/40/48/56, because prefer_hamming_computer() stops being constexpr and both paths must stay live. A compile-time Zen 4 SIMD level would capture it without that cost, but is a larger change than belongs here.

Caveat on per-size numbers

Sizes whose routing does not change still move by 5-15% between builds, both directions, both vendors: hammings_fixSL() is placement-sensitive and resizing its switch reshuffles it. ncodes 24 is the worst offender, swinging from -13.8% to +19.6% across builds differing only in unrelated code. Treat deltas outside the rerouted set as layout, not signal.

Differential Revision: D115982541

@meta-cla meta-cla Bot added the CLA Signed label Aug 14, 2026
@meta-codesync

meta-codesync Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

@mnorris11 has exported this pull request. If you are a Meta employee, you can view the originating Diff in D115982541.

@meta-codesync meta-codesync Bot changed the title relax codes%8 condition on Hamming distance relax codes%8 condition on Hamming distance (#5520) Aug 14, 2026
mnorris11 pushed a commit to mnorris11/faiss that referenced this pull request Aug 14, 2026
Summary:

`faiss::hammings()` used to `FAISS_THROW_IF_NOT(ncodes % 8 == 0)`, even though the other Hamming entry points (`hammings_knn_hc`, `hamming_range_search`) have always accepted code sizes that are not a whole number of 64-bit words. This lifts that restriction so `hammings()` matches the rest of the API.

Ragged sizes are routed to a new out-of-line helper, `hammings_ragged()`, built on the existing `HammingComputer` family (which already carries a byte tail). Multiples of 8 keep taking the word-level kernels exactly as before, so there is no behavior change for existing callers.

Also adds a `FAISS_NOINLINE` macro to `platform_macros.h` (MSVC `__declspec(noinline)` / GCC-Clang `__attribute__((noinline))`), used to keep `hammings_ragged()` out of line.

## Why FAISS_NOINLINE

Letting the compiler inline `hammings_ragged()` into `hammings_fixSL()` instantiates the whole `HammingComputer` family into that function and roughly doubles its size (3,597 -> 7,565 bytes at the AVX2 level). That measurably slows the word-level code sizes that share the runtime-nwords loop.

Benchmarked on a Xeon 8339HC (Cooper Lake, AVX-512), `mode/opt`, na=256 x nb=4096 = 1M pairs per call, one code size per process, A/B interleaved with order flipping, 300-400 samples per cell, replicated on two cores. Positive = inlining is slower, i.e. `noinline` wins.

| ncodes | static AVX2 | dynamic dispatch (AVX512) |
| --- | --- | --- |
| 8, 16, 32, 64, 128 | ~0% | ~0% |
| 24 | +3.4% | +53% |
| 40 | +5.0% | +22% |
| 48 | ~0% | +28% |
| 56 | +4.4% | +35% |
| 12, 20, 33 (ragged) | -5% | -5% to -12% |

Two distinct mechanisms, both confirmed in the disassembly:

- **Static AVX2** - the inlined version has higher register pressure and spills, adding exactly one reload per pair in the shared outer loop. Retired instruction counts (noise-free) show +1.00 instr/pair at ncodes 24/40/48/56 and 0.00 at 8/16/32/64/128.
- **Dynamic dispatch** - the popcount inner loop is a byte-identical 24-byte sequence in both builds, but out of line it starts at `%32 == 0` (one 32-byte fetch window) and inlined at `%32 == 16` (straddles two). IPC drops 3.02 -> 2.36 at identical clock. This one is a code-placement effect: it is real today but is not something `noinline` reliably controls, and it could invert on a compiler upgrade.

Note that the dedicated `ncodes` 8/16/32/64 kernels are unaffected either way - they have their own specialized `hammings_impl<nbits>` blocks that the ragged inline never touches.

The trade: the ragged path itself is ~5% slower for being out of line. That is accepted here because the word-level sizes are the common case and are the ones that were already supported.

Differential Revision: D115982541
@meta-codesync meta-codesync Bot changed the title relax codes%8 condition on Hamming distance (#5520) Route Hamming code sizes to the fastest kernel, and accept ragged sizes Aug 15, 2026
Summary:
`faiss::hammings()` threw unless the code size was a whole number of 64-bit words, even though the other Hamming entry points accept ragged sizes. And for aligned sizes it always used the bit-level `hammings_impl<nbits>` kernels, never the hand-written `HammingComputer` family, which is faster at some sizes.

Callers needing either case had to reach past `hammings()` into `with_simd_level_a0_spr` and `with_HammingComputer` and hand-roll the sweep plus a routing rule. Laser does this today in `KnnFaissBinaryIndex::measureAllCentroids` (D115840549) and can drop it.

`hammings_fixSL()` now takes the `HammingComputer` path when the size is ragged, or when `prefer_hamming_computer()` says the computer wins there. That predicate is `constexpr` over `THE_SIMD_LEVEL`; sizes it does not name keep the kernels they use today.

## Which sizes

Measured on Intel Cooper Lake (Xeon 8339HC) and AMD Genoa (EPYC, Zen 4), `mode/opt`, interleaved A/B, one code size per process, 250-600 samples per cell. Speedup from rerouting:

| ncodes | AVX2 Intel | AVX2 AMD | AVX512 Intel | AVX512 AMD |
| --- | --- | --- | --- | --- |
| 16 | -25% | -3.0% | word kernel wins, not rerouted | same |
| 32 | -25% | -12.6% | word kernel wins, not rerouted | see below |
| 64 | -47% | -40.4% | **-83%** | **-63%** |

ncodes 64 prefers the computer everywhere on x86: `hamming<512>` has no SIMD specialization above 256 bits, so it is a plain scalar popcount loop, and AVX-512 flags make it worse rather than better (15.0 -> 2.5 ns/pair Intel, 5.7 -> 2.1 AMD).

ncodes 8 and the runtime-nwords sizes (24, 40, 48, 56, 128, 160) keep the word kernels, which beat the computer by 46-218%.

## Known regressions

ncodes 10 and 33 lose 3-5% on AMD (10: +3.2% AVX2, +4.6% AVX512; 33: +4.3% AVX512). Both are ragged sizes whose routing is unchanged — shrinking the switch shifts the layout of the code around them. Reproducible at 600 samples on two cores. Everything else is within noise or faster.

## ncodes 32 at AVX512 differs by vendor

Intel prefers the word kernel there by 27%; AMD prefers the computer by 23%. The table follows Intel, so AMD gives up ~23% at that one size.

Fixing it requires a runtime predicate, which was measured and rejected: routing 32 on `SIMDConfig::avx512_split` gains 23% at ncodes 32 but costs 15-30% at ncodes 24/40/48/56, because `prefer_hamming_computer()` stops being `constexpr` and both paths must stay live. A compile-time Zen 4 SIMD level would capture it without that cost, but is a larger change than belongs here.

## Caveat on per-size numbers

Sizes whose routing does not change still move by 5-15% between builds, both directions, both vendors: `hammings_fixSL()` is placement-sensitive and resizing its switch reshuffles it. ncodes 24 is the worst offender, swinging from -13.8% to +19.6% across builds differing only in unrelated code. Treat deltas outside the rerouted set as layout, not signal.

Differential Revision: D115982541
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant