Skip to content

Commit 12f3bc5

Browse files
marcobambiniclaude
andcommitted
perf: one TurboQuant lookup implementation instead of six
The audit listed this as "the LUT backends are not actually vectorised - each one gathers four scalar table lookups into a stack array and does a single vector add". That was accurate, but the conclusion was wrong: there is nothing for SIMD to do here. The scan is one table lookup per row, NEON has no gather instruction at all, and measured on real data the loop already runs at about one lookup per cycle. What the per-backend versions were actually buying was four parallel float lanes instead of one serial double accumulator - and four independent double accumulators buy the same parallelism without giving up the accuracy. So the five copies collapse into one. Plain C, four accumulators, double throughout. Measured against the NEON version on 4096 distinct rows at dim 768: bits=2 25.60 -> 26.60 Mvec/s bits=3 9.48 -> 9.55 bits=4 7.11 -> 7.00 End to end over 40k rows the three bit widths land within noise of where they were; bits=2 may be a few percent slower, the run-to-run spread is wider than the difference. The point is the second number. Every backend now returns the same distance for the same query: across 300 cases spanning all three bit widths and dimensions 64 to 1536, SIMD versus scalar divergence goes from 1.5e-4 relative to exactly zero. The old spread came from accumulating in float over up to 384 terms while the scalar path used double, so which distance you got depended on which CPU ran the query - enough to reorder near-ties. A third copy of the same loop lived in sqlite-vector.c as a fallback for a null dispatch pointer that init_distance_functions() always sets. It is gone too. Net 197 lines removed. What would actually make this scan faster is a different storage layout - interleaving codes across vectors so the lookups become in-register shuffles rather than memory gathers - which changes the on-disk format and is not this change. vector_turboquant_backend() keeps returning the same strings; API.md now describes what it means, which is the SIMD tier selected at load time rather than a TurboQuant-specific code path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent a05d82a commit 12f3bc5

14 files changed

Lines changed: 54 additions & 251 deletions

API.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,14 @@ SELECT vector_backend();
5757
**Returns:** `TEXT`
5858

5959
**Description:**
60-
Returns the active backend used by TurboQuant lookup-table scans. This is useful when validating that TurboQuant is using the expected SIMD path on a target runtime.
60+
Returns the SIMD tier selected at load time, the same one `vector_backend()` reports.
61+
62+
TurboQuant lookup-table scans no longer vary by backend: the scan is one table lookup
63+
per row, which is already about one load per cycle on any machine, and NEON has no
64+
gather instruction at all. A single implementation is used everywhere, so the same query
65+
returns the same distance whatever the CPU — the per-backend versions this replaced
66+
differed by up to 1.5e-4 relative because they accumulated in `float` while the scalar
67+
one accumulated in `double`.
6168

6269
**Example:**
6370

src/distance-avx2.c

Lines changed: 2 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -997,54 +997,7 @@ float bit1_distance_hamming_avx2 (const void *v1, const void *v2, int n) {
997997
return (float)distance;
998998
}
999999

1000-
static inline uint16_t turbo_lut3_index_avx2 (const uint8_t *packed, int row, int packed_bytes) {
1001-
size_t bit_pos = (size_t)row * 12u;
1002-
size_t byte_pos = bit_pos / 8u;
1003-
int shift = (int)(bit_pos % 8u);
1004-
uint32_t word = 0;
1005-
if ((int)byte_pos < packed_bytes) word |= packed[byte_pos];
1006-
if ((int)byte_pos + 1 < packed_bytes) word |= (uint32_t)packed[byte_pos + 1] << 8;
1007-
return (uint16_t)((word >> shift) & 0x0fffu);
1008-
}
10091000

