cuDDL
Loading...
Searching...
No Matches
reference_database.cuh
Go to the documentation of this file.
1#pragma once
2
3#include <cuda_runtime.h>
4#include <cub/device/device_radix_sort.cuh>
5#include <cub/device/device_scan.cuh>
6#include <cub/device/device_segmented_sort.cuh>
7#include <cub/device/device_select.cuh>
8#include <cub/device/device_transform.cuh>
9#include <cuda/algorithm>
10#include <cuda/buffer>
11#include <cuda/iterator>
12#include <cuda/memory_pool>
13#include <cuda/std/cstdint>
14#include <cuda/std/functional>
15#include <cuda/std/optional>
16#include <cuda/std/utility>
17#include <cuda/stream>
18
19#include <algorithm>
20#include <array>
21#include <atomic>
22#include <bit>
23#include <cstddef>
24#include <functional>
25#include <limits>
26#include <map>
27#include <memory>
28#include <mutex>
29#include <optional>
30#include <span>
31#include <string>
32#include <type_traits>
33#include <utility>
34#include <vector>
35
36#include <cuddl/detail/hash.cuh>
41#include <cuddl/device_span.cuh>
42#include <cuddl/error.hpp>
44
45namespace cuddl {
46
47template <uint32_t K, size_t BucketCount, typename Layout = default_register_layout>
48class reference_index;
49
50namespace detail {
55using device_scope = ::cuda::__ensure_current_context;
56} // namespace detail
57
61
64 uint8_t kmer_length{};
65 uint16_t bucket_count{};
68 uint8_t exponent_bits{};
69 uint8_t mantissa_bits{};
70 uint32_t hash_identity{};
71 uint64_t hash_seed{};
75 uint16_t key_mask{};
76
78 template <uint32_t K, size_t BucketCount, typename Layout = default_register_layout>
79 [[nodiscard]] static constexpr score_compatibility current() noexcept {
80 static_assert(K <= std::numeric_limits<uint8_t>::max());
81 static_assert(BucketCount <= std::numeric_limits<uint16_t>::max());
82 return {
83 .kmer_length = K,
84 .bucket_count = BucketCount,
85 .indexed_bucket_count = BucketCount,
86 .score_encoder_identity = 1U,
87 .exponent_bits = Layout::exponent_bits,
88 .mantissa_bits = Layout::mantissa_bits,
89 .hash_identity = 1U,
90 .hash_seed = detail::seed,
91 .canonicalisation_policy = 1U,
92 .blacklist_identity = 0U,
93 .blacklist_version = 0U,
94 // Genomic winner scores virtually never set the top bit, so folding it halves the
95 // dense index at no practical cost in selectivity.
96 .key_mask = 0x7fffU,
97 };
98 }
99
100 friend bool operator==(score_compatibility const&, score_compatibility const&) = default;
101};
102
111
120
123 uint32_t query_id{};
124 uint32_t reference_id{};
126
127 friend bool operator==(batch_search_result const&, batch_search_result const&) = default;
128};
129
150 uint32_t const* match_counts{};
152 uint32_t const* pass_bits{};
154 uint32_t const* word_offsets{};
155 uint32_t first_query_id{};
156 uint32_t query_count{};
157 uint32_t reference_count{};
159 uint32_t bucket_count{};
161
162 [[nodiscard]] __host__ __device__ constexpr uint32_t words_per_query() const noexcept {
163 return (reference_count + 31U) / 32U;
164 }
165
167 [[nodiscard]] __host__ __device__ uint64_t slot_count() const noexcept {
168 if (query_count == 0U) return 0U;
169 auto const last = query_count - 1U;
170 if (upper_triangle) {
171 return first_query_id + last + 1U >= reference_count
172 ? slot(last, reference_count)
173 : slot(last, reference_count - 1U) + 1U;
174 }
175 return static_cast<uint64_t>(query_count) * reference_count;
176 }
177
179 [[nodiscard]] __host__ __device__ bool
180 passed(uint32_t query_id, uint32_t reference_id) const noexcept {
181 if (upper_triangle && reference_id < query_id) {
182 cuda::std::swap(query_id, reference_id);
183 }
184 if (query_id < first_query_id || query_id - first_query_id >= query_count ||
185 reference_id >= reference_count || (upper_triangle && reference_id <= query_id)) {
186 return false;
187 }
188 if (pass_bits == nullptr) return true;
189 return ((pass_bits[word(query_id, reference_id)] >> (reference_id % 32U)) & 1U) != 0U;
190 }
191
194 [[nodiscard]] __host__ __device__ cuda::std::optional<batch_search_result>
195 find(uint32_t query_id, uint32_t reference_id) const noexcept {
196 if (!passed(query_id, reference_id)) return cuda::std::nullopt;
197 if (upper_triangle && reference_id < query_id) {
198 auto const stored = results[position(reference_id, query_id)].unpack(bucket_count);
199 return batch_search_result{query_id, reference_id, stored.mirrored()};
200 }
201 return result(query_id, reference_id, results[position(query_id, reference_id)]);
202 }
203
206 [[nodiscard]] __host__ __device__ uint32_t
207 match_count(uint32_t query_id, uint32_t reference_id) const noexcept {
208 if (upper_triangle && reference_id < query_id) {
209 cuda::std::swap(query_id, reference_id);
210 }
211 return match_counts[position(query_id, reference_id)];
212 }
213
215 template <typename F>
216 __host__ __device__ void for_each_passing(F&& f) const {
218 }
219
223 template <typename F>
224 __host__ __device__ void for_each_passing(uint32_t begin, uint32_t end, F&& f) const {
225 auto next = begin < end ? stored_before(begin) : 0U;
226 for (uint32_t query = begin; query < end; ++query) {
227 auto const query_id = first_query_id + query;
228 if (pass_bits == nullptr) {
229 // Every slot passes, and slots are in this order.
230 for (auto reference = upper_triangle ? query_id + 1U : 0U;
231 reference < reference_count;
232 ++reference) {
233 f(result(query_id, reference, results[next++]));
234 }
235 continue;
236 }
237 auto const words = words_per_query();
238 for (uint32_t w = 0; w < words; ++w) {
239 for (auto bits = pass_bits[static_cast<size_t>(query) * words + w]; bits != 0U;
240 bits &= bits - 1U) {
241 auto const reference =
242 w * 32U + static_cast<uint32_t>(cuda::std::countr_zero(bits));
243 auto const at = word_offsets != nullptr ? next++ : slot(query, reference);
244 f(result(query_id, reference, results[at]));
245 }
246 }
247 }
248 }
249
252 [[nodiscard]] __host__ __device__ uint64_t passing_before(uint32_t query) const noexcept {
253 if (query >= query_count) return count();
254 if (pass_bits == nullptr || word_offsets != nullptr) return stored_before(query);
255 auto const words = static_cast<size_t>(query) * words_per_query();
256 uint64_t total = 0U;
257 for (size_t w = 0; w < words; ++w) {
258 total += static_cast<uint64_t>(cuda::std::popcount(pass_bits[w]));
259 }
260 return total;
261 }
262
264 [[nodiscard]] __host__ __device__ uint64_t count() const noexcept {
265 if (pass_bits == nullptr) return slot_count();
266 auto const words = static_cast<size_t>(query_count) * words_per_query();
267 if (words == 0U) return 0U;
268 if (word_offsets != nullptr) {
269 return static_cast<uint64_t>(word_offsets[words - 1U]) +
270 static_cast<uint64_t>(cuda::std::popcount(pass_bits[words - 1U]));
271 }
272 uint64_t total = 0U;
273 for (size_t w = 0; w < words; ++w) {
274 total += static_cast<uint64_t>(cuda::std::popcount(pass_bits[w]));
275 }
276 return total;
277 }
278
279 private:
280 [[nodiscard]] __host__ __device__ batch_search_result
281 result(uint32_t query_id, uint32_t reference_id, packed_pairwise_counts counts) const noexcept {
282 return {
283 .query_id = query_id,
284 .reference_id = reference_id,
285 .counts = counts.unpack(bucket_count),
286 };
287 }
288
289 [[nodiscard]] __host__ __device__ size_t
290 word(uint32_t query_id, uint32_t reference_id) const noexcept {
291 return static_cast<size_t>(query_id - first_query_id) * words_per_query() +
292 reference_id / 32U;
293 }
294
295 [[nodiscard]] __host__ __device__ uint64_t
296 position(uint32_t query_id, uint32_t reference_id) const noexcept {
297 if (word_offsets != nullptr) {
298 auto const w = word(query_id, reference_id);
299 auto const below = (1U << (reference_id % 32U)) - 1U;
300 return static_cast<uint64_t>(word_offsets[w]) +
301 static_cast<uint64_t>(cuda::std::popcount(pass_bits[w] & below));
302 }
303 return slot(query_id - first_query_id, reference_id);
304 }
305
306 [[nodiscard]] __host__ __device__ uint64_t
307 slot(uint32_t query, uint32_t reference_id) const noexcept {
308 return upper_triangle ? detail::batch_result_slot<true>(
309 query, reference_id, first_query_id, reference_count
310 )
311 : detail::batch_result_slot<false>(
312 query, reference_id, first_query_id, reference_count
313 );
314 }
315
317 [[nodiscard]] __host__ __device__ uint64_t stored_before(uint32_t query) const noexcept {
318 if (pass_bits == nullptr) {
319 return slot(query, upper_triangle ? first_query_id + query + 1U : 0U);
320 }
321 if (word_offsets == nullptr || reference_count == 0U) return 0U;
322 return word_offsets[static_cast<size_t>(query) * words_per_query()];
323 }
324};
325
335 public:
337 explicit host_batch_results(cuda::stream_ref stream)
339 stream,
340 detail::device_pinned_pool(stream.device(), detail::pinned_placement::device_node)
341 ) {}
342
344 host_batch_results(cuda::stream_ref stream, cuda::pinned_memory_pool_ref pool)
345 : pool_(pool),
346 results_(stream, pool, 0, cuda::no_init),
347 match_counts_(stream, pool, 0, cuda::no_init),
348 pass_bits_(stream, pool, 0, cuda::no_init),
349 word_offsets_(stream, pool, 0, cuda::no_init) {}
350
353
354 __host__ host_batch_results(host_batch_results&& other) noexcept
355 : pool_(other.pool_),
356 results_(std::move(other.results_)),
357 match_counts_(std::move(other.match_counts_)),
358 pass_bits_(std::move(other.pass_bits_)),
359 word_offsets_(std::move(other.word_offsets_)),
360 count_(std::exchange(other.count_, 0U)),
361 has_match_counts_(other.has_match_counts_),
362 layout_(other.layout_) {}
363
365
370 [[nodiscard]] std::vector<batch_search_result> passing() const {
371 std::vector<batch_search_result> kept;
372 kept.reserve(static_cast<size_t>(count_));
373 tile().for_each_passing([&](batch_search_result const& result) { kept.push_back(result); });
374 return kept;
375 }
376
379 // A span into a temporary copy would dangle, so rvalues are rejected.
380 [[nodiscard]] std::span<uint32_t const> passing_match_counts() const& noexcept {
381 return {match_counts_.data(), has_match_counts_ ? static_cast<size_t>(count_) : 0U};
382 }
383 void passing_match_counts() const&& = delete;
384
386 [[nodiscard]] batch_result_tile tile() const& noexcept {
387 auto view = layout_;
388 view.results = results_.data();
389 view.match_counts = has_match_counts_ ? match_counts_.data() : nullptr;
390 view.pass_bits = layout_.pass_bits == nullptr ? nullptr : pass_bits_.data();
391 view.word_offsets = layout_.pass_bits == nullptr ? nullptr : word_offsets_.data();
392 return view;
393 }
394 void tile() const&& = delete;
395
396 private:
397 template <typename T>
398 using pinned_buffer = cuda::buffer<T, cuda::mr::host_accessible, cuda::mr::device_accessible>;
399
401 template <typename T>
402 void reserve(pinned_buffer<T>& buffer, size_t count, cuda::stream_ref stream) {
403 if (buffer.size() < count) buffer = pinned_buffer<T>(stream, pool_, count, cuda::no_init);
404 }
405
406 friend Result<void> download(batch_result_tile const&, cuda::stream_ref, host_batch_results&);
407
408 cuda::pinned_memory_pool_ref pool_;
409 pinned_buffer<packed_pairwise_counts> results_;
410 pinned_buffer<uint32_t> match_counts_;
411 pinned_buffer<uint32_t> pass_bits_;
412 pinned_buffer<uint32_t> word_offsets_;
413 uint64_t count_{};
414 bool has_match_counts_{};
415 batch_result_tile layout_{};
416};
417
426[[nodiscard]] inline Result<void>
427download(batch_result_tile const& tile, cuda::stream_ref stream, host_batch_results& host) {
428 detail::device_scope const scope{stream};
429 host.layout_ = tile;
430 host.layout_.word_offsets = nullptr;
431 host.has_match_counts_ = tile.match_counts != nullptr;
432 if (tile.pass_bits == nullptr) {
433 auto const slots = static_cast<size_t>(tile.slot_count());
434 host.count_ = slots;
435 host.reserve(host.results_, slots, stream);
436 if (slots != 0U) {
438 cuda::copy_bytes(
439 stream,
440 cuda::std::span{tile.results, slots},
441 cuda::std::span{host.results_.data(), slots}
442 )
443 );
444 }
445 if (host.has_match_counts_) {
446 host.reserve(host.match_counts_, slots, stream);
447 if (slots != 0U) {
449 cuda::copy_bytes(
450 stream,
451 cuda::std::span{tile.match_counts, slots},
452 cuda::std::span{host.match_counts_.data(), slots}
453 )
454 );
455 }
456 }
457 CUDDL_CUDA_TRY(stream.sync());
458 return Ok();
459 }
460
461 auto const words = static_cast<size_t>(tile.query_count) * tile.words_per_query();
462 host.reserve(host.pass_bits_, words, stream);
463 host.reserve(host.word_offsets_, words, stream);
464 if (words == 0U) {
465 host.count_ = 0U;
466 CUDDL_CUDA_TRY(stream.sync());
467 return Ok();
468 }
469 auto const popcounts = cuda::make_transform_iterator(tile.pass_bits, detail::word_popcount{});
470 size_t scan_bytes = 0;
472 cub::DeviceScan::ExclusiveSum(
473 nullptr,
474 scan_bytes,
475 popcounts,
476 host.word_offsets_.data(),
477 static_cast<int64_t>(words),
478 stream.get()
479 )
480 );
481 auto scan_storage = CUDDL_CUDA_TRY(
482 cuda::make_device_buffer<uint8_t>(stream, stream.device(), scan_bytes, cuda::no_init)
483 );
485 cub::DeviceScan::ExclusiveSum(
486 scan_storage.data(),
487 scan_bytes,
488 popcounts,
489 host.word_offsets_.data(),
490 static_cast<int64_t>(words),
491 stream.get()
492 )
493 );
495 cuda::copy_bytes(
496 stream,
497 cuda::std::span{tile.pass_bits, words},
498 cuda::std::span{host.pass_bits_.data(), words}
499 )
500 );
501 CUDDL_CUDA_TRY(stream.sync());
502 host.count_ = static_cast<uint64_t>(host.word_offsets_.data()[words - 1U]) +
503 static_cast<uint64_t>(cuda::std::popcount(host.pass_bits_.data()[words - 1U]));
504 auto const count = static_cast<size_t>(host.count_);
505 host.reserve(host.results_, count, stream);
506 if (host.has_match_counts_) host.reserve(host.match_counts_, count, stream);
507 if (count == 0U) {
508 CUDDL_CUDA_TRY(stream.sync());
509 return Ok();
510 }
511 // Passing records are gathered into bounded device staging, so each chunk crosses the bus as
512 // one bulk copy rather than as scattered small writes.
513 constexpr size_t staging_records = size_t{1} << 23U;
514 auto const staged = std::min(count, staging_records);
515 auto staging = CUDDL_CUDA_TRY(
516 cuda::make_device_buffer<packed_pairwise_counts>(
517 stream, stream.device(), staged, cuda::no_init
518 )
519 );
520 auto staging_matches = CUDDL_CUDA_TRY(
521 cuda::make_device_buffer<uint32_t>(
522 stream, stream.device(), tile.match_counts == nullptr ? 0U : staged, cuda::no_init
523 )
524 );
525 auto const kernel = tile.upper_triangle
526 ? detail::gather_passing_kernel<true, packed_pairwise_counts>
527 : detail::gather_passing_kernel<false, packed_pairwise_counts>;
528 auto const* offsets = host.word_offsets_.data();
529 for (size_t first_word = 0U; first_word < words;) {
530 auto const base = offsets[first_word];
531 // The last word whose results all fit the staging buffer ends the chunk.
532 auto const last_word = static_cast<size_t>(
533 std::upper_bound(offsets + first_word + 1U, offsets + words, base + staged - 32U) -
534 offsets
535 );
536 auto const end = last_word == words ? count : static_cast<size_t>(offsets[last_word]);
537 kernel<<<
538 detail::warp_grid_blocks(last_word - first_word),
540 0,
541 stream.get()>>>(
542 tile.results,
543 tile.match_counts,
544 tile.pass_bits,
545 host.word_offsets_.data(),
546 tile.first_query_id,
547 tile.reference_count,
548 first_word,
549 last_word,
550 base,
551 staging.data(),
552 tile.match_counts == nullptr ? nullptr : staging_matches.data()
553 );
554 CUDDL_CUDA_TRY(cudaGetLastError());
555 auto const records = end - base;
556 if (records != 0U) {
558 cuda::copy_bytes(
559 stream,
560 cuda::std::span{staging.data(), records},
561 cuda::std::span{host.results_.data() + base, records}
562 )
563 );
564 if (tile.match_counts != nullptr) {
566 cuda::copy_bytes(
567 stream,
568 cuda::std::span{staging_matches.data(), records},
569 cuda::std::span{host.match_counts_.data() + base, records}
570 )
571 );
572 }
573 }
574 first_word = last_word;
575 }
576 CUDDL_CUDA_TRY(stream.sync());
577 return Ok();
578}
579
582[[nodiscard]] inline Result<host_batch_results>
583download(batch_result_tile const& tile, cuda::stream_ref stream) {
584 host_batch_results host(stream);
585 CUDDL_TRY(download(tile, stream, host));
586 return Result<host_batch_results>::ok(std::move(host));
587}
588
589namespace detail {
590
592inline constexpr size_t visit_chunk_records = size_t{1} << 20U;
593
596struct visit_chunk {
597 size_t first{};
598 size_t last{};
599 size_t first_record{};
600 size_t end_record{};
601};
602
603} // namespace detail
604
613template <typename F>
614[[nodiscard]] Result<void>
615for_each_passing(batch_result_tile const& tile, cuda::stream_ref stream, F&& f) {
616 detail::device_scope const scope{stream};
617 using pinned_counts = cuda::
618 buffer<packed_pairwise_counts, cuda::mr::host_accessible, cuda::mr::device_accessible>;
619 auto const memory =
621 constexpr auto chunk_records = detail::visit_chunk_records;
622 auto const threshold = tile.pass_bits != nullptr;
623 auto const words = static_cast<size_t>(tile.query_count) * tile.words_per_query();
624
625 // Chunk plan, and for threshold tiles the host copies of the pass bits and word offsets.
626 std::vector<detail::visit_chunk> chunks;
627 std::vector<uint32_t> pass_bits;
628 std::vector<uint32_t> offsets;
629 cuda::device_buffer<uint32_t> device_offsets(
630 stream, cuda::device_default_memory_pool(stream.device())
631 );
632 if (!threshold) {
633 size_t record = 0U;
634 detail::visit_chunk chunk{};
635 for (uint32_t query = 0; query < tile.query_count; ++query) {
636 auto const query_id = tile.first_query_id + query;
637 auto const row = static_cast<size_t>(
638 tile.upper_triangle
639 ? (tile.reference_count > query_id + 1U ? tile.reference_count - query_id - 1U
640 : 0U)
641 : tile.reference_count
642 );
643 if (record + row - chunk.first_record > chunk_records && query != chunk.first) {
644 chunk.last = query;
645 chunk.end_record = record;
646 chunks.push_back(chunk);
647 // NVCC 13.4 mislowers assignments that skip aggregate members.
648 chunk.first = query;
649 chunk.first_record = record;
650 }
651 record += row;
652 }
653 chunk.last = tile.query_count;
654 chunk.end_record = record;
655 if (record != 0U) chunks.push_back(chunk);
656 } else if (words != 0U) {
657 device_offsets = CUDDL_CUDA_TRY(
658 cuda::make_device_buffer<uint32_t>(stream, stream.device(), words, cuda::no_init)
659 );
660 auto const popcounts =
661 cuda::make_transform_iterator(tile.pass_bits, detail::word_popcount{});
662 size_t scan_bytes = 0;
664 cub::DeviceScan::ExclusiveSum(
665 nullptr,
666 scan_bytes,
667 popcounts,
668 device_offsets.data(),
669 static_cast<int64_t>(words),
670 stream.get()
671 )
672 );
673 auto scan_storage = CUDDL_CUDA_TRY(
674 cuda::make_device_buffer<uint8_t>(stream, stream.device(), scan_bytes, cuda::no_init)
675 );
677 cub::DeviceScan::ExclusiveSum(
678 scan_storage.data(),
679 scan_bytes,
680 popcounts,
681 device_offsets.data(),
682 static_cast<int64_t>(words),
683 stream.get()
684 )
685 );
686 pass_bits.resize(words);
687 offsets.resize(words);
689 cuda::copy_bytes(
690 stream, cuda::std::span{tile.pass_bits, words}, cuda::std::span{pass_bits}
691 )
692 );
693 CUDDL_CUDA_TRY(cuda::copy_bytes(stream, device_offsets, cuda::std::span{offsets}));
694 CUDDL_CUDA_TRY(stream.sync());
695 auto const total = static_cast<size_t>(offsets.back()) +
696 static_cast<size_t>(cuda::std::popcount(pass_bits.back()));
697 for (size_t first = 0U; first < words;) {
698 auto const base = static_cast<size_t>(offsets[first]);
699 // Whole words only: a word holds at most 32 results.
700 auto const last = static_cast<size_t>(
701 std::upper_bound(
702 offsets.begin() + static_cast<std::ptrdiff_t>(first) + 1,
703 offsets.end(),
704 static_cast<uint32_t>(base + chunk_records - 32U)
705 ) -
706 offsets.begin()
707 );
708 auto const end = last == words ? total : static_cast<size_t>(offsets[last]);
709 if (end != base) chunks.push_back({first, last, base, end});
710 first = last;
711 }
712 }
713 if (chunks.empty()) {
714 CUDDL_CUDA_TRY(stream.sync());
715 return Ok();
716 }
717
718 auto staging = CUDDL_CUDA_TRY(
719 cuda::make_device_buffer<packed_pairwise_counts>(
720 stream, stream.device(), threshold ? chunk_records : 0U, cuda::no_init
721 )
722 );
723 std::array<pinned_counts, 2> buffers{
724 pinned_counts(stream, memory, chunk_records, cuda::no_init),
725 pinned_counts(stream, memory, chunk_records, cuda::no_init),
726 };
727 std::array<cuda::event, 2> ready{cuda::event(stream.device()), cuda::event(stream.device())};
728 auto const enqueue = [&](size_t k) -> Result<void> {
729 auto const& chunk = chunks[k];
730 auto const records = chunk.end_record - chunk.first_record;
731 packed_pairwise_counts const* source = tile.results + chunk.first_record;
732 if (threshold) {
733 auto const kernel = tile.upper_triangle
734 ? detail::gather_passing_kernel<true, packed_pairwise_counts>
735 : detail::gather_passing_kernel<false, packed_pairwise_counts>;
736 kernel<<<
737 detail::warp_grid_blocks(chunk.last - chunk.first),
739 0,
740 stream.get()>>>(
741 tile.results,
742 nullptr,
743 tile.pass_bits,
744 device_offsets.data(),
745 tile.first_query_id,
746 tile.reference_count,
747 chunk.first,
748 chunk.last,
749 static_cast<uint32_t>(chunk.first_record),
750 staging.data(),
751 nullptr
752 );
753 CUDDL_CUDA_TRY(cudaGetLastError());
754 source = staging.data();
755 }
757 cuda::copy_bytes(
758 stream,
759 cuda::std::span{source, records},
760 cuda::std::span{buffers[k % 2U].data(), records}
761 )
762 );
763 ready[k % 2U].record(stream);
764 return Ok();
765 };
766
767 CUDDL_TRY(enqueue(0U));
768 for (size_t k = 0; k < chunks.size(); ++k) {
769 // Buffer (k + 1) % 2 last held chunk k - 1, which the host has finished visiting.
770 if (k + 1U < chunks.size()) CUDDL_TRY(enqueue(k + 1U));
771 CUDDL_CUDA_TRY(ready[k % 2U].sync());
772 auto const* packed = buffers[k % 2U].data();
773 auto const& chunk = chunks[k];
774 size_t next = 0U;
775 auto const emit = [&](uint32_t query_id, uint32_t reference_id) {
777 .query_id = query_id,
778 .reference_id = reference_id,
779 .counts = packed[next++].unpack(tile.bucket_count),
780 });
781 };
782 if (!threshold) {
783 for (auto query = static_cast<uint32_t>(chunk.first); query < chunk.last; ++query) {
784 auto const query_id = tile.first_query_id + query;
785 for (auto reference = tile.upper_triangle ? query_id + 1U : 0U;
786 reference < tile.reference_count;
787 ++reference) {
788 emit(query_id, reference);
789 }
790 }
791 continue;
792 }
793 auto const words_per_query = tile.words_per_query();
794 auto query = static_cast<uint32_t>(chunk.first / words_per_query);
795 auto column = static_cast<uint32_t>(chunk.first % words_per_query);
796 auto w = chunk.first;
797 while (w < chunk.last) {
798 // A transfer chunk may start or end inside a query row.
799 auto const row_end = std::min(chunk.last, w + words_per_query - column);
800 auto const query_id = tile.first_query_id + query;
801 for (auto base = column * 32U; w < row_end; ++w, base += 32U) {
802 for (auto bits = pass_bits[w]; bits != 0U; bits &= bits - 1U) {
803 emit(query_id, base + static_cast<uint32_t>(cuda::std::countr_zero(bits)));
804 }
805 }
806 ++query;
807 column = 0U;
808 }
809 }
810 return Ok();
811}
812
826
829 uint32_t minimum_matches = 5;
830};
831
840 uint32_t index = 0U;
841 uint32_t count = 1U;
842};
843
844namespace detail {
845
847 uint16_t mask;
848 template <typename Row>
849 __host__ __device__ uint16_t operator()(Row row) const noexcept {
850 auto const score = static_cast<uint16_t>(row);
851 return score == 0U || mask == 0xffffU ? score : static_cast<uint16_t>((score & mask) + 1U);
852 }
853};
856 __host__ __device__ uint32_t operator()(uint32_t i) const noexcept {
857 return i % reference_count;
858 }
859};
862 __host__ __device__ int64_t operator()(uint32_t bucket) const noexcept {
863 return static_cast<int64_t>(bucket) * reference_count;
864 }
865};
866
867template <uint32_t K, size_t BucketCount, typename Layout = default_register_layout>
869 score_compatibility const& compatibility
870) {
871 if (compatibility.kmer_length != K) {
872 return Err(Error::invalid_argument("k-mer length does not match the database type"));
873 }
874 if (compatibility.bucket_count != BucketCount) {
875 return Err(Error::invalid_argument("bucket count does not match the database type"));
876 }
877 if (compatibility.score_encoder_identity == 0U ||
878 compatibility.exponent_bits != Layout::exponent_bits ||
879 compatibility.mantissa_bits != Layout::mantissa_bits) {
880 return Err(Error::invalid_argument("score encoding does not match the register layout"));
881 }
882 if (compatibility.hash_identity == 0U) {
883 return Err(Error::invalid_argument("hash identity must be specified"));
884 }
885 if (compatibility.canonicalisation_policy == 0U) {
886 return Err(Error::invalid_argument("canonicalisation policy must be specified"));
887 }
888 return Ok();
889}
890
891template <uint32_t K, size_t BucketCount, typename Layout = default_register_layout>
893 score_compatibility const& compatibility
894) {
895 CUDDL_TRY((validate_score_compatibility<K, BucketCount, Layout>(compatibility)));
896 if (compatibility.indexed_bucket_count != BucketCount) {
897 return Err(Error::invalid_argument("non-indexed builds require every bucket"));
898 }
899 if (compatibility.key_mask != 0xffffU && compatibility.key_mask != 0x7fffU) {
900 return Err(Error::invalid_argument("non-indexed builds require a 16-bit or 15-bit key"));
901 }
902 return Ok();
903}
904
905template <uint32_t K, size_t BucketCount, typename Layout = default_register_layout>
907 score_compatibility const& compatibility
908) {
909 CUDDL_TRY((validate_score_compatibility<K, BucketCount, Layout>(compatibility)));
910 auto const full_bucket_count = static_cast<uint32_t>(BucketCount);
911 if (compatibility.indexed_bucket_count != full_bucket_count &&
912 compatibility.indexed_bucket_count != full_bucket_count / 2U) {
913 return Err(Error::invalid_argument("indexed builds require a full or half bucket count"));
914 }
915 if (compatibility.key_mask != 0xffffU && compatibility.key_mask != 0x7fffU) {
916 return Err(Error::invalid_argument("indexed builds require a 16-bit or 15-bit key mask"));
917 }
918 return Ok();
919}
920
921[[nodiscard]] inline constexpr uint64_t indexed_cell_count(
922 score_compatibility const& compatibility
923) noexcept {
924 return static_cast<uint64_t>(compatibility.indexed_bucket_count) *
925 (static_cast<uint64_t>(compatibility.key_mask) + 1U);
926}
927[[nodiscard]] inline constexpr uint64_t
928indexed_posting_count(uint32_t reference_count, score_compatibility const& compatibility) noexcept {
929 return static_cast<uint64_t>(reference_count) * compatibility.indexed_bucket_count;
930}
931
935[[nodiscard]] inline constexpr index_storage
936smaller_index_storage(uint32_t reference_count, score_compatibility const& compatibility) noexcept {
937 auto const dense = (indexed_cell_count(compatibility) + 1U) * sizeof(uint32_t);
938 auto const sparse = indexed_posting_count(reference_count, compatibility) * sizeof(uint16_t) +
939 static_cast<uint64_t>(sparse_directory_entries(reference_count)) *
940 compatibility.indexed_bucket_count * sizeof(uint32_t);
942}
943
944[[nodiscard]] inline uintptr_t align_up(uintptr_t address, size_t alignment) noexcept {
945 return (address + alignment - 1U) & ~(static_cast<uintptr_t>(alignment) - 1U);
946}
947
950// ponytail: one crossover measured on an RTX 5070 Ti with 2048-bucket RefSeq/synthetic data;
951// make it per-device if other GPUs disagree.
952constexpr double index_pair_fraction_limit = 0.03;
953
955constexpr uint32_t batch_query_tile_count = 128U;
956
957[[nodiscard]] constexpr uint32_t batch_query_tile_size(
958 uint32_t reference_count,
959 uint32_t query_count,
960 uint32_t tile_limit = batch_query_tile_count
961) noexcept {
962 if (reference_count == 0U || query_count == 0U) {
963 return 0U;
964 }
965 auto const count_capacity = std::numeric_limits<uint32_t>::max() / reference_count;
966 return std::min(query_count, std::min(tile_limit, std::max(1U, count_capacity)));
967}
968
976[[nodiscard]] constexpr uint32_t
977all_to_all_tile_queries(uint32_t reference_count, uint32_t parts = 1U) noexcept {
978 constexpr uint32_t pair_budget = 1U << 25U;
979 constexpr uint64_t tiles_per_part = 4U;
980 auto const limit = reference_count == 0U
982 : std::max(batch_query_tile_count, pair_budget / reference_count);
983 auto const whole = batch_query_tile_size(reference_count, reference_count, limit);
984 if (parts < 2U || whole == 0U || whole >= reference_count) return whole;
985 auto const per_part = static_cast<uint32_t>(reference_count / (parts * tiles_per_part));
986 return std::min(whole, std::max(batch_query_tile_count, per_part));
987}
988
995[[nodiscard]] inline uint32_t
996external_batch_query_limit(uint32_t reference_count, cuda::device_ref device) {
997 constexpr size_t bytes_per_pair = sizeof(packed_pairwise_counts) + 3U * sizeof(uint32_t);
998 auto const budget = device.attribute(cuda::device_attributes::total_global_memory) / 4U;
999 auto const pairs = budget / bytes_per_pair;
1000 auto const queries = reference_count == 0U ? pairs : pairs / reference_count;
1001 return static_cast<uint32_t>(
1002 std::clamp<size_t>(queries, batch_query_tile_count, std::numeric_limits<uint32_t>::max())
1003 );
1004}
1005
1006} // namespace detail
1007
1024 public:
1025 struct tile {
1026 uint32_t first_query_id{};
1027 uint32_t query_count{};
1028 };
1029
1031 uint32_t reference_count,
1032 all_to_all_shard shard = {},
1033 uint32_t searchers = 1U,
1034 std::function<uint64_t()> next_tile = {}
1035 ) {
1036 if (shard.count == 0U || shard.index >= shard.count) {
1037 return Err(Error::invalid_argument("all-to-all shard index must be below its count"));
1038 }
1039 if (searchers == 0U || shard.count > std::numeric_limits<uint32_t>::max() / searchers) {
1040 return Err(Error::invalid_argument("all-to-all searcher count is out of range"));
1041 }
1043 queue.reference_count_ = reference_count;
1044 queue.tile_queries_ =
1045 detail::all_to_all_tile_queries(reference_count, shard.count * searchers);
1046 auto const count = queue.tile_queries_ == 0U
1047 ? 0U
1048 : (reference_count + queue.tile_queries_ - 1U) / queue.tile_queries_;
1049 for (uint32_t t = 0U; t < count; ++t) {
1050 auto const lane = t % shard.count;
1051 auto const owner = (t / shard.count) % 2U == 0U ? lane : shard.count - 1U - lane;
1052 if (next_tile || owner == shard.index) queue.tiles_.push_back(t);
1053 }
1054 queue.next_tile_ = std::move(next_tile);
1055 return Result<all_to_all_tile_queue>::ok(std::move(queue));
1056 }
1057
1059 : tiles_(std::move(other.tiles_)),
1060 next_(other.next_.load()),
1061 next_tile_(std::move(other.next_tile_)),
1062 cancelled_(other.cancelled_.load()),
1063 tile_queries_(other.tile_queries_),
1064 reference_count_(other.reference_count_) {}
1065
1067 [[nodiscard]] std::optional<tile> claim() noexcept {
1068 if (cancelled_.load(std::memory_order_relaxed)) return std::nullopt;
1069 auto const i = next_tile_ ? next_tile_() : next_.fetch_add(1U, std::memory_order_relaxed);
1070 if (i >= tiles_.size()) return std::nullopt;
1071 auto const first = tiles_[i] * tile_queries_;
1072 return tile{first, std::min(tile_queries_, reference_count_ - first)};
1073 }
1074
1077 void cancel() noexcept {
1078 cancelled_.store(true);
1079 }
1080
1082 [[nodiscard]] bool shared() const noexcept {
1083 return static_cast<bool>(next_tile_);
1084 }
1085
1087 [[nodiscard]] uint32_t tile_queries() const noexcept {
1088 return tile_queries_;
1089 }
1090
1092 [[nodiscard]] size_t size() const noexcept {
1093 return tiles_.size();
1094 }
1095
1097 [[nodiscard]] uint32_t reference_count() const noexcept {
1098 return reference_count_;
1099 }
1100
1102 [[nodiscard]] size_t position(uint32_t first_query_id) const noexcept {
1103 return static_cast<size_t>(
1104 std::lower_bound(tiles_.begin(), tiles_.end(), first_query_id / tile_queries_) -
1105 tiles_.begin()
1106 );
1107 }
1108
1109 private:
1110 all_to_all_tile_queue() = default;
1111
1112 std::vector<uint32_t> tiles_;
1113 std::atomic<size_t> next_{0U};
1114 std::function<uint64_t()> next_tile_;
1115 std::atomic<bool> cancelled_{false};
1116 uint32_t tile_queries_{};
1117 uint32_t reference_count_{};
1118};
1119
1120namespace detail {
1121
1128template <uint32_t K, size_t BucketCount, typename Layout = default_register_layout>
1130 static_assert(K >= 1 && K <= 31);
1131 static_assert(BucketCount >= (size_t{1} << 11) && BucketCount <= (size_t{1} << 13));
1132 static_assert((BucketCount & (BucketCount - 1)) == 0);
1133
1134 public:
1135 using score_type = uint16_t;
1139
1140 __host__ __device__ constexpr reference_database_view(
1143 device_span<uint32_t const> index_offsets = {},
1144 device_span<uint32_t const> index_postings = {},
1145 bool indexed = false,
1146 device_span<uint16_t const> index_keys = {},
1147 device_span<uint32_t const> key_directory = {},
1148 double index_pair_fraction = 0.0,
1149 device_span<uint32_t const> bitmap_cells = {},
1150 device_span<uint32_t const> posting_bitmaps = {},
1151 reference_delta_view deltas = {}
1152 ) noexcept
1153 : planes_(planes),
1154 metadata_(metadata),
1155 index_offsets_(index_offsets),
1156 index_postings_(index_postings),
1157 index_keys_(index_keys),
1158 key_directory_(key_directory),
1159 bitmap_cells_(bitmap_cells),
1160 posting_bitmaps_(posting_bitmaps),
1161 deltas_(deltas),
1162 index_pair_fraction_(index_pair_fraction),
1163 indexed_(indexed) {}
1164
1166 [[nodiscard]] __host__ __device__ constexpr device_span<uint32_t const>
1167 planes() const noexcept {
1168 return planes_;
1169 }
1170
1172 [[nodiscard]] constexpr reference_database_metadata metadata() const noexcept {
1173 return metadata_;
1174 }
1175
1177 [[nodiscard]] constexpr uint32_t reference_count() const noexcept {
1178 return metadata_.reference_count;
1179 }
1180
1182 [[nodiscard]] constexpr bool has_index() const noexcept {
1183 return indexed_;
1184 }
1185
1187 [[nodiscard]] static constexpr size_t persistent_row_bytes(uint32_t reference_count) noexcept {
1188 return static_cast<size_t>(reference_count) * BucketCount * sizeof(score_type);
1189 }
1190
1192 [[nodiscard]] constexpr size_t persistent_row_bytes() const noexcept {
1193 return planes_.size_bytes();
1194 }
1195
1197 [[nodiscard]] constexpr size_t persistent_index_bytes() const noexcept {
1198 return index_offsets_.size_bytes() + index_postings_.size_bytes() +
1199 index_keys_.size_bytes();
1200 }
1201
1203 [[nodiscard]] static constexpr size_t single_query_workspace_bytes(
1204 uint32_t reference_count
1205 ) noexcept {
1206 return reference_count == 0U ? 0U : query_plane_bytes(1U) + plane_alignment - 1U;
1207 }
1208
1210 [[nodiscard]] constexpr size_t single_query_workspace_bytes() const noexcept {
1212 }
1213
1214 [[nodiscard]] static constexpr uint32_t single_query_result_count(
1215 uint32_t reference_count
1216 ) noexcept {
1217 return reference_count;
1218 }
1219
1221 [[nodiscard]] constexpr uint32_t single_query_result_count() const noexcept {
1222 return metadata_.reference_count;
1223 }
1224
1227 cuda::stream_ref stream
1228 ) const {
1229 CUDDL_TRY(validate_index_storage());
1230 if (metadata_.reference_count == 0U) {
1231 return size_t{0};
1232 }
1233
1234 size_t selection_bytes = 0;
1235 auto const ids = cuda::make_counting_iterator(uint32_t{0});
1236 auto const selection = cuda_try(
1237 cub::DeviceSelect::If(
1238 nullptr,
1239 selection_bytes,
1240 ids,
1241 static_cast<uint32_t*>(nullptr),
1242 static_cast<uint32_t*>(nullptr),
1243 static_cast<int64_t>(metadata_.reference_count),
1244 detail::minimum_match_predicate{nullptr, 1U},
1245 stream.get()
1246 )
1247 );
1248 if (!selection) {
1249 return Err(selection.error());
1250 }
1251 // The counting pass stages two words per indexed bucket in the selection storage.
1252 selection_bytes = std::max(
1253 selection_bytes,
1254 static_cast<size_t>(metadata_.compatibility.indexed_bucket_count) * 2U *
1255 sizeof(uint32_t)
1256 );
1257
1258 constexpr size_t alignment_slack = plane_alignment - 1U + 3U + 255U;
1259 auto const arrays_bytes =
1260 query_plane_bytes(1U) +
1261 static_cast<size_t>(metadata_.reference_count) * 2U * sizeof(uint32_t);
1262 if (selection_bytes > std::numeric_limits<size_t>::max() - arrays_bytes - alignment_slack) {
1263 return Err(Error::resource("indexed single-query workspace size overflows"));
1264 }
1265 return arrays_bytes + alignment_slack + selection_bytes;
1266 }
1267
1270 batch_search_requirements(uint32_t query_count, cuda::stream_ref stream) const {
1271 auto const pair_count =
1272 CUDDL_TRY(dense_batch_pair_count(external_batch_tile(query_count, stream)));
1273 return make_batch_requirements(
1274 pair_count, pair_count, false, pair_count / reference_divisor()
1275 );
1276 }
1277
1280 indexed_batch_search_requirements(uint32_t query_count, cuda::stream_ref stream) const {
1281 CUDDL_TRY(validate_index_storage());
1282 auto const pair_count =
1283 CUDDL_TRY(dense_batch_pair_count(external_batch_tile(query_count, stream)));
1284 return make_batch_requirements(
1285 pair_count, pair_count, true, pair_count / reference_divisor()
1286 );
1287 }
1288
1291 auto const slot = CUDDL_TRY(all_to_all_slot_pairs());
1292 if (slot > std::numeric_limits<uint32_t>::max() / 2U) {
1293 return Err(Error::resource("all-to-all tile exceeds 32-bit result counts"));
1294 }
1295 return make_batch_requirements(
1296 0U, pipeline_slots(all_to_all_tile_count()) * slot, false, 0U
1297 );
1298 }
1299
1300 [[nodiscard]] static constexpr uint32_t all_to_all_result_capacity(
1301 uint32_t reference_count
1302 ) noexcept {
1303 if (reference_count < 2U) {
1304 return 0U;
1305 }
1307 auto const pair_count = static_cast<uint64_t>(query_count) *
1308 (2ULL * reference_count - query_count - 1ULL) / 2ULL;
1309 auto const tiles = (reference_count + query_count - 1U) / query_count;
1310 return static_cast<uint32_t>(pair_count * pipeline_slots(tiles));
1311 }
1312
1317 auto requirements = CUDDL_TRY(indexed_all_to_all_tile_search_requirements(
1319 ));
1320 auto const exhaustive = CUDDL_TRY(all_to_all_search_requirements());
1321 requirements.maximum_pair_count =
1322 std::max(requirements.maximum_pair_count, exhaustive.maximum_pair_count);
1323 requirements.result_bytes = std::max(requirements.result_bytes, exhaustive.result_bytes);
1324 requirements.match_count_bytes =
1325 std::max(requirements.match_count_bytes, exhaustive.match_count_bytes);
1326 return requirements;
1327 }
1328
1332 score_compatibility const& query_compatibility,
1333 device_span<uint8_t> workspace,
1335 cuda::stream_ref stream
1336 ) const {
1337 device_scope const scope{stream};
1338 auto const expected_scores = static_cast<size_t>(metadata_.reference_count) * BucketCount;
1339 if (!rows_match_metadata(expected_scores)) {
1340 return Err(Error::invalid_argument("database extent does not match its metadata"));
1341 }
1342 if (query.size() != BucketCount || query.data() == nullptr) {
1343 return Err(Error::invalid_argument("query must contain one complete score row"));
1344 }
1345 CUDDL_TRY(
1346 (detail::validate_score_compatibility<K, BucketCount, Layout>(query_compatibility))
1347 );
1348 if (query_compatibility != metadata_.compatibility) {
1349 return Err(Error::invalid_argument("query construction metadata is incompatible"));
1350 }
1351 if (workspace.size_bytes() < single_query_workspace_bytes()) {
1352 return Err(Error::resource("single-query workspace is too small"));
1353 }
1354 if (results.size() < metadata_.reference_count) {
1355 return Err(Error::resource("single-query result capacity is too small"));
1356 }
1357 if (metadata_.reference_count == 0U) {
1358 return Ok();
1359 }
1360 if (results.data() == nullptr) {
1361 return Err(Error::invalid_argument("result storage must be device accessible"));
1362 }
1363
1364 if (workspace.data() == nullptr) {
1365 return Err(Error::invalid_argument("single-query workspace must be device accessible"));
1366 }
1367 auto* query_planes = CUDDL_TRY(stage_query_planes(query, 0U, 1U, workspace, stream));
1368 detail::single_query_planes_kernel<BucketCount>
1371 0,
1372 stream.get()>>>(
1373 query_planes,
1374 planes_.data(),
1375 metadata_.reference_count,
1376 nullptr,
1377 nullptr,
1378 results.data()
1379 );
1380 return cuda_try(cudaGetLastError());
1381 }
1382
1386 score_compatibility const& query_compatibility,
1387 device_span<uint8_t> workspace,
1389 device_span<uint32_t> result_count,
1390 search_options options,
1391 cuda::stream_ref stream
1392 ) const {
1393 device_scope const scope{stream};
1394 auto const expected_scores = static_cast<size_t>(metadata_.reference_count) * BucketCount;
1395 if (!rows_match_metadata(expected_scores)) {
1396 return Err(Error::invalid_argument("database extent does not match its metadata"));
1397 }
1398 CUDDL_TRY(validate_index_storage());
1399 if (query.size() != BucketCount || query.data() == nullptr) {
1400 return Err(Error::invalid_argument("query must contain one complete score row"));
1401 }
1402 CUDDL_TRY(
1403 (detail::validate_score_compatibility<K, BucketCount, Layout>(query_compatibility))
1404 );
1405 if (query_compatibility != metadata_.compatibility) {
1406 return Err(Error::invalid_argument("query construction metadata is incompatible"));
1407 }
1408 if (options.minimum_matches > metadata_.compatibility.indexed_bucket_count) {
1409 return Err(Error::invalid_argument("minimum matches exceeds indexed bucket count"));
1410 }
1411 if (results.size() < metadata_.reference_count) {
1412 return Err(Error::resource("indexed result capacity is too small"));
1413 }
1414 if (result_count.empty()) {
1415 return Err(Error::resource("indexed result count capacity is too small"));
1416 }
1417 if (result_count.data() == nullptr) {
1418 return Err(Error::invalid_argument("result count storage must be device accessible"));
1419 }
1420 if (metadata_.reference_count != 0U && results.data() == nullptr) {
1421 return Err(Error::invalid_argument("result storage must be device accessible"));
1422 }
1423
1424 if (options.minimum_matches == 0U) {
1425 CUDDL_TRY(search_async(query, query_compatibility, workspace, results, stream));
1426 return write_batch_result_count(metadata_.reference_count, result_count, stream);
1427 }
1428 if (metadata_.reference_count == 0U) {
1429 return write_batch_result_count(0U, result_count, stream);
1430 }
1431
1432 auto const required_workspace = CUDDL_TRY(indexed_single_query_workspace_bytes(stream));
1433 if (workspace.size_bytes() < required_workspace) {
1434 return Err(Error::resource("indexed single-query workspace is too small"));
1435 }
1436 if (workspace.data() == nullptr) {
1437 return Err(Error::invalid_argument("indexed workspace must be device accessible"));
1438 }
1439
1440 auto const workspace_begin = reinterpret_cast<uintptr_t>(workspace.data());
1441 auto const workspace_end = workspace_begin + workspace.size_bytes();
1442 if (workspace_end < workspace_begin) {
1443 return Err(Error::resource("indexed workspace address range overflows"));
1444 }
1445 auto* query_planes = CUDDL_TRY(stage_query_planes(query, 0U, 1U, workspace, stream));
1446 auto address = detail::align_up(
1447 reinterpret_cast<uintptr_t>(query_planes) + query_plane_bytes(1U), alignof(uint32_t)
1448 );
1449 auto* match_counts = reinterpret_cast<uint32_t*>(address);
1450 address += static_cast<size_t>(metadata_.reference_count) * sizeof(uint32_t);
1451 address = detail::align_up(address, alignof(uint32_t));
1452 auto* candidate_ids = reinterpret_cast<uint32_t*>(address);
1453 address += static_cast<size_t>(metadata_.reference_count) * sizeof(uint32_t);
1454 address = detail::align_up(address, 256U);
1455 if (address > workspace_end) {
1456 return Err(Error::resource("indexed workspace layout exceeds its capacity"));
1457 }
1458 auto* selection_workspace = reinterpret_cast<void*>(address);
1459 auto selection_bytes = static_cast<size_t>(workspace_end - address);
1460
1461 if (!indexed_) {
1462 detail::count_plane_matches_kernel<BucketCount>
1465 0,
1466 stream.get()>>>(
1467 query_planes,
1468 1U,
1469 planes_.data(),
1470 metadata_.reference_count,
1472 metadata_.compatibility.key_mask,
1474 );
1475 CUDDL_CUDA_TRY(cudaGetLastError());
1476 } else {
1478 cuda::fill_bytes(
1479 stream,
1480 cuda::std::span{match_counts, static_cast<size_t>(metadata_.reference_count)},
1481 0
1482 )
1483 );
1484 // The posting ranges live in the selection workspace, which is free until the
1485 // selection below runs on the same stream.
1487 auto* range_begin = static_cast<uint32_t*>(selection_workspace);
1489 detail::index_posting_ranges_kernel<<<
1490 1,
1492 0,
1493 stream.get()>>>(
1494 query.data(),
1495 index_offsets_.data(),
1496 index_keys_.empty() ? nullptr : index_keys_.data(),
1497 metadata_.reference_count,
1499 metadata_.compatibility.key_mask,
1502 );
1503 CUDDL_CUDA_TRY(cudaGetLastError());
1504 auto const counting_blocks =
1505 static_cast<uint32_t>(
1506 stream.device().attribute(cuda::device_attributes::multiprocessor_count)
1507 ) *
1508 static_cast<uint32_t>(stream.device().attribute(
1509 cuda::device_attributes::max_threads_per_multiprocessor
1510 )) /
1512 detail::count_balanced_index_matches_kernel<<<
1513 counting_blocks,
1515 0,
1516 stream.get()>>>(
1520 index_postings_.data(),
1522 );
1523 CUDDL_CUDA_TRY(cudaGetLastError());
1524 }
1525
1526 auto const ids = cuda::make_counting_iterator(uint32_t{0});
1528 cub::DeviceSelect::If(
1529 selection_workspace,
1530 selection_bytes,
1531 ids,
1533 result_count.data(),
1534 static_cast<int64_t>(metadata_.reference_count),
1535 detail::minimum_match_predicate{match_counts, options.minimum_matches},
1536 stream.get()
1537 )
1538 );
1539
1540 detail::single_query_planes_kernel<BucketCount>
1543 0,
1544 stream.get()>>>(
1545 query_planes,
1546 planes_.data(),
1547 metadata_.reference_count,
1549 result_count.data(),
1550 results.data()
1551 );
1552 return cuda_try(cudaGetLastError());
1553 }
1554
1562 template <typename OnTile>
1565 score_compatibility const& query_compatibility,
1566 uint32_t query_id_offset,
1567 device_span<uint8_t> workspace,
1569 OnTile&& on_tile,
1571 cuda::stream_ref stream
1572 ) const {
1573 device_scope const scope{stream};
1574 auto const query_count =
1575 CUDDL_TRY(validate_batch_queries(queries, query_compatibility, query_id_offset));
1576 auto const requirements = CUDDL_TRY(batch_search_requirements(query_count, stream));
1577 CUDDL_TRY(validate_batch_outputs(requirements, results, result_match_counts));
1578 CUDDL_TRY(validate_workspace(requirements, workspace));
1579 // Result storage holds one full-size tile; two half-size tiles alternate in it.
1580 auto const storage_queries = external_batch_tile(query_count, stream);
1581 auto const tile_size = storage_queries < 2U ? storage_queries : storage_queries / 2U;
1582 auto const tiles = tile_size == 0U ? 0U : (query_count + tile_size - 1U) / tile_size;
1583 auto const slot = static_cast<size_t>(tile_size) * metadata_.reference_count;
1584 uint32_t next_tile = 0U;
1585 return pipeline_tiles(
1586 [&]() -> std::optional<uint32_t> {
1587 if (next_tile >= tiles) return std::nullopt;
1588 return next_tile++;
1589 },
1590 slot,
1591 results,
1593 [&](uint32_t tile,
1594 device_span<batch_result_type> slot_results,
1595 device_span<uint32_t> slot_matches,
1596 cuda::stream_ref work) {
1597 auto const first_query_id = tile * tile_size;
1598 return launch_batch_exhaustive<false>(
1599 queries,
1601 std::min(tile_size, query_count - first_query_id),
1603 workspace,
1604 slot_results,
1605 slot_matches,
1606 work
1607 );
1608 },
1609 on_tile,
1610 stream
1611 );
1612 }
1613
1622 template <typename OnTile>
1625 score_compatibility const& query_compatibility,
1626 uint32_t query_id_offset,
1627 device_span<uint8_t> workspace,
1629 OnTile&& on_tile,
1631 search_options options,
1632 cuda::stream_ref stream
1633 ) const {
1634 device_scope const scope{stream};
1635 auto const query_count =
1636 CUDDL_TRY(validate_batch_queries(queries, query_compatibility, query_id_offset));
1637 auto const requirements = CUDDL_TRY(indexed_batch_search_requirements(query_count, stream));
1638 CUDDL_TRY(validate_indexed_batch_inputs(
1639 requirements, workspace, results, result_match_counts, options
1640 ));
1641 auto const tile_size = external_batch_tile(query_count, stream);
1642 for (uint32_t first_query_id = 0U; tile_size != 0U && first_query_id < query_count;
1643 first_query_id += tile_size) {
1644 auto const tile_query_count = std::min(tile_size, query_count - first_query_id);
1645 on_tile(CUDDL_TRY(
1646 launch_batch_indexed<false>(
1647 queries.data() + static_cast<size_t>(first_query_id) * BucketCount,
1649 tile_query_count,
1651 workspace,
1652 results,
1654 options,
1655 stream
1656 )
1657 ));
1658 }
1659 return Ok();
1660 }
1661
1669 template <typename OnTile>
1671 device_span<uint8_t> workspace,
1673 OnTile&& on_tile,
1675 cuda::stream_ref stream,
1676 all_to_all_shard shard = {}
1677 ) const {
1680 workspace, results, std::forward<OnTile>(on_tile), result_match_counts, stream, tiles
1681 );
1682 }
1683
1686 template <typename OnTile>
1688 device_span<uint8_t> workspace,
1690 OnTile&& on_tile,
1692 cuda::stream_ref stream,
1694 ) const {
1695 device_scope const scope{stream};
1696 CUDDL_TRY(validate_stored_rows());
1697 CUDDL_TRY(validate_tile_queue(tiles));
1698 auto const requirements = CUDDL_TRY(all_to_all_search_requirements());
1699 CUDDL_TRY(validate_batch_outputs(requirements, results, result_match_counts));
1700 std::vector<all_to_all_tile_queue::tile> claimed;
1701 return pipeline_tiles(
1702 [&]() -> std::optional<uint32_t> {
1703 auto const tile = tiles.claim();
1704 if (!tile) return std::nullopt;
1705 claimed.push_back(*tile);
1706 return static_cast<uint32_t>(claimed.size() - 1U);
1707 },
1708 CUDDL_TRY(all_to_all_slot_pairs()),
1709 results,
1711 [&](uint32_t index,
1712 device_span<batch_result_type> slot_results,
1713 device_span<uint32_t> slot_matches,
1714 cuda::stream_ref work) {
1715 auto const tile = claimed[index];
1716 return launch_batch_exhaustive<true>(
1717 {},
1718 tile.first_query_id,
1719 tile.query_count,
1720 tile.first_query_id,
1721 workspace,
1722 slot_results,
1723 slot_matches,
1724 work
1725 );
1726 },
1727 on_tile,
1728 stream
1729 );
1730 }
1731
1740 template <typename OnTile>
1742 device_span<uint8_t> workspace,
1744 OnTile&& on_tile,
1746 search_options options,
1747 cuda::stream_ref stream,
1748 all_to_all_shard shard = {}
1749 ) const {
1752 workspace,
1753 results,
1754 std::forward<OnTile>(on_tile),
1756 options,
1757 stream,
1758 tiles
1759 );
1760 }
1761
1764 template <typename OnTile>
1766 device_span<uint8_t> workspace,
1768 OnTile&& on_tile,
1770 search_options options,
1771 cuda::stream_ref stream,
1773 ) const {
1774 device_scope const scope{stream};
1775 CUDDL_TRY(validate_stored_rows());
1776 CUDDL_TRY(validate_tile_queue(tiles));
1777 auto const requirements = CUDDL_TRY(indexed_all_to_all_search_requirements());
1778 CUDDL_TRY(validate_indexed_batch_inputs(
1779 requirements, workspace, results, result_match_counts, options
1780 ));
1781 // Every tile reuses the same storage, so the next tile is claimed once the previous
1782 // tile's consumers are done; tiles claimed from a shared queue then go to whichever
1783 // device frees up first.
1784 std::optional<cuda::event> consumed;
1785 while (true) {
1786 if (consumed) CUDDL_CUDA_TRY(consumed->sync());
1787 auto const tile = tiles.claim();
1788 if (!tile) break;
1789 on_tile(CUDDL_TRY(
1790 launch_batch_indexed<true>(
1791 nullptr,
1792 tile->first_query_id,
1793 tile->query_count,
1794 tile->first_query_id,
1795 workspace,
1796 results,
1798 options,
1799 stream
1800 )
1801 ));
1802 if (!consumed) consumed.emplace(CUDDL_CUDA_TRY(cuda::event(stream.device())));
1803 CUDDL_CUDA_TRY(consumed->record(stream));
1804 }
1805 return Ok();
1806 }
1807
1808 private:
1810 [[nodiscard]] static constexpr uint32_t pipeline_slots(uint32_t tiles) noexcept {
1811 return tiles < 2U ? 1U : 2U;
1812 }
1813
1814 [[nodiscard]] constexpr uint32_t all_to_all_tile_count() const noexcept {
1815 auto const tile_size = detail::all_to_all_tile_queries(metadata_.reference_count);
1816 return tile_size == 0U ? 0U : (metadata_.reference_count + tile_size - 1U) / tile_size;
1817 }
1818
1820 [[nodiscard]] Result<void> validate_tile_queue(all_to_all_tile_queue const& tiles) const {
1821 if (tiles.reference_count() != metadata_.reference_count) {
1822 return Err(Error::invalid_argument("tile queue was made for another reference count"));
1823 }
1824 return Ok();
1825 }
1826
1828 [[nodiscard]] Result<uint32_t> all_to_all_slot_pairs() const {
1829 return all_to_all_pair_count(
1831 );
1832 }
1833
1848 template <typename Next, typename Launch, typename OnTile>
1849 [[nodiscard]] Result<void> pipeline_tiles(
1850 Next&& next,
1851 size_t slot,
1852 device_span<batch_result_type> results,
1853 device_span<uint32_t> match_counts,
1854 Launch&& launch,
1855 OnTile&& on_tile,
1856 cuda::stream_ref stream
1857 ) const {
1858 auto const slot_of = [&](uint32_t tile) {
1859 auto const offset = static_cast<size_t>(tile % 2U) * slot;
1860 return std::pair{
1861 device_span<batch_result_type>{results.data() + offset, slot},
1862 match_counts.empty() ? match_counts
1863 : device_span<uint32_t>{match_counts.data() + offset, slot}
1864 };
1865 };
1866 auto const first = next();
1867 if (!first) return Ok();
1868 auto ahead = next();
1869 if (!ahead) {
1870 auto const [slot_results, slot_matches] = slot_of(*first);
1871 on_tile(CUDDL_TRY(launch(*first, slot_results, slot_matches, stream)));
1872 return Ok();
1873 }
1874 auto work = CUDDL_CUDA_TRY(cuda::stream(stream.device()));
1875 std::array<cuda::event, 2> searched{
1876 CUDDL_CUDA_TRY(cuda::event(stream.device())),
1877 CUDDL_CUDA_TRY(cuda::event(stream.device())),
1878 };
1879 std::array<batch_result_tile, 2> pending{};
1880 auto const enqueue = [&](uint32_t tile) -> Result<void> {
1881 // The slot's previous tile is consumed once the caller's stream reaches this point.
1882 CUDDL_CUDA_TRY(work.wait(stream));
1883 auto const [slot_results, slot_matches] = slot_of(tile);
1884 pending[tile % 2U] = CUDDL_TRY(launch(tile, slot_results, slot_matches, work));
1885 CUDDL_CUDA_TRY(searched[tile % 2U].record(work));
1886 return Ok();
1887 };
1888 auto consumed = CUDDL_CUDA_TRY(cuda::event(stream.device()));
1889 CUDDL_TRY(enqueue(*first));
1890 for (auto tile = *first;; ++tile) {
1891 bool const more = ahead.has_value();
1892 if (more) CUDDL_TRY(enqueue(*ahead));
1893 CUDDL_CUDA_TRY(stream.wait(searched[tile % 2U]));
1894 on_tile(pending[tile % 2U]);
1895 if (!more) return Ok();
1896 // Tile n + 2 takes this tile's slot, so it is claimed once this tile's consumers are
1897 // done, while tile n + 1 keeps the device busy.
1898 CUDDL_CUDA_TRY(consumed.record(stream));
1899 CUDDL_CUDA_TRY(consumed.sync());
1900 ahead = next();
1901 }
1902 }
1903
1904 [[nodiscard]] Result<cuddl::batch_search_requirements>
1905 indexed_all_to_all_tile_search_requirements(
1906 uint32_t first_query_id,
1907 uint32_t query_count
1908 ) const {
1909 CUDDL_TRY(validate_index_storage());
1910 auto const dense_pair_count = CUDDL_TRY(dense_batch_pair_count(query_count));
1911 auto const result_pair_count =
1912 CUDDL_TRY(all_to_all_pair_count(first_query_id, query_count));
1913 return make_batch_requirements(dense_pair_count, result_pair_count, true, query_count);
1914 }
1915
1916 [[nodiscard]] Result<void> validate_stored_rows() const {
1917 auto const expected_scores = static_cast<size_t>(metadata_.reference_count) * BucketCount;
1918 if (!rows_match_metadata(expected_scores)) {
1919 return Err(Error::invalid_argument("database extent does not match its metadata"));
1920 }
1921 return Ok();
1922 }
1923
1924 [[nodiscard]] Result<void> validate_index_storage() const {
1925 CUDDL_TRY((detail::validate_indexed_score_compatibility<K, BucketCount, Layout>(
1926 metadata_.compatibility
1927 )));
1928 if (!indexed_) return Ok();
1929 auto const cell_count = detail::indexed_cell_count(metadata_.compatibility);
1930 auto const expected_postings = static_cast<size_t>(
1932 );
1933 auto const sparse = index_offsets_.empty();
1934 auto const valid_keys =
1935 sparse ? index_keys_.size() == expected_postings &&
1936 (expected_postings == 0U || index_keys_.data() != nullptr)
1937 : index_keys_.empty() &&
1938 index_offsets_.size() == static_cast<size_t>(cell_count + 1U) &&
1939 index_offsets_.data() != nullptr;
1940 if (!indexed_ || !valid_keys || index_postings_.size() != expected_postings ||
1941 (expected_postings != 0U && index_postings_.data() == nullptr)) {
1942 return Err(Error::invalid_argument("database has no valid retrieval index"));
1943 }
1944 return Ok();
1945 }
1946
1947 [[nodiscard]] uint32_t
1948 external_batch_tile(uint32_t query_count, cuda::stream_ref stream) const {
1950 metadata_.reference_count,
1952 detail::external_batch_query_limit(metadata_.reference_count, stream.device())
1953 );
1954 }
1955
1956 [[nodiscard]] Result<uint32_t> dense_batch_pair_count(uint32_t query_count) const {
1957 auto const pair_count = static_cast<uint64_t>(query_count) * metadata_.reference_count;
1958 if (pair_count > std::numeric_limits<uint32_t>::max()) {
1959 return Err(Error::resource("batch pair count exceeds 32-bit workspace IDs"));
1960 }
1961 return static_cast<uint32_t>(pair_count);
1962 }
1963
1964 [[nodiscard]] Result<uint32_t>
1965 all_to_all_pair_count(uint32_t first_query_id, uint32_t query_count) const {
1966 if (first_query_id > metadata_.reference_count ||
1968 return Err(Error::invalid_argument("all-to-all query tile is outside the database"));
1969 }
1970 if (query_count == 0U) {
1971 return 0U;
1972 }
1973
1974 uint64_t left = query_count;
1975 uint64_t right = 2U * static_cast<uint64_t>(metadata_.reference_count) -
1976 2U * first_query_id - query_count - 1U;
1977 if ((left & 1U) == 0U) {
1978 left /= 2U;
1979 } else {
1980 right /= 2U;
1981 }
1982 auto const pair_count = left * right;
1983 if (pair_count > std::numeric_limits<uint32_t>::max()) {
1984 return Err(Error::resource("all-to-all tile exceeds 32-bit result counts"));
1985 }
1986 return static_cast<uint32_t>(pair_count);
1987 }
1988
1990 static constexpr size_t plane_alignment = 16U;
1991
1993 [[nodiscard]] static constexpr size_t query_plane_bytes(uint32_t query_count) noexcept {
1994 return static_cast<size_t>(query_count) * BucketCount * sizeof(uint16_t);
1995 }
1996
1998 [[nodiscard]] constexpr uint32_t reference_divisor() const noexcept {
1999 return metadata_.reference_count == 0U ? 1U : metadata_.reference_count;
2000 }
2001
2004 [[nodiscard]] constexpr bool compares_planes() const noexcept {
2005 return !indexed_ ||
2006 (bitmap_cells_.empty() && index_pair_fraction_ > detail::index_pair_fraction_limit);
2007 }
2008
2015 [[nodiscard]] Result<cuddl::batch_search_requirements> make_batch_requirements(
2016 uint32_t dense_pair_count,
2017 uint32_t maximum_pair_count,
2018 bool indexed,
2019 uint32_t staged_queries
2020 ) const {
2022 .maximum_pair_count = maximum_pair_count,
2023 .result_bytes = static_cast<size_t>(maximum_pair_count) * sizeof(batch_result_type),
2024 .match_count_bytes = static_cast<size_t>(maximum_pair_count) * sizeof(uint32_t),
2025 };
2026 auto const staging_bytes = query_plane_bytes(staged_queries);
2027 if (!indexed || dense_pair_count == 0U) {
2028 // The refinement's cell counter follows the staged query planes.
2029 if (maximum_pair_count != 0U) {
2030 requirements.counter_bytes = sizeof(uint32_t);
2031 requirements.workspace_bytes =
2032 staging_bytes + plane_alignment - 1U + sizeof(uint32_t);
2033 }
2034 return requirements;
2035 }
2036
2037 auto const tile_query_count = dense_pair_count / reference_divisor();
2038 auto const word_count = static_cast<size_t>(tile_query_count) *
2039 detail::candidate_bit_words(metadata_.reference_count);
2040 requirements.candidate_bytes = word_count * sizeof(uint32_t);
2041 if (!compares_planes()) {
2042 requirements.counter_bytes = static_cast<size_t>(dense_pair_count) * sizeof(uint32_t);
2043 if (!index_keys_.empty()) {
2044 // Sparse counting first stages the tile's posting ranges.
2045 requirements.temporary_bytes = static_cast<size_t>(tile_query_count) *
2047 sizeof(uint2);
2048 }
2049 if (!bitmap_cells_.empty()) {
2050 requirements.temporary_bytes = static_cast<size_t>(tile_query_count) *
2052 (sizeof(uint2) + sizeof(uint32_t)) +
2053 sizeof(uint32_t));
2054 }
2055 } else {
2056 // The match filter and refinement reuse one cell counter.
2057 requirements.counter_bytes = sizeof(uint32_t);
2058 }
2059 constexpr size_t alignment_slack = alignof(uint32_t) - 1U + plane_alignment - 1U + 255U;
2060 requirements.workspace_bytes = requirements.counter_bytes + requirements.candidate_bytes +
2061 staging_bytes + requirements.temporary_bytes +
2062 alignment_slack;
2063 return requirements;
2064 }
2065
2066 [[nodiscard]] static Result<void> validate_workspace(
2067 cuddl::batch_search_requirements const& requirements,
2068 device_span<uint8_t> workspace
2069 ) {
2070 if (workspace.size_bytes() < requirements.workspace_bytes) {
2071 return Err(Error::resource("batch workspace is too small"));
2072 }
2073 if (requirements.workspace_bytes != 0U && workspace.data() == nullptr) {
2074 return Err(Error::invalid_argument("batch workspace must be device accessible"));
2075 }
2076 return Ok();
2077 }
2078
2081 [[nodiscard]] Result<uint32_t*> stage_query_planes(
2082 device_span<score_type const> queries,
2083 size_t first_row,
2084 uint32_t count,
2085 device_span<uint8_t> workspace,
2086 cuda::stream_ref stream
2087 ) const {
2088 auto const address =
2089 detail::align_up(reinterpret_cast<uintptr_t>(workspace.data()), plane_alignment);
2090 if (address + query_plane_bytes(count) >
2091 reinterpret_cast<uintptr_t>(workspace.data()) + workspace.size_bytes()) {
2092 return Err(Error::resource("query staging exceeds the workspace"));
2093 }
2094 auto* planes = reinterpret_cast<uint32_t*>(address);
2095 detail::build_score_planes_kernel<BucketCount>
2096 <<<detail::warp_grid_blocks(static_cast<size_t>(count) * (BucketCount / 32U)),
2098 0,
2099 stream.get()>>>(queries.data() + first_row * BucketCount, count, planes);
2100 CUDDL_CUDA_TRY(cudaGetLastError());
2101 return planes;
2102 }
2103
2104 [[nodiscard]] Result<uint32_t> validate_batch_queries(
2105 device_span<score_type const> queries,
2106 score_compatibility const& query_compatibility,
2107 uint32_t query_id_offset
2108 ) const {
2109 CUDDL_TRY(validate_stored_rows());
2110 if (queries.size() % BucketCount != 0U) {
2111 return Err(Error::invalid_argument("query tile must contain complete score rows"));
2112 }
2113 if (!queries.empty() && queries.data() == nullptr) {
2114 return Err(Error::invalid_argument("query tile must be device accessible"));
2115 }
2116 auto const query_count = queries.size() / BucketCount;
2117 if (query_count > std::numeric_limits<uint32_t>::max()) {
2118 return Err(Error::resource("query tile exceeds stable 32-bit IDs"));
2119 }
2120 auto const count = static_cast<uint32_t>(query_count);
2121 if (count != 0U && query_id_offset > std::numeric_limits<uint32_t>::max() - (count - 1U)) {
2122 return Err(Error::resource("query IDs exceed the stable 32-bit range"));
2123 }
2124 if (auto const validation =
2125 detail::validate_score_compatibility<K, BucketCount, Layout>(query_compatibility);
2126 !validation) {
2127 return Err(validation.error());
2128 }
2129 if (query_compatibility != metadata_.compatibility) {
2130 return Err(Error::invalid_argument("query construction metadata is incompatible"));
2131 }
2132 return count;
2133 }
2134
2135 [[nodiscard]] Result<void> validate_batch_outputs(
2136 cuddl::batch_search_requirements const& requirements,
2137 device_span<batch_result_type> results,
2138 device_span<uint32_t> result_match_counts
2139 ) const {
2140 if (results.size() < requirements.maximum_pair_count) {
2141 return Err(Error::resource("batch result capacity is too small"));
2142 }
2143 if (requirements.maximum_pair_count != 0U && results.data() == nullptr) {
2144 return Err(Error::invalid_argument("batch results must be device accessible"));
2145 }
2146 if (!result_match_counts.empty()) {
2147 if (result_match_counts.size() < requirements.maximum_pair_count) {
2148 return Err(Error::resource("batch match-count capacity is too small"));
2149 }
2150 if (result_match_counts.data() == nullptr) {
2151 return Err(Error::invalid_argument("batch match counts must be device accessible"));
2152 }
2153 }
2154 return Ok();
2155 }
2156
2157 [[nodiscard]] Result<void> validate_indexed_batch_inputs(
2158 cuddl::batch_search_requirements const& requirements,
2159 device_span<uint8_t> workspace,
2160 device_span<batch_result_type> results,
2161 device_span<uint32_t> result_match_counts,
2162 search_options options
2163 ) const {
2164 CUDDL_TRY(validate_index_storage());
2165 if (options.minimum_matches > metadata_.compatibility.indexed_bucket_count) {
2166 return Err(Error::invalid_argument("minimum matches exceeds indexed bucket count"));
2167 }
2168 if (workspace.size_bytes() < requirements.workspace_bytes) {
2169 return Err(Error::resource("indexed batch workspace is too small"));
2170 }
2171 if (requirements.workspace_bytes != 0U && workspace.data() == nullptr) {
2172 return Err(
2173 Error::invalid_argument("indexed batch workspace must be device accessible")
2174 );
2175 }
2176 return validate_batch_outputs(requirements, results, result_match_counts);
2177 }
2178
2179 [[nodiscard]] static Result<void> write_batch_result_count(
2180 uint32_t count,
2181 device_span<uint32_t> result_count,
2182 cuda::stream_ref stream
2183 ) {
2184 return cuda_try(
2185 cub::DeviceTransform::Transform(
2186 cuda::make_constant_iterator(count),
2187 result_count.data(),
2188 1,
2189 cuda::std::identity{},
2190 stream
2191 )
2192 );
2193 }
2194
2198 template <bool AllToAll>
2199 [[nodiscard]] Result<batch_result_tile> launch_batch_exhaustive(
2200 device_span<score_type const> queries,
2201 uint32_t first_query,
2202 uint32_t query_count,
2203 uint32_t query_id_offset,
2204 device_span<uint8_t> workspace,
2205 device_span<batch_result_type> results,
2206 device_span<uint32_t> result_match_counts,
2207 cuda::stream_ref stream
2208 ) const {
2209 uint32_t const* query_planes = nullptr;
2210 if constexpr (AllToAll) {
2211 query_planes = planes_.data() + static_cast<size_t>(first_query) * (BucketCount / 2U);
2212 } else {
2213 query_planes =
2214 CUDDL_TRY(stage_query_planes(queries, first_query, query_count, workspace, stream));
2215 }
2216 // The cell counter follows the staged query planes; see make_batch_requirements.
2217 auto const counter_address =
2218 detail::align_up(reinterpret_cast<uintptr_t>(workspace.data()), plane_alignment) +
2219 query_plane_bytes(AllToAll ? 0U : query_count);
2220 if (counter_address + sizeof(uint32_t) >
2221 reinterpret_cast<uintptr_t>(workspace.data()) + workspace.size_bytes()) {
2222 return Err(Error::resource("exhaustive batch workspace is too small"));
2223 }
2224 auto* cell_counter = reinterpret_cast<uint32_t*>(counter_address);
2225 CUDDL_CUDA_TRY(cudaMemsetAsync(cell_counter, 0, sizeof(uint32_t), stream.get()));
2226 if (deltas_.child_count != 0U && result_match_counts.empty() && corrects_deltas(stream)) {
2227 // Bases are refined exactly; every child pair then follows from its base pair.
2228 CUDDL_TRY((launch_refine<detail::refine_candidates::reference_mask, AllToAll>(
2229 query_planes,
2232 static_cast<uint32_t>(BucketCount),
2233 uint16_t{0xffffU},
2234 deltas_.base_bits,
2235 results,
2237 stream,
2238 0U,
2239 nullptr,
2241 )));
2242 auto const kernel =
2243 detail::apply_reference_deltas_kernel<BucketCount, batch_result_type, AllToAll>;
2244 CUDDL_CUDA_TRY(cudaFuncSetAttribute(
2245 reinterpret_cast<void const*>(kernel),
2246 cudaFuncAttributeMaxDynamicSharedMemorySize,
2247 static_cast<int>(detail::delta_correction_shared_bytes<BucketCount>)
2248 ));
2249 kernel<<<
2250 static_cast<uint32_t>(
2251 stream.device().attribute(cuda::device_attributes::multiprocessor_count)
2252 ),
2254 detail::delta_correction_shared_bytes<BucketCount>,
2255 stream.get()>>>(
2256 query_planes,
2259 metadata_.reference_count,
2260 deltas_,
2261 results.data()
2262 );
2263 CUDDL_CUDA_TRY(cudaGetLastError());
2264 } else {
2265 // Over every bucket with the full key, index match counts are the equal buckets.
2266 CUDDL_TRY((launch_refine<detail::refine_candidates::all, AllToAll>(
2267 query_planes,
2270 static_cast<uint32_t>(BucketCount),
2271 uint16_t{0xffffU},
2272 nullptr,
2273 results,
2275 stream,
2276 0U,
2277 nullptr,
2279 )));
2280 }
2281 return make_tile(
2283 );
2284 }
2285
2287 [[nodiscard]] static bool corrects_deltas(cuda::stream_ref stream) {
2288 return static_cast<size_t>(stream.device().attribute(
2289 cuda::device_attributes::max_shared_memory_per_block_optin
2290 )) >= detail::delta_correction_shared_bytes<BucketCount>;
2291 }
2292
2293 [[nodiscard]] batch_result_tile make_tile(
2294 device_span<batch_result_type> results,
2295 device_span<uint32_t> result_match_counts,
2296 uint32_t const* pass_bits,
2297 uint32_t first_query_id,
2298 uint32_t query_count,
2299 bool upper_triangle
2300 ) const noexcept {
2301 return {
2302 .results = results.data(),
2303 .match_counts = result_match_counts.empty() ? nullptr : result_match_counts.data(),
2304 .pass_bits = pass_bits,
2305 .first_query_id = first_query_id,
2306 .query_count = query_count,
2307 .reference_count = metadata_.reference_count,
2308 .bucket_count = static_cast<uint32_t>(BucketCount),
2309 .upper_triangle = upper_triangle,
2310 };
2311 }
2312
2316 template <detail::refine_candidates Candidates, bool UpperTriangle>
2317 [[nodiscard]] Result<void> launch_refine(
2318 uint32_t const* query_planes,
2319 uint32_t query_id_offset,
2320 uint32_t query_count,
2321 uint32_t indexed_bucket_count,
2322 uint16_t key_mask,
2323 uint32_t const* candidate_bits,
2324 device_span<batch_result_type> results,
2325 device_span<uint32_t> result_match_counts,
2326 cuda::stream_ref stream,
2327 uint32_t minimum_matches = 0U,
2328 uint32_t* pass_bits = nullptr,
2329 uint32_t* cell_counter = nullptr
2330 ) const {
2331 auto const counting = !result_match_counts.empty() || pass_bits != nullptr;
2332 if constexpr (
2333 (Candidates == detail::refine_candidates::all ||
2335 detail::match_filter_supported<BucketCount>
2336 ) {
2337 if (!counting) {
2338 return launch_all_pairs<UpperTriangle>(
2339 query_planes,
2342 results,
2343 stream,
2346 : nullptr
2347 );
2348 }
2349 }
2350 auto const select_kernel = [&]<uint32_t Group>() {
2351 return counting ? detail::refine_batch_bitmap_kernel<
2352 BucketCount,
2353 true,
2355 Candidates,
2356 UpperTriangle,
2357 Group>
2358 : detail::refine_batch_bitmap_kernel<
2359 BucketCount,
2360 false,
2362 Candidates,
2363 UpperTriangle,
2364 Group>;
2365 };
2366 auto kernel = select_kernel.template operator()<detail::bitmap_refine_group<BucketCount>>();
2367 auto shared_bytes = detail::bitmap_refine_dynamic_bytes<BucketCount>;
2368 if constexpr (detail::bitmap_refine_staged<BucketCount>) {
2369 // On Hopper the densest query groups, not L2 row reads, set the kernel's time: the
2370 // 115,223-genome threshold search took 416 ms on a GH200 with groups of 8 queries,
2371 // 438 ms with 24 and 445 ms with 32. An RTX 5070 Ti is fastest with 24.
2372 constexpr uint32_t hopper_group =
2373 cuda::std::min(8U, detail::bitmap_refine_group<BucketCount>);
2374 if (stream.device().attribute(cuda::device_attributes::compute_capability_major) == 9) {
2375 kernel = select_kernel.template operator()<hopper_group>();
2376 shared_bytes = hopper_group * BucketCount * sizeof(uint16_t);
2377 }
2378 }
2379 CUDDL_CUDA_TRY(cudaFuncSetAttribute(
2380 reinterpret_cast<void const*>(kernel),
2381 cudaFuncAttributeMaxDynamicSharedMemorySize,
2382 static_cast<int>(shared_bytes)
2383 ));
2384 int resident_blocks = 0;
2385 CUDDL_CUDA_TRY(cudaOccupancyMaxActiveBlocksPerMultiprocessor(
2386 &resident_blocks, kernel, detail::bitmap_refine_block_size, shared_bytes
2387 ));
2388 auto const multiprocessors = static_cast<uint32_t>(
2389 stream.device().attribute(cuda::device_attributes::multiprocessor_count)
2390 );
2391 kernel<<<
2392 multiprocessors* static_cast<uint32_t>(resident_blocks > 0 ? resident_blocks : 1),
2394 shared_bytes,
2395 stream.get()>>>(
2396 query_planes,
2399 planes_.data(),
2400 metadata_.reference_count,
2402 key_mask,
2404 results.data(),
2405 result_match_counts.empty() ? nullptr : result_match_counts.data(),
2407 pass_bits,
2409 );
2410 return cuda_try(cudaGetLastError());
2411 }
2412
2415 [[nodiscard]] Result<uint16_t const*> row_major_rows(cuda::stream_ref stream) const {
2416 static std::mutex mutex;
2417 // Never freed: the buffers would outlive the CUDA context at exit.
2418 static auto* const decoded = new std::map<uint32_t const*, uint16_t*>();
2419 std::lock_guard lock(mutex);
2420 auto& rows = (*decoded)[planes_.data()];
2421 if (rows == nullptr) {
2422 auto const count = metadata_.reference_count;
2423 CUDDL_CUDA_TRY(cudaMalloc(&rows, static_cast<size_t>(count) * BucketCount * 2U));
2424 detail::decode_score_planes_kernel<BucketCount>
2425 <<<detail::warp_grid_blocks(static_cast<size_t>(count) * (BucketCount / 32U)),
2427 0,
2428 stream.get()>>>(planes_.data(), count, rows);
2429 CUDDL_CUDA_TRY(cudaGetLastError());
2430 }
2432 }
2433
2437 template <bool UpperTriangle>
2438 [[nodiscard]] Result<void> launch_all_pairs(
2439 uint32_t const* query_planes,
2440 uint32_t query_id_offset,
2441 uint32_t query_count,
2442 device_span<batch_result_type> results,
2443 cuda::stream_ref stream,
2444 uint32_t* cell_counter,
2445 uint32_t const* reference_mask
2446 ) const {
2447 if constexpr (detail::ablations_enabled) {
2448 bool const naive = detail::ablated("lop3-kernel");
2449 if (naive || detail::ablated("bit-slicing")) {
2450 auto const* rows = CUDDL_TRY(row_major_rows(stream));
2451 auto const kernel = naive ? detail::row_major_all_pairs_kernel<
2452 BucketCount,
2454 UpperTriangle,
2455 false>
2456 : detail::row_major_all_pairs_kernel<
2457 BucketCount,
2459 UpperTriangle,
2460 true>;
2461 auto const multiprocessors = static_cast<uint32_t>(
2462 stream.device().attribute(cuda::device_attributes::multiprocessor_count)
2463 );
2464 kernel<<<multiprocessors * 8U, detail::block_size, 0, stream.get()>>>(
2465 rows,
2468 metadata_.reference_count,
2469 results.data(),
2471 );
2472 return cuda_try(cudaGetLastError());
2473 }
2474 }
2475 constexpr auto group = detail::bitmap_refine_group<BucketCount>;
2476 constexpr auto shared_bytes = detail::bitmap_refine_dynamic_bytes<BucketCount>;
2477 auto const kernel =
2478 detail::refine_all_pairs_kernel<BucketCount, batch_result_type, UpperTriangle, group>;
2479 CUDDL_CUDA_TRY(cudaFuncSetAttribute(
2480 reinterpret_cast<void const*>(kernel),
2481 cudaFuncAttributeMaxDynamicSharedMemorySize,
2482 static_cast<int>(shared_bytes)
2483 ));
2484 auto const multiprocessors = static_cast<uint32_t>(
2485 stream.device().attribute(cuda::device_attributes::multiprocessor_count)
2486 );
2487 kernel<<<multiprocessors, detail::all_pairs_block_size, shared_bytes, stream.get()>>>(
2488 query_planes,
2491 planes_.data(),
2492 metadata_.reference_count,
2493 results.data(),
2496 );
2497 return cuda_try(cudaGetLastError());
2498 }
2499
2503 template <bool UpperTriangle>
2504 [[nodiscard]] Result<void> launch_match_filter(
2505 uint32_t const* query_planes,
2506 uint32_t query_id_offset,
2507 uint32_t query_count,
2508 uint32_t indexed_bucket_count,
2509 uint32_t minimum_matches,
2510 uint32_t* candidate_bits,
2511 uint32_t* cell_counter,
2512 cuda::stream_ref stream
2513 ) const {
2514 constexpr auto group = detail::bitmap_refine_group<BucketCount>;
2515 constexpr auto shared_bytes = detail::bitmap_refine_dynamic_bytes<BucketCount>;
2516 auto const kernel = detail::match_filter_kernel<BucketCount, UpperTriangle, group>;
2517 CUDDL_CUDA_TRY(cudaFuncSetAttribute(
2518 reinterpret_cast<void const*>(kernel),
2519 cudaFuncAttributeMaxDynamicSharedMemorySize,
2520 static_cast<int>(shared_bytes)
2521 ));
2522 int resident_blocks = 0;
2523 CUDDL_CUDA_TRY(cudaOccupancyMaxActiveBlocksPerMultiprocessor(
2524 &resident_blocks, kernel, detail::match_filter_block_size, shared_bytes
2525 ));
2526 auto const multiprocessors = static_cast<uint32_t>(
2527 stream.device().attribute(cuda::device_attributes::multiprocessor_count)
2528 );
2529 kernel<<<
2530 multiprocessors* static_cast<uint32_t>(resident_blocks > 0 ? resident_blocks : 1),
2532 shared_bytes,
2533 stream.get()>>>(
2534 query_planes,
2537 planes_.data(),
2538 metadata_.reference_count,
2543 );
2544 return cuda_try(cudaGetLastError());
2545 }
2546
2554 template <bool AllToAll>
2555 [[nodiscard]] Result<batch_result_tile> launch_batch_indexed(
2556 score_type const* external_queries,
2557 uint32_t first_query,
2558 uint32_t query_count,
2559 uint32_t query_id_offset,
2560 device_span<uint8_t> workspace,
2561 device_span<batch_result_type> results,
2562 device_span<uint32_t> result_match_counts,
2563 search_options options,
2564 cuda::stream_ref stream
2565 ) const {
2566 auto const requirements = CUDDL_TRY(make_batch_requirements(
2567 CUDDL_TRY(dense_batch_pair_count(query_count)), 0U, true, query_count
2568 ));
2569 auto const workspace_begin = reinterpret_cast<uintptr_t>(workspace.data());
2570 auto const workspace_end = workspace_begin + workspace.size_bytes();
2571 if (workspace_end < workspace_begin) {
2572 return Err(Error::resource("indexed batch workspace address range overflows"));
2573 }
2574 auto const word_count = static_cast<size_t>(query_count) *
2575 detail::candidate_bit_words(metadata_.reference_count);
2576 auto address = detail::align_up(workspace_begin, alignof(uint32_t));
2577 auto* match_counts = reinterpret_cast<uint32_t*>(address);
2578 address += requirements.counter_bytes;
2579 auto* candidate_bits = reinterpret_cast<uint32_t*>(address);
2580 address += requirements.candidate_bytes;
2581 address = detail::align_up(address, plane_alignment);
2582 auto* staging = reinterpret_cast<void*>(address);
2583 address += query_plane_bytes(query_count);
2584 address = detail::align_up(address, 256U);
2585 if (address + requirements.temporary_bytes > workspace_end) {
2586 return Err(Error::resource("indexed batch workspace layout exceeds its capacity"));
2587 }
2588 auto* temporary_workspace = reinterpret_cast<void*>(address);
2589 constexpr uint32_t warp_width = 32;
2590 constexpr uint32_t warps_per_block = detail::block_size / warp_width;
2591 auto const multiprocessors = static_cast<uint32_t>(
2592 stream.device().attribute(cuda::device_attributes::multiprocessor_count)
2593 );
2594 auto const tile_pair_count = query_count * metadata_.reference_count;
2595 auto const reference_count = metadata_.reference_count;
2597 auto const key_mask = metadata_.compatibility.key_mask;
2598 auto const result_tile = make_tile(
2600 );
2601 if (word_count == 0U) {
2602 return result_tile;
2603 }
2604
2605 // External queries arrive as scores and are converted to bit-planes; all-to-all queries
2606 // are database planes, decoded to scores only for the index lookups.
2607 uint16_t const* query_scores = external_queries;
2608 uint32_t const* query_planes = nullptr;
2609 if constexpr (AllToAll) {
2610 query_planes = planes_.data() + static_cast<size_t>(first_query) * (BucketCount / 2U);
2611 if (!compares_planes()) {
2612 auto* decoded = static_cast<uint16_t*>(staging);
2613 detail::decode_score_planes_kernel<BucketCount>
2615 static_cast<size_t>(query_count) * (BucketCount / warp_width)
2616 ),
2618 0,
2619 stream.get()>>>(query_planes, query_count, decoded);
2620 CUDDL_CUDA_TRY(cudaGetLastError());
2621 query_scores = decoded;
2622 }
2623 } else {
2624 auto* converted = static_cast<uint32_t*>(staging);
2625 detail::build_score_planes_kernel<BucketCount>
2627 static_cast<size_t>(query_count) * (BucketCount / warp_width)
2628 ),
2630 0,
2631 stream.get()>>>(external_queries, query_count, converted);
2632 CUDDL_CUDA_TRY(cudaGetLastError());
2633 query_planes = converted;
2634 }
2635
2636 auto const refine_selected = [&](bool thresholded) -> Result<void> {
2637 CUDDL_CUDA_TRY(cudaMemsetAsync(match_counts, 0, sizeof(uint32_t), stream.get()));
2638 return launch_refine<detail::refine_candidates::bitmap, AllToAll>(
2639 query_planes,
2643 key_mask,
2645 results,
2647 stream,
2648 thresholded ? options.minimum_matches : 0U,
2649 thresholded ? candidate_bits : nullptr,
2651 );
2652 };
2653
2654 if (compares_planes()) {
2655 if constexpr (AllToAll) {
2656 // The filter and refinement kernels skip words wholly below the triangle, so
2657 // those words must already read as no pair.
2659 cudaMemsetAsync(candidate_bits, 0, word_count * sizeof(uint32_t), stream.get())
2660 );
2661 }
2662 // A low-plane bound selects candidates; the refinement counts their matches exactly
2663 // and keeps, in the same bitmap, those meeting the threshold.
2664 if constexpr (detail::match_filter_supported<BucketCount>) {
2665 if (!detail::ablated("match-filter")) {
2667 cudaMemsetAsync(match_counts, 0, sizeof(uint32_t), stream.get())
2668 );
2669 CUDDL_TRY((launch_match_filter<AllToAll>(
2670 query_planes,
2674 options.minimum_matches,
2677 stream
2678 )));
2679 CUDDL_TRY(refine_selected(true));
2680 return result_tile;
2681 }
2682 }
2683 // Every passing pair's summary lands at its slot as the pass bitmap is written.
2684 CUDDL_TRY((launch_refine<detail::refine_candidates::all, AllToAll>(
2685 query_planes,
2689 key_mask,
2690 nullptr,
2691 results,
2693 stream,
2694 options.minimum_matches,
2696 )));
2697 return result_tile;
2698 }
2699
2700 auto const bits_from_counts = [&]() -> Result<void> {
2701 detail::candidate_bits_from_counts_kernel<<<
2702 detail::warp_grid_blocks((word_count + warp_width - 1U) / warp_width),
2704 0,
2705 stream.get()>>>(
2709 options.minimum_matches,
2711 AllToAll,
2713 );
2714 return cuda_try(cudaGetLastError());
2715 };
2720 multiprocessors,
2721 static_cast<uint32_t>(stream.device().attribute(
2722 cuda::device_attributes::max_shared_memory_per_block_optin
2723 ))
2724 );
2725 auto const tiles = (reference_count + tile - 1U) / tile;
2726 // Sparse ranges are resolved bucket-major first: a block's lookups all search one
2727 // bucket's keys, so their shared upper search levels stay in L1.
2728 uint2* ranges = nullptr;
2729 uint32_t counter_bits = 0U;
2730 if (!index_keys_.empty()) {
2731 ranges = static_cast<uint2*>(temporary_workspace);
2732 detail::sparse_batch_posting_ranges_kernel<BucketCount>
2733 <<<indexed_bucket_count, detail::block_size, 0, stream.get()>>>(
2734 query_scores,
2735 0U,
2737 index_keys_.data(),
2738 key_directory_.data(),
2741 key_mask,
2742 ranges
2743 );
2744 CUDDL_CUDA_TRY(cudaGetLastError());
2745 }
2746 auto const hybrid = !bitmap_cells_.empty();
2747 uint32_t* starts = nullptr;
2748 uint32_t* counts = nullptr;
2749 if (hybrid) {
2750 ranges = static_cast<uint2*>(temporary_workspace);
2751 auto const range_count = static_cast<size_t>(query_count) * indexed_bucket_count;
2752 starts = reinterpret_cast<uint32_t*>(ranges + range_count);
2753 counts = starts + range_count;
2754 detail::hybrid_posting_ranges_kernel<BucketCount>
2755 <<<query_count, detail::block_size, 0, stream.get()>>>(
2756 query_scores,
2757 index_offsets_.empty() ? nullptr : index_offsets_.data(),
2758 bitmap_cells_.data(),
2759 static_cast<uint32_t>(bitmap_cells_.size()),
2760 detail::candidate_bit_words(reference_count),
2762 key_mask,
2763 ranges,
2764 starts,
2765 counts
2766 );
2767 CUDDL_CUDA_TRY(cudaGetLastError());
2768 counter_bits = std::max<uint32_t>(1U, std::bit_width(options.minimum_matches));
2769 }
2770 auto const count_postings = [&]<uint32_t Bits>() -> Result<void> {
2771 CUDDL_CUDA_TRY(cudaFuncSetAttribute(
2772 reinterpret_cast<void const*>(
2773 detail::count_batch_index_tile_kernel<BucketCount, Bits, uint16_t>
2774 ),
2775 cudaFuncAttributeMaxDynamicSharedMemorySize,
2776 static_cast<int>(tile / 2U * sizeof(uint32_t))
2777 ));
2778 detail::count_batch_index_tile_kernel<BucketCount, Bits>
2779 <<<query_count * tiles,
2780 detail::index_tile_block_size,
2781 tile / 2U * sizeof(uint32_t),
2782 stream.get()>>>(
2783 query_scores,
2784 0U,
2785 index_offsets_.data(),
2786 index_postings_.data(),
2789 key_mask,
2790 tile,
2792 index_keys_.empty() ? nullptr : index_keys_.data(),
2793 ranges,
2794 options.minimum_matches,
2796 AllToAll,
2798 hybrid ? posting_bitmaps_.data() : nullptr,
2799 starts,
2800 counts
2801 );
2802 return cuda_try(cudaGetLastError());
2803 };
2804 switch (counter_bits) {
2805 case 0:
2806 CUDDL_TRY((count_postings.template operator()<0>()));
2807 break;
2808 case 1:
2809 CUDDL_TRY((count_postings.template operator()<1>()));
2810 break;
2811 case 2:
2812 CUDDL_TRY((count_postings.template operator()<2>()));
2813 break;
2814 case 3:
2815 CUDDL_TRY((count_postings.template operator()<3>()));
2816 break;
2817 case 4:
2818 CUDDL_TRY((count_postings.template operator()<4>()));
2819 break;
2820 case 5:
2821 CUDDL_TRY((count_postings.template operator()<5>()));
2822 break;
2823 case 6:
2824 CUDDL_TRY((count_postings.template operator()<6>()));
2825 break;
2826 case 7:
2827 CUDDL_TRY((count_postings.template operator()<7>()));
2828 break;
2829 case 8:
2830 CUDDL_TRY((count_postings.template operator()<8>()));
2831 break;
2832 case 9:
2833 CUDDL_TRY((count_postings.template operator()<9>()));
2834 break;
2835 case 10:
2836 CUDDL_TRY((count_postings.template operator()<10>()));
2837 break;
2838 case 11:
2839 CUDDL_TRY((count_postings.template operator()<11>()));
2840 break;
2841 case 12:
2842 CUDDL_TRY((count_postings.template operator()<12>()));
2843 break;
2844 case 13:
2845 CUDDL_TRY((count_postings.template operator()<13>()));
2846 break;
2847 case 14:
2848 CUDDL_TRY((count_postings.template operator()<14>()));
2849 break;
2850 case 15:
2851 CUDDL_TRY((count_postings.template operator()<15>()));
2852 break;
2853 case 16:
2854 CUDDL_TRY((count_postings.template operator()<16>()));
2855 break;
2856 }
2857 } else {
2859 cuda::fill_bytes(
2860 stream, cuda::std::span{match_counts, static_cast<size_t>(tile_pair_count)}, 0
2861 )
2862 );
2863 auto const query_buckets = static_cast<size_t>(query_count) * indexed_bucket_count;
2864 constexpr auto cells_per_block = warps_per_block * detail::index_match_cells_per_warp;
2865 auto const bucket_blocks = static_cast<uint32_t>(
2866 std::min<size_t>((query_buckets + cells_per_block - 1U) / cells_per_block, 65535U)
2867 );
2868 detail::count_batch_index_matches_kernel<BucketCount>
2869 <<<bucket_blocks, detail::block_size, 0, stream.get()>>>(
2870 query_scores,
2871 0U,
2873 index_offsets_.data(),
2874 index_postings_.data(),
2877 key_mask,
2879 index_keys_.data()
2880 );
2881 CUDDL_CUDA_TRY(cudaGetLastError());
2882 CUDDL_TRY(bits_from_counts());
2883 }
2884 CUDDL_TRY(refine_selected(false));
2885 return result_tile;
2886 }
2887
2888 private:
2889 [[nodiscard]] __host__ __device__ constexpr bool rows_match_metadata(
2890 size_t expected_scores
2891 ) const noexcept {
2892 return planes_.size() * 2U == expected_scores &&
2893 (expected_scores == 0U || planes_.data() != nullptr);
2894 }
2895
2896 device_span<uint32_t const> planes_;
2897 reference_database_metadata metadata_;
2898 device_span<uint32_t const> index_offsets_;
2899 device_span<uint32_t const> index_postings_;
2900 device_span<uint16_t const> index_keys_;
2901 device_span<uint32_t const> key_directory_;
2902 device_span<uint32_t const> bitmap_cells_;
2903 device_span<uint32_t const> posting_bitmaps_;
2904 reference_delta_view deltas_;
2905 double index_pair_fraction_{};
2906 bool indexed_{};
2907};
2908
2909} // namespace detail
2910
2918template <uint32_t K, size_t BucketCount, typename Layout = default_register_layout>
2921 template <uint32_t, size_t, typename>
2922 friend class reference_index;
2924 static_assert(K >= 1 && K <= 31);
2925 static_assert(BucketCount >= (size_t{1} << 11) && BucketCount <= (size_t{1} << 13));
2926 static_assert((BucketCount & (BucketCount - 1)) == 0);
2927
2928 public:
2929 using layout_type = Layout;
2930 using score_type = uint16_t;
2933
2936
2937 // An explicit body keeps NVCC from inferring device-side buffer destruction.
2938 __host__ ~reference_database() {} // NOLINT(modernize-use-equals-default)
2939
2941 : planes_(std::move(other.planes_)),
2942 device_(other.device_),
2943 metadata_(std::exchange(other.metadata_, {})),
2944 names_(std::move(other.names_)),
2945 identity_(std::move(other.identity_)),
2946 deltas_(std::move(other.deltas_)) {}
2947
2949 if (this != &other) {
2950 planes_ = std::move(other.planes_);
2951 device_ = other.device_;
2952 metadata_ = std::exchange(other.metadata_, {});
2953 names_ = std::move(other.names_);
2954 identity_ = std::move(other.identity_);
2955 deltas_ = std::move(other.deltas_);
2956 }
2957 return *this;
2958 }
2959
2967 score_compatibility compatibility,
2968 cuda::stream_ref stream
2969 ) {
2970 detail::device_scope const scope{stream};
2971 auto const reference_count = CUDDL_TRY(validate_rows(rows, compatibility));
2972 auto database = CUDDL_CUDA_TRY(reference_database(stream));
2973 database.metadata_ = {.compatibility = compatibility, .reference_count = reference_count};
2974 if (!rows.empty()) {
2975 database.planes_ = CUDDL_CUDA_TRY(
2976 cuda::make_device_buffer<uint32_t>(
2977 stream, stream.device(), rows.size() / 2U, cuda::no_init
2978 )
2979 );
2980 detail::build_score_planes_kernel<BucketCount>
2981 <<<detail::
2982 warp_grid_blocks(static_cast<size_t>(reference_count) * (BucketCount / 32U)),
2984 0,
2985 stream.get()>>>(rows.data(), reference_count, database.planes_.data());
2986 CUDDL_CUDA_TRY(cudaGetLastError());
2987 // Rows are compared as base references plus changed buckets; see
2988 // detail::reference_delta_view. Building it waits for the stream.
2989 if (!detail::ablated("deltas")) {
2990 database.deltas_ = CUDDL_TRY(
2991 detail::build_reference_deltas<BucketCount>(
2992 rows.data(), reference_count, stream
2993 )
2994 );
2995 }
2996 }
2997 return Result<reference_database>::ok(std::move(database));
2998 }
2999
3001 [[nodiscard]] Result<void>
3002 copy_scores_async(device_span<score_type> scores, cuda::stream_ref stream) const {
3003 detail::device_scope const scope{stream};
3004 auto const count = static_cast<size_t>(metadata_.reference_count) * BucketCount;
3005 if (scores.size() < count) {
3006 return Err(Error::resource("score destination is too small"));
3007 }
3008 if (count == 0U) {
3009 return Ok();
3010 }
3011 if (scores.data() == nullptr) {
3012 return Err(Error::invalid_argument("score destination must be device accessible"));
3013 }
3014 detail::decode_score_planes_kernel<BucketCount>
3016 static_cast<size_t>(metadata_.reference_count) * (BucketCount / 32U)
3017 ),
3019 0,
3020 stream.get()>>>(planes_.data(), metadata_.reference_count, scores.data());
3021 return cuda_try(cudaGetLastError());
3022 }
3023
3025 [[nodiscard]] std::span<std::string const> names() const noexcept {
3026 return names_;
3027 }
3028
3030 [[nodiscard]] reference_database_metadata metadata() const noexcept {
3031 return metadata_;
3032 }
3033
3035 [[nodiscard]] uint32_t reference_count() const noexcept {
3036 return metadata_.reference_count;
3037 }
3038
3040 [[nodiscard]] cuda::device_ref device() const noexcept {
3041 return device_;
3042 }
3043
3045 [[nodiscard]] static constexpr size_t persistent_row_bytes(uint32_t reference_count) noexcept {
3047 }
3048
3050 [[nodiscard]] size_t persistent_row_bytes() const noexcept {
3051 return view().persistent_row_bytes();
3052 }
3053
3054 [[nodiscard]] static constexpr size_t single_query_workspace_bytes(
3055 uint32_t reference_count
3056 ) noexcept {
3058 }
3059
3061 [[nodiscard]] size_t single_query_workspace_bytes() const noexcept {
3062 return view().single_query_workspace_bytes();
3063 }
3064
3065 [[nodiscard]] static constexpr uint32_t all_to_all_result_capacity(
3066 uint32_t reference_count
3067 ) noexcept {
3069 }
3070
3071 [[nodiscard]] static constexpr uint32_t single_query_result_count(
3072 uint32_t reference_count
3073 ) noexcept {
3075 }
3076
3078 [[nodiscard]] uint32_t single_query_result_count() const noexcept {
3079 return view().single_query_result_count();
3080 }
3081
3084 score_compatibility const& query_compatibility,
3085 device_span<uint8_t> workspace,
3087 cuda::stream_ref stream
3088 ) const {
3089 return view().search_async(query, query_compatibility, workspace, results, stream);
3090 }
3091
3092 template <typename OnTile>
3095 score_compatibility const& query_compatibility,
3096 uint32_t query_id_offset,
3097 device_span<uint8_t> workspace,
3099 OnTile&& on_tile,
3100 device_span<uint32_t> result_match_counts,
3101 cuda::stream_ref stream
3102 ) const {
3103 return view().search_batch_async(
3104 queries,
3105 query_compatibility,
3106 query_id_offset,
3107 workspace,
3108 results,
3109 std::forward<OnTile>(on_tile),
3110 result_match_counts,
3111 stream
3112 );
3113 }
3114
3115 template <typename OnTile>
3117 device_span<uint8_t> workspace,
3119 OnTile&& on_tile,
3120 device_span<uint32_t> result_match_counts,
3121 cuda::stream_ref stream,
3122 all_to_all_shard shard = {}
3123 ) const {
3124 return view().search_all_to_all_async(
3125 workspace, results, std::forward<OnTile>(on_tile), result_match_counts, stream, shard
3126 );
3127 }
3128
3131 template <typename OnTile>
3133 device_span<uint8_t> workspace,
3135 OnTile&& on_tile,
3136 device_span<uint32_t> result_match_counts,
3137 cuda::stream_ref stream,
3139 ) const {
3140 return view().search_all_to_all_async(
3141 workspace, results, std::forward<OnTile>(on_tile), result_match_counts, stream, tiles
3142 );
3143 }
3144
3146 cuda::stream_ref stream,
3147 reference_index<K, BucketCount, Layout> const* index = nullptr
3148 ) const {
3149 auto search_view = CUDDL_TRY(view(index));
3150 return search_view.indexed_single_query_workspace_bytes(stream);
3151 }
3152
3154 uint32_t query_count,
3155 cuda::stream_ref stream,
3156 reference_index<K, BucketCount, Layout> const* index = nullptr
3157 ) const {
3158 auto search_view = CUDDL_TRY(view(index));
3159 return search_view.indexed_batch_search_requirements(query_count, stream);
3160 }
3161
3163 reference_index<K, BucketCount, Layout> const* index = nullptr
3164 ) const {
3165 auto search_view = CUDDL_TRY(view(index));
3166 return search_view.indexed_all_to_all_search_requirements();
3167 }
3168
3171 score_compatibility const& query_compatibility,
3172 device_span<uint8_t> workspace,
3174 device_span<uint32_t> result_count,
3175 search_options options,
3176 cuda::stream_ref stream,
3177 reference_index<K, BucketCount, Layout> const* index = nullptr
3178 ) const {
3179 auto search_view = CUDDL_TRY(view(index));
3180 return search_view.search_indexed_async(
3181 query, query_compatibility, workspace, results, result_count, options, stream
3182 );
3183 }
3184
3185 template <typename OnTile>
3188 score_compatibility const& query_compatibility,
3189 uint32_t query_id_offset,
3190 device_span<uint8_t> workspace,
3192 OnTile&& on_tile,
3193 device_span<uint32_t> result_match_counts,
3194 search_options options,
3195 cuda::stream_ref stream,
3196 reference_index<K, BucketCount, Layout> const* index = nullptr
3197 ) const {
3198 auto search_view = CUDDL_TRY(view(index));
3199 return search_view.search_batch_indexed_async(
3200 queries,
3201 query_compatibility,
3202 query_id_offset,
3203 workspace,
3204 results,
3205 std::forward<OnTile>(on_tile),
3206 result_match_counts,
3207 options,
3208 stream
3209 );
3210 }
3211
3212 template <typename OnTile>
3214 device_span<uint8_t> workspace,
3216 OnTile&& on_tile,
3217 device_span<uint32_t> result_match_counts,
3218 search_options options,
3219 cuda::stream_ref stream,
3220 reference_index<K, BucketCount, Layout> const* index = nullptr,
3221 all_to_all_shard shard = {}
3222 ) const {
3223 auto search_view = CUDDL_TRY(view(index));
3224 return search_view.search_all_to_all_indexed_async(
3225 workspace,
3226 results,
3227 std::forward<OnTile>(on_tile),
3228 result_match_counts,
3229 options,
3230 stream,
3231 shard
3232 );
3233 }
3234
3237 template <typename OnTile>
3239 device_span<uint8_t> workspace,
3241 OnTile&& on_tile,
3242 device_span<uint32_t> result_match_counts,
3243 search_options options,
3244 cuda::stream_ref stream,
3247 ) const {
3248 auto search_view = CUDDL_TRY(view(index));
3249 return search_view.search_all_to_all_indexed_async(
3250 workspace,
3251 results,
3252 std::forward<OnTile>(on_tile),
3253 result_match_counts,
3254 options,
3255 stream,
3256 tiles
3257 );
3258 }
3259
3260 private:
3262
3263 [[nodiscard]] view_type view() const noexcept {
3264 return view_type(
3265 {planes_.data(), planes_.size()},
3266 metadata_,
3267 {},
3268 {},
3269 false,
3270 {},
3271 {},
3272 0.0,
3273 {},
3274 {},
3275 delta_view()
3276 );
3277 }
3278
3279 [[nodiscard]] Result<view_type> view(
3280 reference_index<K, BucketCount, Layout> const* index
3281 ) const {
3282 if (index == nullptr) return view();
3283 if (!identity_ || index->identity_ != identity_ || !index->indexed_) {
3284 return Err(Error::invalid_argument("index does not belong to this database"));
3285 }
3286 auto const offset_count = index->index_offsets_.size();
3287 auto const offsets =
3288 device_span<uint32_t const>{index->index_offsets_.data(), offset_count};
3289 auto const postings = device_span<uint32_t const>{
3290 index->index_postings_.data(), index->index_posting_capacity_
3291 };
3292 return view_type(
3293 {planes_.data(), planes_.size()},
3294 metadata_,
3295 offsets,
3296 postings,
3297 index->indexed_,
3298 index->index_keys_,
3299 index->key_directory_,
3300 detail::ablated("index-fallback") ? 0.0 : index->pair_fraction_,
3301 {index->bitmap_cells_.data(), index->bitmap_count_},
3302 index->posting_bitmaps_,
3303 delta_view()
3304 );
3305 }
3306
3307 [[nodiscard]] detail::reference_delta_view delta_view() const noexcept {
3308 return deltas_ ? deltas_->view() : detail::reference_delta_view{};
3309 }
3310
3311 explicit reference_database(cuda::stream_ref stream)
3312 : planes_(stream, cuda::device_default_memory_pool(stream.device())),
3313 device_(stream.device()) {}
3314
3315 [[nodiscard]] static Result<uint32_t>
3316 validate_rows(device_span<score_type const> rows, score_compatibility compatibility) {
3317 CUDDL_TRY(
3318 (detail::validate_indexed_score_compatibility<K, BucketCount, Layout>(compatibility))
3319 );
3320 if (rows.size() % BucketCount != 0U) {
3321 return Err(Error::invalid_argument("row extent must contain complete rows"));
3322 }
3323 if (!rows.empty() && rows.data() == nullptr) {
3324 return Err(Error::invalid_argument("rows must be device accessible"));
3325 }
3326 auto const reference_count = rows.size() / BucketCount;
3327 if (reference_count > std::numeric_limits<uint32_t>::max()) {
3328 return Err(Error::resource("reference count exceeds stable 32-bit IDs"));
3329 }
3330 return static_cast<uint32_t>(reference_count);
3331 }
3332
3333 cuda::device_buffer<uint32_t> planes_;
3334 cuda::device_ref device_{0};
3335 reference_database_metadata metadata_{};
3336 std::vector<std::string> names_;
3337 // A shared identity prevents accepting an index for a different or recycled row allocation.
3338 std::shared_ptr<char const> identity_ = std::make_shared<char>(0);
3339 std::optional<detail::reference_deltas> deltas_;
3340};
3341
3342} // namespace cuddl
3343
Thread-safe source of the all-to-all tiles one process searches.
uint32_t tile_queries() const noexcept
Query rows per tile (the last tile may hold fewer).
static Result< all_to_all_tile_queue > make(uint32_t reference_count, all_to_all_shard shard={}, uint32_t searchers=1U, std::function< uint64_t()> next_tile={})
size_t size() const noexcept
Tiles this process owns, or every tile of the search when they are shared.
std::optional< tile > claim() noexcept
The next unclaimed tile, or nothing once all are claimed or after cancel.
void cancel() noexcept
Makes every later claim of this queue return nothing, for example after a device failed.
bool shared() const noexcept
Whether processes share the tiles through a next_tile counter.
size_t position(uint32_t first_query_id) const noexcept
Position, in claim order, of the tile starting at first_query_id.
all_to_all_tile_queue(all_to_all_tile_queue &&other) noexcept
uint32_t reference_count() const noexcept
Reference count the tiles cover.
Non-owning, trivially copyable view of one selected reference-row backing.
constexpr reference_database_metadata metadata() const noexcept
Compatibility metadata and reference count.
__host__ __device__ constexpr reference_database_view(device_span< uint32_t const > planes, reference_database_metadata metadata, device_span< uint32_t const > index_offsets={}, device_span< uint32_t const > index_postings={}, bool indexed=false, device_span< uint16_t const > index_keys={}, device_span< uint32_t const > key_directory={}, double index_pair_fraction=0.0, device_span< uint32_t const > bitmap_cells={}, device_span< uint32_t const > posting_bitmaps={}, reference_delta_view deltas={}) noexcept
Result< cuddl::batch_search_requirements > indexed_all_to_all_search_requirements() const
Storage for every unique database-row pair through the index, or with a threshold but no index; it al...
constexpr uint32_t single_query_result_count() const noexcept
Results one exhaustive query writes: one per reference.
Result< void > search_all_to_all_indexed_async(device_span< uint8_t > workspace, device_span< batch_result_type > results, OnTile &&on_tile, device_span< uint32_t > result_match_counts, search_options options, cuda::stream_ref stream, all_to_all_shard shard={}) const
Indexed search of every unique database-row pair using bounded storage.
Result< cuddl::batch_search_requirements > all_to_all_search_requirements() const
Storage reused while exhaustively searching every unique database-row pair.
constexpr size_t persistent_index_bytes() const noexcept
Bytes this view's index needs for offsets, postings, and keys.
static constexpr uint32_t single_query_result_count(uint32_t reference_count) noexcept
Result< void > search_all_to_all_async(device_span< uint8_t > workspace, device_span< batch_result_type > results, OnTile &&on_tile, device_span< uint32_t > result_match_counts, cuda::stream_ref stream, all_to_all_tile_queue &tiles) const
Exhaustive all-to-all search of the tiles claimed from tiles, which other searches,...
Result< cuddl::batch_search_requirements > batch_search_requirements(uint32_t query_count, cuda::stream_ref stream) const
Storage reused while exhaustively searching query_count compact rows.
static constexpr size_t persistent_row_bytes(uint32_t reference_count) noexcept
Bytes one compact row store needs for reference_count references.
static constexpr uint32_t all_to_all_result_capacity(uint32_t reference_count) noexcept
constexpr size_t persistent_row_bytes() const noexcept
Bytes this view's own row store needs.
constexpr uint32_t reference_count() const noexcept
Reference count.
Result< void > search_async(device_span< score_type const > query, score_compatibility const &query_compatibility, device_span< uint8_t > workspace, device_span< result_type > results, cuda::stream_ref stream) const
Compares one compact query row with every reference row on stream.
Result< cuddl::batch_search_requirements > indexed_batch_search_requirements(uint32_t query_count, cuda::stream_ref stream) const
Storage reused while searching query_count compact rows through the index.
Result< void > search_indexed_async(device_span< score_type const > query, score_compatibility const &query_compatibility, device_span< uint8_t > workspace, device_span< result_type > results, device_span< uint32_t > result_count, search_options options, cuda::stream_ref stream) const
Finds and exactly refines references meeting options on stream.
Result< void > search_all_to_all_async(device_span< uint8_t > workspace, device_span< batch_result_type > results, OnTile &&on_tile, device_span< uint32_t > result_match_counts, cuda::stream_ref stream, all_to_all_shard shard={}) const
Exhaustively searches every unique database-row pair using bounded storage.
Result< size_t > indexed_single_query_workspace_bytes(cuda::stream_ref stream) const
Caller-owned bytes required by one positive-threshold indexed query.
constexpr size_t single_query_workspace_bytes() const noexcept
Caller-owned bytes for one exhaustive query on this view.
static constexpr size_t single_query_workspace_bytes(uint32_t reference_count) noexcept
Caller-owned bytes for one exhaustive query: the query's bit-planes.
Result< void > search_batch_indexed_async(device_span< score_type const > queries, score_compatibility const &query_compatibility, uint32_t query_id_offset, device_span< uint8_t > workspace, device_span< batch_result_type > results, OnTile &&on_tile, device_span< uint32_t > result_match_counts, search_options options, cuda::stream_ref stream) const
Indexed search for compact queries using bounded reusable storage.
Result< void > search_all_to_all_indexed_async(device_span< uint8_t > workspace, device_span< batch_result_type > results, OnTile &&on_tile, device_span< uint32_t > result_match_counts, search_options options, cuda::stream_ref stream, all_to_all_tile_queue &tiles) const
Indexed all-to-all search of the tiles claimed from tiles, which other searches may claim from concur...
__host__ __device__ constexpr device_span< uint32_t const > planes() const noexcept
Reference bit-plane rows (detail::score_plane_index layout), in reference-ID order.
constexpr bool has_index() const noexcept
True when an index backs this view.
Result< void > search_batch_async(device_span< score_type const > queries, score_compatibility const &query_compatibility, uint32_t query_id_offset, device_span< uint8_t > workspace, device_span< batch_result_type > results, OnTile &&on_tile, device_span< uint32_t > result_match_counts, cuda::stream_ref stream) const
Exhaustively searches compact queries using bounded reusable storage.
Host copy of a batch_result_tile holding only the passing results, packed in query-major,...
void tile() const &&=delete
friend Result< void > download(batch_result_tile const &, cuda::stream_ref, host_batch_results &)
Copies a device tile's passing results into host's pinned buffers, growing them only when needed,...
void passing_match_counts() const &&=delete
batch_result_tile tile() const &noexcept
View of the packed host results; valid while this object lives.
__host__ host_batch_results(host_batch_results &&other) noexcept
std::span< uint32_t const > passing_match_counts() const &noexcept
The passing results' index match counts, in passing order; empty unless match counts were requested.
host_batch_results(host_batch_results const &)=delete
std::vector< batch_search_result > passing() const
Every passing result, query-major and in reference order, as full values.
host_batch_results & operator=(host_batch_results const &)=delete
host_batch_results(cuda::stream_ref stream, cuda::pinned_memory_pool_ref pool)
Empty results to download into, with pinned buffers taken from pool.
host_batch_results(cuda::stream_ref stream)
Empty results to download into, with pinned buffers for stream's device.
Host-owned reference sketches and labels, ready for binary storage or GPU upload.
Move-only owner of one immutable contiguous reference database.
uint32_t reference_count() const noexcept
Reference count.
static constexpr size_t single_query_workspace_bytes(uint32_t reference_count) noexcept
size_t persistent_row_bytes() const noexcept
Bytes this database's own row store needs.
reference_database(reference_database &&other) noexcept
static constexpr uint32_t all_to_all_result_capacity(uint32_t reference_count) noexcept
Result< void > copy_scores_async(device_span< score_type > scores, cuda::stream_ref stream) const
Writes the reference scores, row-major in reference-ID order, to scores.
std::span< std::string const > names() const noexcept
Reference labels in reference-ID order.
static constexpr uint32_t single_query_result_count(uint32_t reference_count) noexcept
uint32_t single_query_result_count() const noexcept
Results one exhaustive query writes: one per reference.
Result< void > search_all_to_all_async(device_span< uint8_t > workspace, device_span< batch_result_type > results, OnTile &&on_tile, device_span< uint32_t > result_match_counts, cuda::stream_ref stream, all_to_all_tile_queue &tiles) const
Exhaustive search of the tiles claimed from tiles; see all_to_all_tile_queue.
Result< size_t > search_workspace_bytes(cuda::stream_ref stream, reference_index< K, BucketCount, Layout > const *index=nullptr) const
Result< void > search_batch_async(device_span< score_type const > queries, score_compatibility const &query_compatibility, uint32_t query_id_offset, device_span< uint8_t > workspace, device_span< batch_result_type > results, OnTile &&on_tile, device_span< uint32_t > result_match_counts, cuda::stream_ref stream) const
reference_database_metadata metadata() const noexcept
Compatibility metadata and reference count.
Result< void > search_all_to_all_async(device_span< uint8_t > workspace, device_span< batch_result_type > results, OnTile &&on_tile, device_span< uint32_t > result_match_counts, cuda::stream_ref stream, all_to_all_shard shard={}) const
reference_database & operator=(reference_database &&other) noexcept
reference_database & operator=(reference_database const &)=delete
Result< void > search_all_to_all_async(device_span< uint8_t > workspace, device_span< batch_result_type > results, OnTile &&on_tile, device_span< uint32_t > result_match_counts, search_options options, cuda::stream_ref stream, reference_index< K, BucketCount, Layout > const *index, all_to_all_tile_queue &tiles) const
Indexed or threshold search of the tiles claimed from tiles; see all_to_all_tile_queue.
size_t single_query_workspace_bytes() const noexcept
Caller-owned bytes for one exhaustive query on this database; always zero.
reference_database(reference_database const &)=delete
Result< void > search_async(device_span< score_type const > query, score_compatibility const &query_compatibility, device_span< uint8_t > workspace, device_span< result_type > results, device_span< uint32_t > result_count, search_options options, cuda::stream_ref stream, reference_index< K, BucketCount, Layout > const *index=nullptr) const
static Result< reference_database > build_async(device_span< score_type const > rows, score_compatibility compatibility, cuda::stream_ref stream)
Builds a database from flat row-major scores on stream.
Result< cuddl::batch_search_requirements > all_to_all_search_requirements(reference_index< K, BucketCount, Layout > const *index=nullptr) const
Result< void > search_batch_async(device_span< score_type const > queries, score_compatibility const &query_compatibility, uint32_t query_id_offset, device_span< uint8_t > workspace, device_span< batch_result_type > results, OnTile &&on_tile, device_span< uint32_t > result_match_counts, search_options options, cuda::stream_ref stream, reference_index< K, BucketCount, Layout > const *index=nullptr) const
Result< cuddl::batch_search_requirements > batch_search_requirements(uint32_t query_count, cuda::stream_ref stream, reference_index< K, BucketCount, Layout > const *index=nullptr) const
cuda::device_ref device() const noexcept
Device holding the rows: the device of the stream that built them.
Result< void > search_async(device_span< score_type const > query, score_compatibility const &query_compatibility, device_span< uint8_t > workspace, device_span< result_type > results, cuda::stream_ref stream) const
static constexpr size_t persistent_row_bytes(uint32_t reference_count) noexcept
Bytes one compact row store needs for reference_count references.
Result< void > search_all_to_all_async(device_span< uint8_t > workspace, device_span< batch_result_type > results, OnTile &&on_tile, device_span< uint32_t > result_match_counts, search_options options, cuda::stream_ref stream, reference_index< K, BucketCount, Layout > const *index=nullptr, all_to_all_shard shard={}) const
Index-file persistence and the validation boundary for reference_index.
Move-only acceleration data for an immutable reference database.
#define CUDDL_CUDA_TRY(...)
Propagates a CUDA error wrapped in cuddl::Result<void>.
Definition error.hpp:248
#define CUDDL_TRY(expr)
Propagates a cuddl::Result failure from the enclosing function (GNU statement expression).
Definition error.hpp:225
uint32_t warp_grid_blocks(size_t warps) noexcept
Blocks for a grid-stride launch covering warps warps of block_size threads.
Definition kernels.cuh:1524
constexpr uint32_t index_match_cells_per_warp
Cells one warp owns per iteration of count_batch_index_matches_kernel: one per lane.
Definition kernels.cuh:392
__global__ uint32_t reference_count
Definition kernels.cuh:437
auto const * query
Definition kernels.cuh:1030
__global__ size_t uint32_t bool blacklist_view bool Layout
Definition kernels.cuh:155
__global__ uint32_t const uint32_t const * pass_bits
Definition kernels.cuh:1784
__global__ size_t uint32_t const uint32_t const uint32_t uint32_t uint16_t uint32_t uint32_t uint16_t const uint2 const uint32_t uint32_t bool uint32_t * candidate_bits
Definition kernels.cuh:929
constexpr uint32_t batch_query_tile_count
Minimum query rows per batch tile.
constexpr uint32_t match_filter_block_size
Threads per match_filter_kernel block.
Definition kernels.cuh:2090
__host__ __device__ uint64_t batch_result_slot(uint32_t query_index, uint32_t reference_id, uint32_t query_id_offset, uint32_t reference_count) noexcept
Position of the pair (query_index of the tile, reference_id) in a tile's results: rows of reference_c...
Definition kernels.cuh:1751
constexpr uint32_t posting_range_block_size
Threads of the single index_posting_ranges_kernel block.
Definition kernels.cuh:527
__global__ size_t uint32_t const uint32_t const uint32_t uint32_t uint16_t uint32_t uint32_t uint16_t const uint2 const * ranges
Definition kernels.cuh:925
auto const lane
Definition kernels.cuh:966
static __global__ uint32_t const uint32_t uint32_t const uint32_t * match_counts
Definition kernels.cuh:576
__global__ size_t uint32_t const uint32_t const uint32_t uint32_t uint16_t uint32_t uint32_t uint16_t const uint2 const uint32_t minimum_matches
Definition kernels.cuh:926
constexpr uint32_t all_to_all_tile_queries(uint32_t reference_count, uint32_t parts=1U) noexcept
Query rows one all-to-all tile compares: about 2^25 pairs, and at least batch_query_tile_count.
auto const pairs
Definition kernels.cuh:1592
auto const count
Definition kernels.cuh:1031
__global__ uint32_t uint32_t indexed_bucket_count
Definition kernels.cuh:438
@ reference_mask
Every pair of the layout whose reference is set in one reference-wide bitmap.
@ all
Every pair of the layout, or with pass bits requested, those meeting the threshold.
auto const thresholded
Definition kernels.cuh:1888
constexpr uint32_t all_pairs_block_size
Threads per refine_all_pairs_kernel block.
Definition kernels.cuh:2299
Result< void > validate_score_compatibility(score_compatibility const &compatibility)
static __global__ uint32_t const uint16_t const uint32_t uint32_t uint16_t key_mask
Definition kernels.cuh:540
constexpr uint32_t delta_correction_warps
Warps per apply_reference_deltas_kernel block, and the most and fewest children one block cell takes.
constexpr uint32_t batch_query_tile_size(uint32_t reference_count, uint32_t query_count, uint32_t tile_limit=batch_query_tile_count) noexcept
@ device_node
The device's host NUMA node, where the system supports NUMA pools: for buffers the device writes,...
constexpr uint64_t seed
Domain separation constant folded into every packed k-mer before hashing.
Definition hash.cuh:18
static __global__ uint32_t const uint16_t const uint32_t uint32_t uint16_t uint32_t uint32_t * range_end_scan
Definition kernels.cuh:543
Result< void > validate_indexed_score_compatibility(score_compatibility const &compatibility)
constexpr uint32_t block_size
Threads per CTA for the single-pair and cardinality reduction kernels.
Definition kernels.cuh:28
__global__ uint32_t uint32_t uint64_t uint32_t * ids
__global__ size_t uint32_t const uint32_t const uint32_t uint32_t uint16_t uint32_t tile
Definition kernels.cuh:922
__global__ uint32_t uint32_t uint32_t const uint32_t uint32_t uint16_t uint32_t const SearchResult uint32_t * result_match_counts
Definition kernels.cuh:1852
constexpr uint32_t warps_per_block
Definition kernels.cuh:935
__global__ uint32_t const uint32_t const uint32_t const uint32_t first_query_id
Definition kernels.cuh:1786
uintptr_t align_up(uintptr_t address, size_t alignment) noexcept
static __global__ uint32_t const uint16_t const uint32_t uint32_t uint16_t uint32_t * range_begin
Definition kernels.cuh:541
constexpr double index_pair_fraction_limit
Index pair fraction (reference_index::pair_fraction) above which batch search compares bit-planes for...
Result< void > validate_non_indexed_score_compatibility(score_compatibility const &compatibility)
bool uses_tiled_index_counts(uint32_t reference_count, uint32_t query_count, uint32_t indexed_bucket_count) noexcept
Whether a batch counts through count_batch_index_tile_kernel.
Definition kernels.cuh:691
__global__ size_t uint32_t query_count
Definition kernels.cuh:600
__global__ uint32_t const uint32_t uint32_t const uint32_t const SearchResult * results
Definition kernels.cuh:1544
__global__ size_t uint32_t const uint32_t const uint32_t uint32_t uint16_t uint32_t uint32_t uint16_t const uint2 const uint32_t uint32_t query_id_offset
Definition kernels.cuh:927
@ hybrid
CPU inflation, followed by GPU FASTA normalisation of the inflated bytes.
constexpr uint64_t indexed_posting_count(uint32_t reference_count, score_compatibility const &compatibility) noexcept
::cuda::__ensure_current_context device_scope
Makes stream's device current for the enclosing scope and restores the previous context on exit.
constexpr bool ablations_enabled
Definition ablation.hpp:79
__host__ __device__ constexpr uint32_t sparse_directory_entries(uint32_t reference_count) noexcept
Bounds per bucket in a sparse key directory.
Definition kernels.cuh:429
constexpr uint64_t indexed_cell_count(score_compatibility const &compatibility) noexcept
__global__ uint16_t const uint32_t uint32_t uint32_t unsigned long long * work
Definition kernels.cuh:474
constexpr uint32_t group
Definition kernels.cuh:1858
__global__ uint32_t uint32_t uint32_t const uint32_t uint32_t uint16_t uint32_t const SearchResult uint32_t uint32_t uint32_t uint32_t * cell_counter
Definition kernels.cuh:1856
constexpr index_storage smaller_index_storage(uint32_t reference_count, score_compatibility const &compatibility) noexcept
Storage needing fewer device bytes for reference_count references; dense on ties.
constexpr bool ablated(std::string_view) noexcept
Always false: this build compiles the ablation switches out.
Definition ablation.hpp:82
auto const word_count
Definition top_k.cuh:238
constexpr uint32_t bitmap_refine_block_size
Threads per refine_batch_bitmap_kernel block.
Definition kernels.cuh:1649
static __global__ uint32_t const * offsets
Definition kernels.cuh:536
__global__ uint32_t uint32_t uint32_t SearchResult uint32_t const * reference_mask
Definition kernels.cuh:2492
auto const tiles
Definition kernels.cuh:937
uint32_t external_batch_query_limit(uint32_t reference_count, cuda::device_ref device)
Query rows one external batch tile may hold on device.
static __global__ uint32_t const uint32_t uint32_t const * postings
Definition kernels.cuh:574
__host__ __device__ uint16_t score(uint64_t hash) noexcept
Computes the 16-bit DDL rarity score for hash.
Definition register.cuh:43
__global__ uint32_t const uint32_t uint32_t const * candidate_ids
Definition kernels.cuh:1541
__global__ size_t uint16_t * rows
Definition kernels.cuh:1499
uint32_t index_tile_references(uint32_t reference_count, uint32_t query_count, uint32_t multiprocessors, uint32_t max_shared_bytes) noexcept
References per tile, bounded by the device's shared memory and small enough that the batch launches a...
Definition kernels.cuh:702
cuda::pinned_memory_pool_ref device_pinned_pool(cuda::device_ref device, pinned_placement placement)
A page-locked memory pool accessible from device only.
auto const row
Definition top_k.cuh:202
auto const query_id
Definition top_k.cuh:201
Batch operations over contiguous device rows.
Definition a48.hpp:14
@ counts
One 24-byte batch_search_result per pair: query ID, reference ID and the lower, equal,...
Result< void > Ok() noexcept
Success return for Result<void>; same as return {}.
Definition error.hpp:181
Result< void > download(batch_result_tile const &tile, cuda::stream_ref stream, host_batch_results &host)
Copies a device tile's passing results into host's pinned buffers, growing them only when needed,...
index_storage
Dense offsets favor query latency; sparse sorted keys reduce index memory and build time.
cuda::std::unexpected< Error > Err(Error error)
Failure return; converts to any Result<T> via cuda::std::unexpected.
Definition error.hpp:176
Result< void > cuda_try(cudaError_t error, std::source_location location=std::source_location::current())
Checks a CUDA runtime call and returns an error on failure.
Definition error.hpp:187
Result< void > for_each_passing(batch_result_tile const &tile, cuda::stream_ref stream, F &&f)
Calls f on the host with every passing result of a device tile, query-major and in reference order; s...
static Error invalid_argument(std::string message)
Definition error.hpp:99
static Error resource(std::string message)
Definition error.hpp:103
Fallible API result: cuda::std::expected<T, Error> with cuDDL factories.
Definition error.hpp:118
static Result ok(T value)
Definition error.hpp:121
Part cuDDL of count disjoint parts of an all-to-all search.
Non-owning view of one batch-search tile's results.
packed_pairwise_counts const * results
__host__ __device__ uint32_t match_count(uint32_t query_id, uint32_t reference_id) const noexcept
The pair's index match count, in either order for all-to-all tiles; the pair must pass and counts mus...
uint32_t const * match_counts
Index match counts parallel to results, when requested; otherwise null.
__host__ __device__ constexpr uint32_t words_per_query() const noexcept
__host__ __device__ cuda::std::optional< batch_search_result > find(uint32_t query_id, uint32_t reference_id) const noexcept
The pair's result, or nothing when it did not pass.
__host__ __device__ bool passed(uint32_t query_id, uint32_t reference_id) const noexcept
Whether the pair has a result in this tile, in either order for all-to-all tiles.
uint32_t const * word_offsets
For packed results: the number of passing results before each pass-bit word.
uint32_t const * pass_bits
Passing pairs, when the tile applied a threshold; null when every slot is filled.
uint32_t bucket_count
Buckets per sketch, from which both-empty counts are recovered.
__host__ __device__ uint64_t passing_before(uint32_t query) const noexcept
Number of passing results in the tile's query rows before row query, which may be query_count.
__host__ __device__ uint64_t slot_count() const noexcept
Slots of the tile's layout, filled or not.
__host__ __device__ void for_each_passing(uint32_t begin, uint32_t end, F &&f) const
Calls f with the passing results of the tile's query rows [begin, end), in the order of the whole-til...
__host__ __device__ void for_each_passing(F &&f) const
Calls f with every passing result, query-major and in reference order.
__host__ __device__ uint64_t count() const noexcept
Number of passing results.
Caller-owned storage requirements for one bounded query tile.
friend bool operator==(batch_search_requirements const &, batch_search_requirements const &)=default
Stable query/reference IDs and their exact query-relative bucket comparison counts.
friend bool operator==(batch_search_result const &, batch_search_result const &)=default
uint32_t const * base_bits
One bit per reference in the candidate-bitmap layout, set for bases.
__host__ __device__ uint16_t operator()(Row row) const noexcept
__host__ __device__ uint32_t operator()(uint32_t i) const noexcept
__host__ __device__ int64_t operator()(uint32_t bucket) const noexcept
Popcount of one pass-bit word.
Definition kernels.cuh:1768
A span that is assumed to point to device-accessible memory.
One pair's exact bucket comparison counts in 64 bits, the storage and transfer form of batch-search r...
__host__ __device__ constexpr pairwise_counts unpack(uint32_t bucket_count) const noexcept
Raw per-bucket classification counts for one sketch pair comparison.
Recorded metadata for an immutable reference database.
friend bool operator==(reference_database_metadata const &, reference_database_metadata const &)=default
Stable reference ID and exact query-relative bucket comparison counts.
friend bool operator==(reference_search_result const &, reference_search_result const &)=default
Construction parameters of compatible score rows.
friend bool operator==(score_compatibility const &, score_compatibility const &)=default
static constexpr score_compatibility current() noexcept
Metadata for score rows produced by the current cuDDL construction path.
Matching-bucket threshold, independent of whether an index accelerates the search.