Skip to content

Commit 4a52ad4

Browse files
Michael Norrisfacebook-github-bot
authored andcommitted
Route Hamming code sizes to the fastest kernel, and accept ragged sizes
Summary: `faiss::hammings()` had two problems. It threw unless the code size was a whole number of 64-bit words, even though the other Hamming entry points (`hammings_knn_hc`, `hamming_range_search`) have always accepted ragged sizes. And for aligned sizes it always used the bit-level `hammings_impl<nbits>` kernels, never the hand-written `HammingComputer` family, which is substantially faster at some sizes. Together those forced callers who cared about either case to reach past `hammings()` into `with_simd_level_a0_spr` and `with_HammingComputer` — faiss internals — and hand-roll the sweep plus a routing rule. Laser does exactly this today in `KnnFaissBinaryIndex::measureAllCentroids` (D115840549), and can drop all of it after this. `hammings_fixSL` now takes the `HammingComputer` path when the size is ragged, or when `prefer_hamming_computer()` says the computer beats the word kernel there. That predicate is `constexpr` over `THE_SIMD_LEVEL`, because which sizes qualify depends on the ISA. Sizes it does not name keep the kernels they use today, so aligned sizes with no dedicated computer (24, 40, 48, 56, 128, ...) are unaffected by design. ## Which sizes, and why Both kernels measured in one process, interleaved rep by rep, at na=1/nb=65536 and na=256/nb=4096. Negative means the computer wins. | ncodes | static AVX2 | dynamic dispatch (AVX512) | | --- | --- | --- | | 8 | kernel wins 46% | kernel wins 202% | | 16 | **computer wins 25%** | kernel wins 45% | | 32 | **computer wins 23%** | kernel wins 27% | | 64 | **computer wins 42%** | **computer wins 73%** | | 4, 20 | tie — already routed to the computer | tie | | 24/40/48/56/128/160 | kernel wins 32-98% | kernel wins 42-144% | So only 64 qualifies at every level. `hamming<512>` has no SIMD specialization above 256 bits on x86 — it is a plain scalar popcount loop — and compiling it with AVX-512 flags makes it markedly worse rather than better (15.0 ns/pair, versus 6.7 under AVX2). Size 8 never qualifies, which is worth noting because Laser's current rule assumes it does. ## FAISS_NOINLINE removed An earlier version of this diff put `FAISS_NOINLINE` on the ragged helper, because inlining it doubled `hammings_fixSL` and slowed the sizes sharing the runtime-nwords loop. Re-measuring after the routing change, that no longer reproduces anywhere: with 16/32/64 moved off the switch, inlining is neutral-to-better in both build modes (ncodes 24: -10.1% static, -3.1% DD; ragged sizes -3 to -12%). The attribute and its `platform_macros.h` macro are dropped; nothing else in faiss used it. ## Net effect Versus the previous state of this diff. Interleaved A/B, one code size per process, 300 samples per cell. | ncodes | static AVX2 | dynamic dispatch (AVX512) | | --- | --- | --- | | 16 | **-25.2%** | -0.5% | | 32 | **-25.2%** | -0.2% | | 64 | **-47.3%** | **-83.3%** (15.0 -> 2.50 ns/pair) | | 12 / 20 / 33 (ragged) | -7.3 / -11.8 / -5.1% | -0.2 / -10.5 / -4.0% | | 8, 40, 48, 56, 128 | within noise (<=1.6%) | within noise (<=2.6%) | | **24** | **+19.6%** | -3.5% | ## Caveats **ncodes 24, static AVX2, +19.6%.** Not explained. Retired instructions per pair are identical (36.86 vs 36.83), both hot loops are 32-byte aligned, and DSB uops, uops issued/retired, resource stalls, memory stalls, 4K aliasing and branch mispredicts are all unchanged; the same instruction sequence simply takes 28% more cycles. This size has swung 30-50% from unrelated code motion three separate times during this work, so the working theory is that its runtime-nwords loop is unusually placement-sensitive on this core and the change landed it badly. Plausible follow-up is to give that path its own out-of-line function so it stops sharing layout with the switch; not attempted here. **Measured only on Intel Cooper Lake** (Xeon Platinum 8339HC), `mode/opt`. `prefer_hamming_computer()` is a per-ISA table, and AVX-512 behaves quite differently on AMD Zen, so the AVX512 rows should be re-measured before relying on them there. Levels that were not measured (NONE, NEON, SVE, RVV) deliberately keep their current kernels. AVX512_SPR is grouped with AVX512 on the mechanism above rather than on a measurement, since no SPR host was available. Differential Revision: D115982541
1 parent 1f93154 commit 4a52ad4

3 files changed

Lines changed: 82 additions & 3 deletions

File tree

