PQ L2 distance kernel for f32 dense vectors, the asymmetric-distance compute
that runs on every IvfPq / IvfHnswPq ANN query in Lance.
Landed. kernels.rs starts as a clone of upstream's
compute_pq_distance + build_distance_table_l2 via lance-snapshots.
The agent's job is to find generalizable speedups on top of upstream SOTA.
Two functions in crates/pq-l2/src/kernels.rs, mirroring upstream's API split:
PqKernel::distance_table(query, &mut out), writes the asymmetric distance table ([num_sub_vectors][num_centroids]) for one query against the codebook into a caller-provided&mut [f32]buffer. The bench pre-allocates and reuses one buffer per workload so allocator cost stays out of the per-query timing. Cost:num_sub_vectors × num_centroids × sub_vector_dimMAC ops per query. Mirrors upstream'sbuild_distance_table_l2.PqKernel::compute_distances(table, &mut out), for each PQ-encoded vector, accumulates the L2 distance vianum_sub_vectorstable lookups and writesnum_vectorsdistances into the caller-provided&mut [f32]. Cost:num_vectors × num_sub_vectorslookups per query. Dominant cost at typical scales. Mirrors upstream'scompute_pq_distance.
Top-K selection is external to the kernel (in run_experiment.rs),
matching upstream's split.
PqKernel::new(shape, codebook, codes, num_vectors) is also editable ,
the agent may pre-process anything (codebook transpose, codes transpose,
L2Prepared SoA layout, cached c·c, packed LUTs) and amortize over
queries; build cost is excluded from per-query timing.
Upstream lance-format/lance at SHA 5cf70b27b3ad38ecdcd1547b7af385e05f67598a,
rust/lance-index/src/vector/pq/storage.rs:
// lines 862-870: PQDistCalculator::new — builds the distance table per query
let distance_table = match distance_type {
DistanceType::L2 | DistanceType::Cosine => {
build_distance_table_l2(codebook, num_bits, num_sub_vectors, query)
}
DistanceType::Dot => {
build_distance_table_dot(codebook, num_bits, num_sub_vectors, query)
}
_ => unimplemented!("DistanceType is not supported: {:?}", distance_type),
};// lines 921-929: PQDistCalculator::distance_all — computes distances to all codes
fn distance_all(&self, k_hint: usize) -> Vec<f32> {
match self.distance_type {
DistanceType::L2 => compute_pq_distance(
&self.distance_table,
self.num_bits,
self.num_sub_vectors,
self.pq_code.values(),
k_hint,
),
...This kernel's PqKernel::distance_table corresponds to line 864
(build_distance_table_l2); PqKernel::compute_distances corresponds to
line 923 (compute_pq_distance). Hot caller: PQDistCalculator is the
DistCalculator impl used by IVF-PQ probe in lance-index::vector::ivf.
Vendored verbatim in crates/lance-snapshots/src/pq.rs (at the SHA pinned
in lance-snapshots/src/lib.rs):
build_distance_table_l2←rust/lance-index/src/vector/pq/distance.rscompute_pq_distance← same filetranspose←rust/lance-index/src/vector/pq/storage.rs
The reference kernel (reference.rs) calls these directly; the agent's
kernels.rs starts as the same call set and optimizes from there.
When porting a winning kernel upstream:
- File:
lance-linalg/src/distance/l2.rsand the L2-specific path inlance-index/src/vector/pq/distance.rs. - License: Apache-2.0 (matches our Apache-2.0 directly).
Float-accumulator-tolerance match against the upstream-vendored reference.
Per harness_common::MAX_ABS_ERR = 1e-4:
- Distance table values must match within
1e-4per element. - Per-vector distances vec must match within
1e-4per element.
Both gates assert on every input combination, five input distributions ×
three PQ shapes = 15 cases per trial. Loose enough for legal SIMD-accumulator
reordering, tight enough to catch real arithmetic bugs (e.g., the
q²+c²-2q·c cancellation trap on the large_dynamic_range fixture).
Three shapes:
(128, 16, 256), SIFT-like; sub_vector_dim = 8(256, 16, 256), sub_vector_dim = 16(768, 96, 256), BERT-base-like; large codebook
Three data distributions:
Clustered, 32 cluster centers, low intra-cluster noiseUniform, uniform on [-1, 1]Sparse, 90% zeros + 10% Gaussian
Per (shape × distribution): 20,000 base vectors PQ-encoded, 32 queries
timed. Total trial wall-clock: ~30s on a developer laptop (1-pass) or
~90s (--mode baseline, 3-pass).
correctness: pass | fail
arch: aarch64 | x86_64 | ...
passes: 1 | 3
shapes_tested: (128,16,256) (256,16,256) (768,96,256)
distributions_tested: clustered uniform sparse
geomean_ns_per_query: <u64>
geomean_ns_ci_90pct: [<u64>, <u64>]
median_ns_per_query: <u64>
geomean_cycles_per_query: <u64> | n/a (no PMU access on this platform)
geomean_instructions_per_query: <u64> | n/a (no PMU access on this platform)
worst_ns_per_query: <u64> (<shape>, <dist>)
best_ns_per_query: <u64> (<shape>, <dist>)
per_combo_geomean_ns:
(...)
peak_mem_mb: <f64>
total_seconds: <f64>
See crates/pq-l2/program.md "Lance-PQ-specific priors" for the canonical
arch-split list. The full set of caveats (which tricks fail correctness,
which combinations regress small shapes) lives in crates/pq-l2/lessons.md
(gitignored per-machine; populated as trials surface findings).