perf: add SIMD kernels for bf16 distance functions#6510
Conversation
Add optional `numkong` feature flag to lance-linalg that integrates NumKong's hardware-accelerated SIMD kernels for distance computation. NumKong provides runtime CPU detection and dispatches to the best available ISA (AVX-512, AVX2, NEON, SVE, etc.) with compensated summation for improved numerical accuracy. Platform-specific dispatch strategy based on benchmarking: - bf16: NumKong on all architectures (~48% faster on Apple Silicon, filling the "TODO: add SIMD support" gap) - f32/f64: NumKong on x86_64 only (LLVM auto-vec already generates excellent NEON code on ARM; NumKong's f64 accumulator widening adds overhead that isn't offset by NEON's 128-bit registers) Benchmarks on Apple Silicon (1M x 1024-dim vectors): - bf16 dot: 138ms -> 74ms (-46%) - f32 dot: 61ms (unchanged, auto-vec path preserved) - f64 dot: 119ms (unchanged, auto-vec path preserved) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Extend the numkong feature to also cover f16 dot product, L2, and cosine distance. When enabled, NumKong's runtime-dispatched SIMD kernels replace Lance's hand-written C FFI kernels (fp16kernels). This allows benchmarking NumKong f16 vs Lance's existing per-platform C kernels to determine if the custom C code can be simplified. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Benchmarking on both Apple Silicon (NEON) and AMD Ryzen 5 4500 (AVX2) showed that NumKong's compensated summation overhead causes regressions for f16, f32, and f64 on both platforms. Only bf16 shows consistent improvement because Lance had no SIMD for it at all. Results on Apple Silicon (M-series, 1M x 1024-dim vectors): - bf16: 138ms -> 74ms (-46%) -- NumKong win - f32: 61ms -> 211ms (+243%) -- regression - f64: 119ms -> 387ms (+226%) -- regression Results on AMD Ryzen 5 4500 (Zen 2, AVX2, 1M x 1024-dim vectors): - bf16: 568ms -> 166ms (-71%) -- NumKong win - f16: 111ms -> 194ms (+74%) -- regression - f32: 161ms -> 352ms (+113%) -- regression - f64: 306ms -> 535ms (+75%) -- regression Root cause: NumKong promotes all accumulators to wider types (f32->f64) and uses Neumaier compensated summation for numerical accuracy. This extra precision work costs throughput. LLVM's auto-vectorizer already generates good SIMD code (NEON on ARM, AVX2 on x86) for the simple multiply-accumulate loops used by f16/f32/f64, making NumKong slower despite its hand-tuned SIMD kernels. bf16 is the exception because Lance had zero SIMD support for it (the code literally had "// TODO: add SIMD support"), so even with the accumulation overhead, NumKong's SIMD kernels are a large win. This commit reverts f16/f32/f64 to their original implementations and keeps NumKong for bf16 only, on all platforms. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
|
We may need to bump clang for this to work. |
|
Given this only improves bf16, why not just implement our own, learn from its implementation? So we can avoid taking a dependency just for this specific feature |
I discussed that option with Yang. It's possible the performance regression is due to a technique being used that increases precision, and there's the fact we wouldn't have to maintain the kernels anymore. |
I think this would be my main concern. If it's a new version of clang that can easily be installed on ubuntu then maybe, as an opt-in feature, it would be nice. If it requires something like building clang from source then it will probably be a non-starter for most users. The benefit of the current approach is that we don't have to worry about any portability concerns. As long as that can continue I don't have much issue adopting numkong. For starters though, I agree it should be an opt-in feature as in this PR. |
Lift bf16 distance kernels (dot, L2, cosine, norm_l2) into lance-linalg as C source compiled via build.rs, following the existing f16 kernel pattern. Runtime CPU dispatch via SIMD_SUPPORT ensures kernels are only used when the hardware supports them (NEON on aarch64, AVX2/AVX-512 on x86_64, LSX/LASX on loongarch64), with scalar fallback otherwise. Removes the numkong optional dependency entirely. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
|
@jackye1995 no more NumKong direct dependency. :) |
BubbleCal
left a comment
There was a problem hiding this comment.
LGTM
maybe need a new PR title
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
westonpace
left a comment
There was a problem hiding this comment.
There are some merge conflicts. If we resolve those I can merge. Thanks for investigating this. It's nice to see better support for bf16!
Resolve merge conflict in cosine benchmarks by accepting upstream u8 cosine benchmarks. Apply cargo fmt to fix formatting in extern fn signatures and benchmark files. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Summary
numkongdependency with in-tree C kernels for bf16 distance computation (dot product, L2, cosine, norm_l2)build.rswith per-architecture flags, runtime CPU dispatch viaSIMD_SUPPORTfp16kernelsfeature flagBenchmark Results
Tested on two platforms with 1M x 1024-dim vectors:
Apple Silicon (M-series, NEON)
AMD Ryzen 5 4500 (Zen 2, AVX2)
Why the approach works
BF16-to-f32 conversion is a simple left-shift by 16 bits. The C kernels compiled with architecture-specific flags (
-march=haswell,-mtune=apple-m1, etc.) plus-ffast-mathand vectorization pragmas give the compiler more freedom to emit tight SIMD code than LLVM gets from the Rust scalar loops. ARM benefits more because the baseline Rust auto-vectorization was weaker there.Files Changed
rust/lance-linalg/src/simd/bf16.c— C kernels for dot, L2, cosine, norm_l2rust/lance-linalg/build.rs— compile bf16.c for each architecturerust/lance-linalg/src/distance/{dot,l2,cosine,norm_l2}.rs— runtime SIMD dispatch for bf16rust/lance-linalg/Cargo.toml— removednumkongdependency and featurerust/lance-linalg/benches/{dot,l2,cosine}.rs— removed numkong benchmark sectionsscripts/bench_numkong.shTest plan
cargo test -p lance-linalg --features fp16kernels— all bf16 tests pass (kernel path)cargo test -p lance-linalg— all bf16 tests pass (scalar fallback)cargo clippy -p lance-linalg --features fp16kernels --tests --benches -- -D warnings— clean🤖 Generated with Claude Code