Skip to content

Commit a04576b

Browse files
authored
Merge pull request #55 from sqliteai/perf/unify-turboquant-lut
One TurboQuant lookup implementation instead of six
2 parents a05d82a + 12f3bc5 commit a04576b

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)