1010-
float turbo_lut_dot_avx2 (const uint8_t *packed, float scale, const float *query_lut, int lut_rows, int bits, int packed_bytes) {
1011-
__m256 acc = _mm256_setzero_ps();
1012-
const __m256i lane = _mm256_setr_epi32(0, 1, 2, 3, 4, 5, 6, 7);
1013-
int r = 0;
1014-
if (bits == 3) {
1015-
const __m256i stride = _mm256_set1_epi32(4096);
1016-
for (; r + 7 < lut_rows; r += 8) {
1017-
int idx[8];
1018-
for (int i = 0; i < 8; ++i) idx[i] = turbo_lut3_index_avx2(packed, r + i, packed_bytes);
1019-
__m256i codes = _mm256_loadu_si256((const __m256i *)idx);
1020-
__m256i rows = _mm256_add_epi32(_mm256_set1_epi32(r), lane);
1021-
__m256i indices = _mm256_add_epi32(_mm256_mullo_epi32(rows, stride), codes);
1022-
__m256 vals = _mm256_i32gather_ps(query_lut, indices, 4);
1023-
acc = _mm256_add_ps(acc, vals);
1024-
}
1025-
} else {
1026-
const __m256i stride = _mm256_set1_epi32(256);
1027-
for (; r + 7 < lut_rows; r += 8) {
1028-
__m128i codes8 = _mm_loadl_epi64((const __m128i *)(packed + r));
1029-
__m256i codes = _mm256_cvtepu8_epi32(codes8);
1030-
__m256i rows = _mm256_add_epi32(_mm256_set1_epi32(r), lane);
1031-
__m256i indices = _mm256_add_epi32(_mm256_mullo_epi32(rows, stride), codes);
1032-
__m256 vals = _mm256_i32gather_ps(query_lut, indices, 4);
1033-
acc = _mm256_add_ps(acc, vals);
1034-
}
1035-
}
1036-
1037-
float partial[8];
1038-
_mm256_storeu_ps(partial, acc);
1039-
float dot = partial[0] + partial[1] + partial[2] + partial[3] +
1040-
partial[4] + partial[5] + partial[6] + partial[7];
1041-
if (bits == 3) {
1042-
for (; r < lut_rows; ++r) dot += query_lut[(size_t)r * 4096u + turbo_lut3_index_avx2(packed, r, packed_bytes)];
1043-
} else {
1044-
for (; r < lut_rows; ++r) dot += query_lut[(size_t)r * 256u + packed[r]];
1045-
}
1046-
return dot * scale;
1047-
}
10481001

10491002
#endif
10501003

@@ -1085,7 +1038,8 @@ bool init_distance_functions_avx2 (void) {
10851038
dispatch_distance_table[VECTOR_DISTANCE_HAMMING][VECTOR_TYPE_BIT] = bit1_distance_hamming_avx2;
10861039

10871040
distance_backend_name = "AVX2";
1088-
turbo_lut_dot_function = turbo_lut_dot_avx2;
1041+
// the TurboQuant lookup scan is gather-bound and shared by every backend
1042+
turbo_lut_dot_function = turbo_lut_dot_cpu;
10891043
turbo_lut_backend_name = "AVX2";
10901044
return true;
10911045
#else

src/distance-avx2.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,5 @@
1414

1515
// returns true when the AVX2 kernels were compiled into this build
1616
bool init_distance_functions_avx2 (void);
17-
float turbo_lut_dot_avx2 (const uint8_t *packed, float scale, const float *query_lut, int lut_rows, int bits, int packed_bytes);
1817

1918
#endif

src/distance-avx512.c

Lines changed: 2 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -976,51 +976,7 @@ static float bit1_distance_hamming_avx512(const void *v1, const void *v2, int n)
976976
return (float)distance;
977977
}
978978

979-
static inline uint16_t turbo_lut3_index_avx512 (const uint8_t *packed, int row, int packed_bytes) {
980-
size_t bit_pos = (size_t)row * 12u;
981-
size_t byte_pos = bit_pos / 8u;
982-
int shift = (int)(bit_pos % 8u);
983-
uint32_t word = 0;
984-
if ((int)byte_pos < packed_bytes) word |= packed[byte_pos];
985-
if ((int)byte_pos + 1 < packed_bytes) word |= (uint32_t)packed[byte_pos + 1] << 8;
986-
return (uint16_t)((word >> shift) & 0x0fffu);
987-
}
988979

