Exact binary vector search for RAG in 100 lines of Julia
domluna.com
domluna.com
I loved this post <3 The hamming distance is one of my favorite demos of the conciseness of Julia:
hamming_distance(s1, s2) = mapreduce(!=, +, s1, s2)
I'm a bit swamped at the moment but I'll a response article later - they're still some juicy perf on the table here.
Thanks for the post, such a good showcase.
A great article inspires everyone to participate in the fun.
Cheers =3
db = [rand(Int8) for _ in 1:64, j in 1:(10^6)];
to avoid the vec of vecs structure,
and then
function my_cluster!(db, query, k) db .= query .⊻ db popcounts = mapreduce(count_ones, +, db, dims = 1) results = reshape(popcounts, last(size(db))) partialsortperm!(results, results, k) @views results[begin:k] end
...which I couldn't get to be faster than your version. If you use the `partialsortperm!` and reuse the same cache array, I suspect you'll get good speedups, as you won't be sorting the array every time. This is a classic `nth_element` algorithm.
The above is not the most amazing code, but I suspect the lack of indexing will make it ridiculously friendly for a GPU (Edit: Nope, it chokes on `partialsortperm!`).
I'm guessing the manual loopy approach should be just as good but I battled hard to get it somewhat competitive here in 6 lines of code
#@be my_cluster!(X2, q1, 5) Benchmark: 3 samples with 1 evaluation 42.883 ms (17 allocs: 15.259 MiB) 45.711 ms (17 allocs: 15.259 MiB) 46.670 ms (17 allocs: 15.259 MiB)
#@be k_closest(X1, q1, 5) Benchmark: 4 samples with 1 evaluation 27.994 ms (2 allocs: 176 bytes) 28.733 ms (2 allocs: 176 bytes) 29.000 ms (2 allocs: 176 bytes) 30.709 ms (2 allocs: 176 bytes)
I also didn't try using `FixedSizedArrays.jl` as Mose Giordano recommended in my livestream chat.
Under the hood it’s doing the same thing with a vector of ints (64 bits for bitvectors) and all the bulk manipulation is handled that way so SIMD in inherent as well. Worth a shot.
for i in 0:7
c += (r >> i) & 1
end
This is just popcnt, surely Julia has a built in for that.Thanks! With this the timings are even faster. I'll update the post.
fn hamming_distance_u8(x1: u8, x2: u8) -> usize {
(x1 ^ x2).count_ones() as usize
}julia> @code_llvm hamming_distance(Int8(33), Int8(125)) ; Function Signature: hamming_distance(Int8, Int8) ; @ /Users/lunaticd/code/tiny-binary-rag/rag.jl:13 within `hamming_distance` define i64 @julia_hamming_distance_16366(i8 signext %"x1::Int8", i8 signext %"x2::Int8") #0 { top: ; @ /Users/lunaticd/code/tiny-binary-rag/rag.jl:14 within `hamming_distance` ; ┌ @ int.jl:373 within `xor` %0 = xor i8 %"x2::Int8", %"x1::Int8" ; └ ; ┌ @ int.jl:415 within `count_ones` %1 = call i8 @llvm.ctpop.i8(i8 %0) ; │┌ @ int.jl:549 within `rem` %2 = zext i8 %1 to i64 ; └└ ret i64 %2 }
it lowers to the machine instruction now.
I also tried 8 Int64s vs 64 Int8s and it doesn't seem to make a difference when doing the search.
EDIT: apologize for the formatting
RAG = Retrieval-Augmented Generation
The field is machine learning. Retrieval = get relevant documents. Generation = create answer for user (based on the docs).
Have you tried to benchmark also this heap implementation?
https://juliacollections.github.io/DataStructures.jl/latest/...