Replace fused Butina Python/Triton path with CUDA backend#226
Replace fused Butina Python/Triton path with CUDA backend#226Matthew-Neba wants to merge 24 commits into
Conversation
Signed-off-by: Matthew Neba <mattneba4343@gmail.com> Opps, behind some commits
Signed-off-by: Matthew Neba <mattneba4343@gmail.com> Finalize rebase
Signed-off-by: Matthew Neba <mattneba4343@gmail.com> Finalize rebase
Signed-off-by: Matthew Neba <mattneba4343@gmail.com>
Signed-off-by: Matthew Neba <mattneba4343@gmail.com>
Signed-off-by: Matthew Neba <mattneba4343@gmail.com>
Signed-off-by: Matthew Neba <mattneba4343@gmail.com>
Signed-off-by: Matthew Neba <mattneba4343@gmail.com>
|
| Filename | Overview |
|---|---|
| src/fused_butina.cu | New file implementing the CUDA fused Butina pipeline; uses cudaGraphConditionalHandleCreate / cudaGraphCondTypeWhile (sm_90+ / CUDA 12.4 requirement); fingerprint index arithmetic correctly uses static_caststd::size_t throughout, addressing the prior overflow concern. |
| src/butina_common.cuh | New header owning ConditionalLoopGraph RAII wrapper; well-structured with proper move/copy suppression, but cudaGraphConditionalHandleCreate and cudaGraphCondTypeWhile require sm_90+. |
| src/fingerprint_similarity_device.cuh | New device-side header consolidating Tanimoto/cosine similarity computation; bounds checks, pruning predicates, and denominator-zero handling are all correct. |
| src/similarity.cpp | Fixes the CPU-result stream-sync bug: records a cudaEvent_t on the caller's input stream and makes every ScopedStream wait on it before accessing fingerprint buffers; correctly threaded through both single-shot and chunked paths. |
| nvmolkit/clustering.py | fused_butina now delegates entirely to the C++ backend; metric/stream parameter order swapped vs old signature (pre-existing outside-diff note); both butina() and fused_butina() share _wrap_result for consistent AsyncGpuResult output. |
| nvmolkit/_fingerprint_inputs.py | New shared helper for packed fingerprint preparation; correctly enqueues tensor moves on the resolved CUDA stream, validates dtype (int32/uint32) and 2D shape, and reinterprets int32 as uint32 in-place. |
| nvmolkit/similarity.py | Wraps CrossTanimoto/Cosine calls inside with torch.cuda.stream(active_stream) so that kernel dispatch lands on the right stream; CPU-returning variants pass the active stream to C++ for the event-wait fix. |
| nvmolkit/DataStructs.cpp | CrossTanimotoSimilarityCPURawBuffers / CrossCosineSimilarityCPURawBuffers now accept a stream pointer and forward it to the C++ compute path; duplicate boilerplate eliminated via crossSimilarityCPUFromRawBuffers / crossSimilarityGPUFromRawBuffers templates. |
| src/butina.h | Old in-place butinaGpu overloads replaced by butinaFromDistanceMatrix / butinaFromHitMatrix / fusedButinaGpu returning the shared ButinaResult struct; well-documented with parameter semantics clarified. |
| nvmolkit/types.py | Adds np.ascontiguousarray guard before torch.as_tensor for non-C-contiguous NumPy arrays; small but correct correctness fix. |
| nvmolkit/fingerprints.py | unpack_fingerprint now casts to int64 before shifting (avoids signed-shift UB on uint32 MSB), pack_fingerprint accumulates in int64 and converts to uint32; both functions now validate ndim and dtype explicitly. |
Reviews (9): Last reviewed commit: "Merge branch 'main' into main" | Re-trigger Greptile
Signed-off-by: Matthew Neba <mattneba4343@gmail.com>
Signed-off-by: Matthew Neba <mattneba4343@gmail.com>
Signed-off-by: Matthew Neba <mattneba4343@gmail.com>
scal444
left a comment
There was a problem hiding this comment.
Thanks for the contribution, I'll be sure to benchmark it!
My main concern is code duplication, but I know the fused/non-fused versions are subtly different enough so that consolidating all the kernels might not make sense. Could you generally take a look and see if anything is worth pulling out into a butina_common.cuh/.cu file?
Signed-off-by: Matthew Neba <mattneba4343@gmail.com>
Signed-off-by: Matthew Neba <mattneba4343@gmail.com>
|
Hi Matthew - I benchmarked the current PR on my local 5080. I'm seeing good speedups for the smaller cases but perf regressions as it grows. I traced it to the initial neighbors kernel (~95%+ of runtime). - it's scaling quadratically and is less efficient that the original triton. We may need to rework it to be more efficient. If you have time to take a look at it, that would be great, otherwise I can give it a stab later this week. |
|
Ok, thanks for the benchmark results. I’ll take a crack at it today |
scal444
left a comment
There was a problem hiding this comment.
Thanks for all the extra cleanup! Perf now looks great -
Size Main Previous PR Updated PR vs main
━━━━━━ ━━━━━━━━━━━━ ━━━━━━━━━━━━━ ━━━━━━━━━━━━ ━━━━━━━━━━━━━━
1k 0.563 ms 0.158 ms 0.148 ms 3.81× faster
────── ──────────── ───────────── ──────────── ──────────────
5k 2.398 ms 1.596 ms 0.453 ms 5.30× faster
────── ──────────── ───────────── ──────────── ──────────────
10k 5.951 ms 5.815 ms 0.997 ms 5.97× faster
────── ──────────── ───────────── ──────────── ──────────────
20k 17.824 ms 22.064 ms 2.905 ms 6.14× faster
────── ──────────── ───────────── ──────────── ──────────────
30k 31.562 ms 48.922 ms 6.088 ms 5.18× faster
────── ──────────── ───────────── ──────────── ──────────────
40k 48.762 ms 86.065 ms 10.312 ms 4.73× faster
────── ──────────── ───────────── ──────────── ──────────────
100k 226.471 ms 532.878 ms 57.488 ms 3.94× faster
────── ──────────── ───────────── ──────────── ──────────────
500k 4.788 s 27.581 s 1.531 s 3.13× faster
A few small comments then this can go in.
| - Fix empty result handling in substructure search with `uniquify` when all matches were already unique ([#112](https://github.com/NVIDIA-Digital-Bio/nvMolKit/issues/112)) | ||
|
|
||
| ### Miscellaneous | ||
| - `fused_butina()` now places its optional `stream` argument last; callers using positional arguments should pass |
There was a problem hiding this comment.
Again, should not mess with the changelog.
| #include <cstdint> | ||
| #include <cuda/std/span> | ||
|
|
||
| #include "src/fingerprint_similarity_device.cuh" |
There was a problem hiding this comment.
I generally would like to avoid polluting .h with .cuh includes. Any .h file should be include-able by a .cpp file without worrying about if it's bringing in something the requires nvcc. Another thing we haven't been great about throughout the codebase, but I'm trying to enforce it going forward.
| cudaCheckError(cudaMemsetAsync(neighborCountsSpan.data(), 0, neighborCountsSpan.size_bytes(), stream)); | ||
| const int numInitialTiles = (n + kInitialTileSize - 1) / kInitialTileSize; | ||
| initialNeighborCountKernel<Metric> | ||
| <<<dim3(numInitialTiles + 1, (numInitialTiles + 1) / 2), kInitialBlockSize, 0, stream>>>(fingerprints, |
There was a problem hiding this comment.
I think we'll run into a grid limit crash here on large enough sizes. We had a similar issue with the fused version #194
Summary
N x Nhit matrix.uint32while retainingint32input supportPerformance
About 12-15x faster than the previous Python implementation on my machine.