Skip to content

Commit 9e6451f

Browse files
amikhail48claude
andauthored
feat: prepared-query inner product estimate, O(dim) per code (#10)
QuantizerProd::estimate_inner_product did two O(dim^2) products for every code: S * y in QJL, and Pi^T * y_hat in the MSE dequantize. Both depend only on the query once the MSE term is rewritten as <y, Pi^T y_hat> = <Pi y, y_hat> (Pi is orthogonal), so scoring a query against N codes paid N * O(dim^2). - QuantizerProd::prepare_query(y) computes Pi*y and S*y once. - QuantizerProd::estimate_inner_product(PreparedQuery, q) is O(dim) per code. - QJL::project / estimate_inner_product_projected and QuantizerMSE::inner_product_rotated are the building blocks. - The existing estimate_inner_product(y, q) now prepares and delegates, so the two entry points produce identical values. turboquant_example output is byte-identical to main (all estimates, 6 d.p.). Measured through recall's MediaIndex (100 entries, d=1536, -c opt): search 23.1 ms -> 0.56 ms on Apple Silicon, 204 ms -> 3.0 ms on Jetson Orin Nano. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 453b733 commit 9e6451f

6 files changed

Lines changed: 60 additions & 4 deletions

File tree

‎include/turboquant/qjl.h‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ class QJL {
1616
float estimate_inner_product(const Vec& y, std::span<const int8_t> signs,
1717
float residual_norm) const;
1818

19+
// Query-only half of estimate_inner_product: S * y, O(dim^2). Compute once
20+
// per query and reuse it across codes.
21+
Vec project(const Vec& y) const;
22+
23+
// estimate_inner_product given project(y): O(dim) per code.
24+
float estimate_inner_product_projected(const Vec& projected_y,
25+
std::span<const int8_t> signs,
26+
float residual_norm) const;
27+
1928
private:
2029
int dim_;
2130
Mat S_;

‎include/turboquant/quantizer_mse.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ class QuantizerMSE {
1818
QuantizedMSE quantize(const Vec& x) const;
1919
Vec dequantize(const QuantizedMSE& q) const;
2020

21+
// <y, dequantize(q)> given rotated_y = rotation() * y, in O(dim): the
22+
// rotation is orthogonal, so <y, Pi^T y_hat> = <Pi y, y_hat>.
23+
float inner_product_rotated(const Vec& rotated_y, const QuantizedMSE& q) const;
24+
2125
int dim() const { return dim_; }
2226
int bitwidth() const { return codebook_.bitwidth; }
2327
const ScalarCodebook& codebook() const { return codebook_; }

‎include/turboquant/quantizer_prod.h‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,23 @@ class QuantizerProd {
1616
public:
1717
QuantizerProd(int dim, int bitwidth, std::mt19937& rng);
1818

19+
// The query-only work of estimate_inner_product — the rotation and the QJL
20+
// projection of y, both O(dim^2). Prepare once per query, then score many
21+
// codes at O(dim) each.
22+
struct PreparedQuery {
23+
Vec rotated; // Pi * y
24+
Vec projected; // S * y
25+
};
26+
1927
QuantizedProd quantize(const Vec& x) const;
2028
Vec dequantize(const QuantizedProd& q) const;
2129
float estimate_inner_product(const Vec& y, const QuantizedProd& q) const;
2230

31+
PreparedQuery prepare_query(const Vec& y) const;
32+
// Same arithmetic as estimate_inner_product(y, q) for p = prepare_query(y),
33+
// so the two agree exactly.
34+
float estimate_inner_product(const PreparedQuery& p, const QuantizedProd& q) const;
35+
2336
int dim() const { return dim_; }
2437
int bitwidth() const { return total_bitwidth_; }
2538

‎src/qjl.cpp‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,20 @@ Vec QJL::dequantize(std::span<const int8_t> signs, float residual_norm) const {
3333

3434
float QJL::estimate_inner_product(const Vec& y, std::span<const int8_t> signs,
3535
float residual_norm) const {
36+
return estimate_inner_product_projected(project(y), signs, residual_norm);
37+
}
38+
39+
Vec QJL::project(const Vec& y) const {
40+
return S_ * y;
41+
}
42+
43+
float QJL::estimate_inner_product_projected(const Vec& projected_y,
44+
std::span<const int8_t> signs,
45+
float residual_norm) const {
3646
// Avoids full dequantization: <y, dequant> = scale * gamma * <S*y, z>
37-
Vec Sy = S_ * y;
3847
float dot = 0.0f;
3948
for (int i = 0; i < dim_; ++i)
40-
dot += Sy(i) * static_cast<float>(signs[i]);
49+
dot += projected_y(i) * static_cast<float>(signs[i]);
4150

4251
return scale_ * residual_norm * dot;
4352
}

‎src/quantizer_mse.cpp‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,12 @@ Vec QuantizerMSE::dequantize(const QuantizedMSE& q) const {
4343
return q.norm * (Pi_.transpose() * y_hat);
4444
}
4545

46+
float QuantizerMSE::inner_product_rotated(const Vec& rotated_y,
47+
const QuantizedMSE& q) const {
48+
float dot = 0.0f;
49+
for (int j = 0; j < dim_; ++j)
50+
dot += rotated_y(j) * codebook_.centroids[q.indices[j]];
51+
return q.norm * dot;
52+
}
53+
4654
} // namespace turboquant

‎src/quantizer_prod.cpp‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,21 @@ Vec QuantizerProd::dequantize(const QuantizedProd& q) const {
4242
}
4343

4444
float QuantizerProd::estimate_inner_product(const Vec& y, const QuantizedProd& q) const {
45-
float ip_mse = y.dot(mse_quantizer_.dequantize(q.mse_part));
46-
float ip_qjl = qjl_.estimate_inner_product(y, q.qjl_signs, q.residual_norm);
45+
return estimate_inner_product(prepare_query(y), q);
46+
}
47+
48+
QuantizerProd::PreparedQuery QuantizerProd::prepare_query(const Vec& y) const {
49+
return PreparedQuery{
50+
.rotated = mse_quantizer_.rotation() * y,
51+
.projected = qjl_.project(y),
52+
};
53+
}
54+
55+
float QuantizerProd::estimate_inner_product(const PreparedQuery& p,
56+
const QuantizedProd& q) const {
57+
float ip_mse = mse_quantizer_.inner_product_rotated(p.rotated, q.mse_part);
58+
float ip_qjl = qjl_.estimate_inner_product_projected(p.projected, q.qjl_signs,
59+
q.residual_norm);
4760
return ip_mse + ip_qjl;
4861
}
4962

0 commit comments

Comments
 (0)