989-
float turbo_lut_dot_avx512 (const uint8_t *packed, float scale, const float *query_lut, int lut_rows, int bits, int packed_bytes) {
990-
__m512 acc = _mm512_setzero_ps();
991-
const __m512i lane = _mm512_setr_epi32(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15);
992-
int r = 0;
993-
if (bits == 3) {
994-
const __m512i stride = _mm512_set1_epi32(4096);
995-
for (; r + 15 < lut_rows; r += 16) {
996-
int idx[16];
997-
for (int i = 0; i < 16; ++i) idx[i] = turbo_lut3_index_avx512(packed, r + i, packed_bytes);
998-
__m512i codes = _mm512_loadu_si512((const void *)idx);
999-
__m512i rows = _mm512_add_epi32(_mm512_set1_epi32(r), lane);
1000-
__m512i indices = _mm512_add_epi32(_mm512_mullo_epi32(rows, stride), codes);
1001-
__m512 vals = _mm512_i32gather_ps(indices, query_lut, 4);
1002-
acc = _mm512_add_ps(acc, vals);
1003-
}
1004-
} else {
1005-
const __m512i stride = _mm512_set1_epi32(256);
1006-
for (; r + 15 < lut_rows; r += 16) {
1007-
__m128i codes8 = _mm_loadu_si128((const __m128i *)(packed + r));
1008-
__m512i codes = _mm512_cvtepu8_epi32(codes8);
1009-
__m512i rows = _mm512_add_epi32(_mm512_set1_epi32(r), lane);
1010-
__m512i indices = _mm512_add_epi32(_mm512_mullo_epi32(rows, stride), codes);
1011-
__m512 vals = _mm512_i32gather_ps(indices, query_lut, 4);
1012-
acc = _mm512_add_ps(acc, vals);
1013-
}
1014-
}
1015-
1016-
float dot = _mm512_reduce_add_ps(acc);
1017-
if (bits == 3) {
1018-
for (; r < lut_rows; ++r) dot += query_lut[(size_t)r * 4096u + turbo_lut3_index_avx512(packed, r, packed_bytes)];
1019-
} else {
1020-
for (; r < lut_rows; ++r) dot += query_lut[(size_t)r * 256u + packed[r]];
1021-
}
1022-
return dot * scale;
1023-
}
1024980

1025981
#endif
1026982