faiss/utils/hamming.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ FAISS_API extern size_t hamming_batch_size;
111111
*
112112
* @param a size na * nbytespercode
113113
* @param b size nb * nbytespercode
114-
* @param nbytespercode should be multiple of 8
114+
* @param nbytespercode any size; multiples of 8 take a faster kernel
115115
* @param dis output distances, size na * nb
116116
*/
117117
void hammings(

faiss/utils/hamming_distance/hamming_impl.h

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,41 @@ void generalized_hammings_knn_hc_impl(
298298
}
299299
}
300300

301+
void hammings_with_computer(
302+
const uint8_t* __restrict a,
303+
const uint8_t* __restrict b,
304+
size_t na,
305+
size_t nb,
306+
size_t ncodes,
307+
hamdis_t* __restrict dis) {
308+
with_HammingComputer<THE_SIMD_LEVEL>(ncodes, [&]<class HammingComputer>() {
309+
for (size_t i = 0; i < na; i++) {
310+
HammingComputer hc(a + i * ncodes, ncodes);
311+
const uint8_t* bi = b;
312+
for (size_t j = 0; j < nb; j++) {
313+
dis[i * nb + j] = hc.hamming(bi);
314+
bi += ncodes;
315+
}
316+
}
317+
});
318+
}
319+
320+
// Word-multiple sizes whose HammingComputer beats the kernel below; measured
321+
// per SIMD level on x86, unmeasured levels keep the kernels. See diff summary.
322+
constexpr bool prefer_hamming_computer(size_t ncodes) {
323+
constexpr bool is_avx512 = THE_SIMD_LEVEL == SIMDLevel::AVX512 ||
324+
THE_SIMD_LEVEL == SIMDLevel::AVX512_SPR;
325+
constexpr bool is_avx2 = THE_SIMD_LEVEL == SIMDLevel::AVX2;
326+
327+
if (ncodes == 64) {
328+
return is_avx2 || is_avx512;
329+
}
330+
if (ncodes == 16 || ncodes == 32) {
331+
return is_avx2;
332+
}
333+
return false;
334+
}
335+
301336
} // anonymous namespace
302337

303338
/******************************************************************
@@ -364,7 +399,10 @@ void hammings_fixSL<THE_SIMD_LEVEL>(
364399
size_t nb,
365400
size_t ncodes,
366401
hamdis_t* dis) {
367-
FAISS_THROW_IF_NOT(ncodes % 8 == 0);
402+
if (ncodes % 8 != 0 || prefer_hamming_computer(ncodes)) {
403+
hammings_with_computer(a, b, na, nb, ncodes, dis);
404+
return;
405+
}
368406
switch (ncodes) {
369407
case 8:
370408
hammings_impl<64>(C64(a), C64(b), na, nb, dis);

tests/test_hamming.cpp

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ TEST(TestHamming, test_hamming_knn) {
316316
ASSERT_EQ(dist_ham_knn, *true_bit_distances) << assert_str.str();
317317
}
318318

319-
for (auto code_size : {8, 16, 24, 32}) {
319+
for (auto code_size : {8, 16, 24, 32, 64}) {
320320
std::stringstream assert_str = get_correct_hamming_example(
321321
na,
322322
nb,
@@ -333,3 +333,44 @@ TEST(TestHamming, test_hamming_knn) {
333333
EXPECT_EQ(dist_gen, *true_bit_distances) << assert_str.str();
334334
}
335335
}
336+
337+
// Code sizes that are not a whole number of 64-bit words take the byte-tail
338+
// kernel rather than the word-level one. The knn entry points have always
339+
// accepted them; hammings() used to throw instead.
340+
TEST(TestHamming, test_hammings_ragged_code_size) {
341+
std::default_random_engine rng(123);
342+
std::uniform_int_distribution<int32_t> uniform(0, 255);
343+
344+
const size_t na = 3;
345+
const size_t nb = 7;
346+
347+
for (auto code_size : {1, 2, 5, 12, 20, 33}) {
348+
std::vector<uint8_t> a(na * code_size);
349+
std::vector<uint8_t> b(nb * code_size);
350+
for (auto& v : a) {
351+
v = uniform(rng);
352+
}
353+
for (auto& v : b) {
354+
v = uniform(rng);
355+
}
356+
357+
// Reference: byte-wise popcount of the XOR, independent of faiss.
358+
std::vector<hamdis_t> expected(na * nb);
359+
for (size_t i = 0; i < na; ++i) {
360+
for (size_t j = 0; j < nb; ++j) {
361+
int d = 0;
362+
for (int c = 0; c < code_size; ++c) {
363+
d += __builtin_popcount(
364+
static_cast<unsigned>(
365+
a[i * code_size + c] ^
366+
b[j * code_size + c]));
367+
}
368+
expected[i * nb + j] = d;
369+
}
370+
}
371+
372+
std::vector<hamdis_t> dis(na * nb);
373+
faiss::hammings(a.data(), b.data(), na, nb, code_size, dis.data());
374+
EXPECT_EQ(dis, expected) << "code_size = " << code_size;
375+
}
376+
}

0 commit comments

Comments
 (0)