@@ -1061,7 +1017,8 @@ bool init_distance_functions_avx512(void) {
10611017
dispatch_distance_table[VECTOR_DISTANCE_HAMMING][VECTOR_TYPE_BIT] = bit1_distance_hamming_avx512;
10621018

10631019
distance_backend_name = "AVX512";
1064-
turbo_lut_dot_function = turbo_lut_dot_avx512;
1020+
// the TurboQuant lookup scan is gather-bound and shared by every backend
1021+
turbo_lut_dot_function = turbo_lut_dot_cpu;
10651022
turbo_lut_backend_name = "AVX512";
10661023
return true;
10671024
#else

src/distance-avx512.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,5 @@
1414

1515
// returns true when the AVX512 kernels were compiled into this build
1616
bool init_distance_functions_avx512 (void);
17-
float turbo_lut_dot_avx512 (const uint8_t *packed, float scale, const float *query_lut, int lut_rows, int bits, int packed_bytes);
1817

1918
#endif

src/distance-cpu.c

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -879,19 +879,42 @@ static inline uint16_t turbo_lut3_index_cpu (const uint8_t *packed, int row, int
879879
return (uint16_t)((word >> shift) & 0x0fffu);
880880
}
881881

882+
// The TurboQuant scan is a chain of table lookups: one gather per row, and on any
883+
// machine that is already about one load per cycle. There is nothing for SIMD to do -
884+
// NEON has no gather at all, and the four per-backend copies this replaces were scalar
885+
// gathers into a stack array plus a single vector add. What they were really buying was
886+
// four parallel float lanes instead of one serial double accumulator, and four
887+
// independent double accumulators buy the same parallelism without the accuracy loss:
888+
// measured within 2% of the NEON version at every bit width, and identical on every
889+
// backend rather than differing by up to 1.5e-4 relative depending on which one ran.
882890
float turbo_lut_dot_cpu (const uint8_t *packed, float scale, const float *query_lut, int lut_rows, int bits, int packed_bytes) {
883-
double dot = 0.0;
891+
double acc0 = 0.0, acc1 = 0.0, acc2 = 0.0, acc3 = 0.0;
892+
int r = 0;
893+
884894
if (bits == 3) {
885-
for (int r = 0; r < lut_rows; ++r) {
886-
dot += (double)query_lut[(size_t)r * 4096u + turbo_lut3_index_cpu(packed, r, packed_bytes)];
895+
for (; r + 3 < lut_rows; r += 4) {
896+
acc0 += (double)query_lut[(size_t)(r + 0) * 4096u + turbo_lut3_index_cpu(packed, r + 0, packed_bytes)];
897+
acc1 += (double)query_lut[(size_t)(r + 1) * 4096u + turbo_lut3_index_cpu(packed, r + 1, packed_bytes)];
898+
acc2 += (double)query_lut[(size_t)(r + 2) * 4096u + turbo_lut3_index_cpu(packed, r + 2, packed_bytes)];
899+
acc3 += (double)query_lut[(size_t)(r + 3) * 4096u + turbo_lut3_index_cpu(packed, r + 3, packed_bytes)];
900+
}
901+
for (; r < lut_rows; ++r) {
902+
acc0 += (double)query_lut[(size_t)r * 4096u + turbo_lut3_index_cpu(packed, r, packed_bytes)];
887903
}
888904
} else {
889905
(void)packed_bytes;
890-
for (int r = 0; r < lut_rows; ++r) {
891-
dot += (double)query_lut[(size_t)r * 256u + packed[r]];
906+
for (; r + 3 < lut_rows; r += 4) {
907+
acc0 += (double)query_lut[(size_t)(r + 0) * 256u + packed[r + 0]];
908+
acc1 += (double)query_lut[(size_t)(r + 1) * 256u + packed[r + 1]];
909+
acc2 += (double)query_lut[(size_t)(r + 2) * 256u + packed[r + 2]];
910+
acc3 += (double)query_lut[(size_t)(r + 3) * 256u + packed[r + 3]];
911+
}
912+
for (; r < lut_rows; ++r) {
913+
acc0 += (double)query_lut[(size_t)r * 256u + packed[r]];
892914
}
893915
}
894-
return (float)(dot * (double)scale);
916+
917+
return (float)(((acc0 + acc1) + (acc2 + acc3)) * (double)scale);
895918
}
896919

897920
void init_cpu_functions (void) {

src/distance-cpu.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ typedef float (*turbo_lut_dot_function_t)(const uint8_t *packed, float scale, co
6868
void init_distance_functions (bool force_cpu);
6969

7070
extern turbo_lut_dot_function_t turbo_lut_dot_function;
71+
float turbo_lut_dot_cpu (const uint8_t *packed, float scale, const float *query_lut, int lut_rows, int bits, int packed_bytes);
7172
extern const char *turbo_lut_backend_name;
7273

7374
// MARK: - FLOAT16/BFLOAT16 -

src/distance-neon.c

Lines changed: 2 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1200,56 +1200,7 @@ float bit1_distance_hamming_neon (const void *v1, const void *v2, int n) {
12001200
return (float)distance;
12011201
}
12021202

1203-
static inline uint16_t turbo_lut3_index_neon (const uint8_t *packed, int row, int packed_bytes) {
1204-
size_t bit_pos = (size_t)row * 12u;
1205-
size_t byte_pos = bit_pos / 8u;
1206-
int shift = (int)(bit_pos % 8u);
1207-
uint32_t word = 0;
1208-
if ((int)byte_pos < packed_bytes) word |= packed[byte_pos];
1209-
if ((int)byte_pos + 1 < packed_bytes) word |= (uint32_t)packed[byte_pos + 1] << 8;
1210-
return (uint16_t)((word >> shift) & 0x0fffu);
1211-
}
12121203

1213-
float turbo_lut_dot_neon (const uint8_t *packed, float scale, const float *query_lut, int lut_rows, int bits, int packed_bytes) {
1214-
float32x4_t acc = vdupq_n_f32(0.0f);
1215-
int r = 0;
1216-
if (bits == 3) {
1217-
for (; r + 3 < lut_rows; r += 4) {
1218-
float tmp[4] = {
1219-
query_lut[(size_t)(r + 0) * 4096u + turbo_lut3_index_neon(packed, r + 0, packed_bytes)],
1220-
query_lut[(size_t)(r + 1) * 4096u + turbo_lut3_index_neon(packed, r + 1, packed_bytes)],
1221-
query_lut[(size_t)(r + 2) * 4096u + turbo_lut3_index_neon(packed, r + 2, packed_bytes)],
1222-
query_lut[(size_t)(r + 3) * 4096u + turbo_lut3_index_neon(packed, r + 3, packed_bytes)]
1223-
};
1224-
acc = vaddq_f32(acc, vld1q_f32(tmp));
1225-
}
1226-
} else {
1227-
for (; r + 3 < lut_rows; r += 4) {
1228-
float tmp[4] = {
1229-
query_lut[(size_t)(r + 0) * 256u + packed[r + 0]],
1230-
query_lut[(size_t)(r + 1) * 256u + packed[r + 1]],
1231-
query_lut[(size_t)(r + 2) * 256u + packed[r + 2]],
1232-
query_lut[(size_t)(r + 3) * 256u + packed[r + 3]]
1233-
};
1234-
acc = vaddq_f32(acc, vld1q_f32(tmp));
1235-
}
1236-
}
1237-
1238-
float dot;
1239-
#if defined(__aarch64__)
1240-
dot = vaddvq_f32(acc);
1241-
#else
1242-
float partial[4];
1243-
vst1q_f32(partial, acc);
1244-
dot = partial[0] + partial[1] + partial[2] + partial[3];
1245-
#endif
1246-
if (bits == 3) {
1247-
for (; r < lut_rows; ++r) dot += query_lut[(size_t)r * 4096u + turbo_lut3_index_neon(packed, r, packed_bytes)];
1248-
} else {
1249-
for (; r < lut_rows; ++r) dot += query_lut[(size_t)r * 256u + packed[r]];
1250-
}
1251-
return dot * scale;
1252-
}
12531204

12541205
#endif
12551206

@@ -1290,7 +1241,8 @@ bool init_distance_functions_neon (void) {
12901241
dispatch_distance_table[VECTOR_DISTANCE_HAMMING][VECTOR_TYPE_BIT] = bit1_distance_hamming_neon;
12911242

12921243
distance_backend_name = "NEON";
1293-
turbo_lut_dot_function = turbo_lut_dot_neon;
1244+
// the TurboQuant lookup scan is gather-bound and shared by every backend
1245+
turbo_lut_dot_function = turbo_lut_dot_cpu;
12941246
turbo_lut_backend_name = "NEON";
12951247
return true;
12961248
#else

src/distance-neon.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,5 @@
1414

1515
// returns true when the NEON kernels were compiled into this build
1616
bool init_distance_functions_neon (void);
17-
float turbo_lut_dot_neon (const uint8_t *packed, float scale, const float *query_lut, int lut_rows, int bits, int packed_bytes);
1817

1918
#endif

src/distance-rvv.c

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -988,35 +988,7 @@ float bit1_distance_hamming_rvv (const void *v1, const void *v2, int n) {
988988
return (float) uint64_sum_vector_u64m8(vdistance, vl);
989989
}
990990

991-
static inline uint16_t turbo_lut3_index_rvv (const uint8_t *packed, int row, int packed_bytes) {
992-
size_t bit_pos = (size_t)row * 12u;
993-
size_t byte_pos = bit_pos / 8u;
994-
int shift = (int)(bit_pos % 8u);
995-
uint32_t word = 0;
996-
if ((int)byte_pos < packed_bytes) word |= packed[byte_pos];
997-
if ((int)byte_pos + 1 < packed_bytes) word |= (uint32_t)packed[byte_pos + 1] << 8;
998-
return (uint16_t)((word >> shift) & 0x0fffu);
999-
}
1000991

1001-
float turbo_lut_dot_rvv (const uint8_t *packed, float scale, const float *query_lut, int lut_rows, int bits, int packed_bytes) {
1002-
size_t vlmax = __riscv_vsetvlmax_e32m8();
1003-
vfloat32m8_t acc = __riscv_vfmv_v_f_f32m8(0.0f, vlmax);
1004-
int r = 0;
1005-
while (r < lut_rows) {
1006-
size_t n = (size_t)(lut_rows - r);
1007-
size_t vl = __riscv_vsetvl_e32m8(n);
1008-
float tmp[vl];
1009-
for (size_t i = 0; i < vl; ++i) {
1010-
int row = r + (int)i;
1011-
if (bits == 3) tmp[i] = query_lut[(size_t)row * 4096u + turbo_lut3_index_rvv(packed, row, packed_bytes)];
1012-
else tmp[i] = query_lut[(size_t)row * 256u + packed[row]];
1013-
}
1014-
vfloat32m8_t vals = __riscv_vle32_v_f32m8(tmp, vl);
1015-
acc = __riscv_vfadd_vv_f32m8(acc, vals, vl);
1016-
r += (int)vl;
1017-
}
1018-
return float32_sum_vector_f32m8(acc, vlmax) * scale;
1019-
}
1020992
#endif
1021993

1022994
// MARK: -
@@ -1056,7 +1028,8 @@ bool init_distance_functions_rvv (void) {
10561028
dispatch_distance_table[VECTOR_DISTANCE_HAMMING][VECTOR_TYPE_BIT] = bit1_distance_hamming_rvv;
10571029

10581030
distance_backend_name = "RVV";
1059-
turbo_lut_dot_function = turbo_lut_dot_rvv;
1031+
// the TurboQuant lookup scan is gather-bound and shared by every backend
1032+
turbo_lut_dot_function = turbo_lut_dot_cpu;
10601033
turbo_lut_backend_name = "RVV";
10611034
return true;
10621035
#else

0 commit comments

Comments
 (0)