cuDDL
Loading...
Searching...
No Matches
reference_index.cuh
Go to the documentation of this file.
1#pragma once
2
4
5namespace cuddl {
6
10template <uint32_t K, size_t BucketCount, typename Layout>
14 friend class reference_database<K, BucketCount, Layout>;
15
16 using score_type = typename database_type::score_type;
17
18 public:
20
22
23 __host__ ~reference_index() {}
24
26 : identity_(std::move(other.identity_)),
27 index_offsets_(std::move(other.index_offsets_)),
28 index_postings_(std::move(other.index_postings_)),
29 index_keys_(std::move(other.index_keys_)),
30 key_directory_(std::move(other.key_directory_)),
31 bitmap_cells_(std::move(other.bitmap_cells_)),
32 posting_bitmaps_(std::move(other.posting_bitmaps_)),
33 bitmap_count_(std::exchange(other.bitmap_count_, 0U)),
34 index_posting_capacity_(std::exchange(other.index_posting_capacity_, 0)),
35 pair_work_(std::exchange(other.pair_work_, 0ULL)),
36 pair_fraction_(std::exchange(other.pair_fraction_, 0.0)),
37 indexed_(std::exchange(other.indexed_, false)) {}
38
41 if (this != &other) {
42 identity_ = std::move(other.identity_);
43 index_offsets_ = std::move(other.index_offsets_);
44 index_postings_ = std::move(other.index_postings_);
45 index_keys_ = std::move(other.index_keys_);
46 key_directory_ = std::move(other.key_directory_);
47 bitmap_cells_ = std::move(other.bitmap_cells_);
48 posting_bitmaps_ = std::move(other.posting_bitmaps_);
49 bitmap_count_ = std::exchange(other.bitmap_count_, 0U);
50 index_posting_capacity_ = std::exchange(other.index_posting_capacity_, 0);
51 pair_work_ = std::exchange(other.pair_work_, 0ULL);
52 pair_fraction_ = std::exchange(other.pair_fraction_, 0.0);
53 indexed_ = std::exchange(other.indexed_, false);
54 }
55 return *this;
56 }
57
59 [[nodiscard]] index_storage storage() const noexcept {
60 return index_offsets_.empty() ? index_storage::sparse : index_storage::dense;
61 }
62
66 database_type const& database,
67 cuda::stream_ref stream,
69 ) {
70 detail::device_scope const scope{stream};
71 auto index = CUDDL_TRY(build_index<score_type>(database, stream, storage));
72 CUDDL_TRY(index.measure_pair_fraction(database, stream));
73 if (!detail::ablated("posting-bitmaps")) {
74 CUDDL_TRY(index.build_posting_bitmaps(database, stream));
75 }
76 return Result<reference_index>::ok(std::move(index));
77 }
78
82 [[nodiscard]] double pair_fraction() const noexcept {
83 return pair_fraction_;
84 }
85
87 [[nodiscard]] size_t persistent_index_bytes() const noexcept {
88 return index_offsets_.size() * sizeof(uint32_t) +
89 index_postings_.size() * sizeof(uint32_t) + index_keys_.size() * sizeof(uint16_t);
90 }
91
92 private:
94 [[nodiscard]] Result<void>
95 measure_pair_fraction(database_type const& database, cuda::stream_ref stream) {
96 auto const reference_count = database.reference_count();
97 auto const compatibility = database.metadata().compatibility;
98 pair_fraction_ = 0.0;
99 pair_work_ = 0U;
100 if (reference_count == 0U || compatibility.indexed_bucket_count == 0U) {
101 return Ok();
102 }
103 auto work = CUDDL_CUDA_TRY(
104 cuda::make_device_buffer<unsigned long long>(stream, stream.device(), 1U, 0ULL)
105 );
106 detail::index_pair_work_kernel<<<
108 warp_grid_blocks(static_cast<size_t>(compatibility.indexed_bucket_count) * 1024U),
110 0,
111 stream.get()>>>(
112 index_offsets_.empty() ? nullptr : index_offsets_.data(),
113 index_keys_.empty() ? nullptr : index_keys_.data(),
114 reference_count,
115 compatibility.indexed_bucket_count,
116 static_cast<uint32_t>(compatibility.key_mask) + 1U,
117 work.data()
118 );
119 CUDDL_CUDA_TRY(cudaGetLastError());
120 unsigned long long host = 0U;
121 CUDDL_CUDA_TRY(cuda::copy_bytes(stream, work, cuda::std::span{&host, size_t{1}}));
122 CUDDL_CUDA_TRY(stream.sync());
123 pair_work_ = host;
124 pair_fraction_ = static_cast<double>(pair_work_) /
125 (static_cast<double>(reference_count) * reference_count *
126 compatibility.indexed_bucket_count);
127 return Ok();
128 }
129
130 reference_index(database_type const& database, cuda::stream_ref stream)
131 : identity_(database.identity_),
132 index_offsets_(stream, cuda::device_default_memory_pool(stream.device())),
133 index_postings_(stream, cuda::device_default_memory_pool(stream.device())),
134 index_keys_(stream, cuda::device_default_memory_pool(stream.device())),
135 key_directory_(stream, cuda::device_default_memory_pool(stream.device())),
136 bitmap_cells_(stream, cuda::device_default_memory_pool(stream.device())),
137 posting_bitmaps_(stream, cuda::device_default_memory_pool(stream.device())) {}
138
139 template <typename Row>
140 [[nodiscard]] static Result<reference_index>
141 build_index(database_type const& source, cuda::stream_ref stream, index_storage storage) {
142 auto compatibility = source.metadata().compatibility;
143 // The database keeps only bit-planes; the index passes read decoded score rows, freed
144 // in stream order once the build's kernels have run.
145 auto decoded = CUDDL_CUDA_TRY(
146 cuda::make_device_buffer<Row>(
147 stream,
148 stream.device(),
149 static_cast<size_t>(source.reference_count()) * BucketCount,
150 cuda::no_init
151 )
152 );
153 CUDDL_TRY(source.copy_scores_async({decoded.data(), decoded.size()}, stream));
154 auto rows = device_span<Row const>{decoded.data(), decoded.size()};
156 storage = detail::smaller_index_storage(source.reference_count(), compatibility);
157 }
159 return Err(Error::invalid_argument("unsupported index storage"));
160 }
161 auto const validated = database_type::validate_rows(rows, compatibility);
162 if (!validated) {
163 return Err(validated.error());
164 }
165 auto const posting_capacity = detail::indexed_posting_count(*validated, compatibility);
166 if (posting_capacity > std::numeric_limits<uint32_t>::max()) {
167 return Err(Error::resource("index postings exceed 32-bit offsets"));
168 }
169 if (posting_capacity > std::numeric_limits<size_t>::max() / sizeof(uint32_t)) {
170 return Err(Error::resource("index posting allocation overflows"));
171 }
172 auto const cell_count = detail::indexed_cell_count(compatibility);
173 auto const offset_count = cell_count + 1U;
174 if (offset_count > std::numeric_limits<size_t>::max() / sizeof(uint32_t)) {
175 return Err(Error::resource("dense index offset allocation overflows"));
176 }
177
178 auto index = CUDDL_CUDA_TRY(reference_index(source, stream));
180 CUDDL_TRY(
181 index.template build_sparse_index<Row>(rows, compatibility, *validated, stream)
182 );
183 index.indexed_ = true;
184 return Result<reference_index>::ok(std::move(index));
185 }
186 index.index_offsets_ = CUDDL_CUDA_TRY(
187 cuda::make_device_buffer<uint32_t>(
188 stream, stream.device(), static_cast<size_t>(offset_count), cuda::no_init
189 )
190 );
191 if (rows.empty()) {
192 CUDDL_CUDA_TRY(cuda::fill_bytes(stream, index.index_offsets_, 0));
193 index.indexed_ = true;
194 return Result<reference_index>::ok(std::move(index));
195 }
196 index.index_postings_ = CUDDL_CUDA_TRY(
197 cuda::make_device_buffer<uint32_t>(
198 stream, stream.device(), static_cast<size_t>(posting_capacity), cuda::no_init
199 )
200 );
201 index.index_posting_capacity_ = static_cast<size_t>(posting_capacity);
202
203 auto const indexed_row_count = static_cast<size_t>(posting_capacity);
204 auto const reference_count = static_cast<uint32_t>(rows.size() / BucketCount);
205
206 // Bucket-major transpose of the indexed buckets: the per-bucket count and scatter
207 // passes read contiguous references from it instead of striding across rows, keeping
208 // each bucket's dense key range L2-resident for its atomics.
209 auto transposed = CUDDL_CUDA_TRY(
210 cuda::make_device_buffer<Row>(stream, stream.device(), indexed_row_count, cuda::no_init)
211 );
212 constexpr uint32_t transpose_tile = 32U;
213 dim3 const transpose_grid(
214 (compatibility.indexed_bucket_count + transpose_tile - 1U) / transpose_tile,
215 (reference_count + transpose_tile - 1U) / transpose_tile
216 );
217 detail::transpose_indexed_scores_kernel<<<
218 transpose_grid,
219 dim3(transpose_tile, transpose_tile),
220 0,
221 stream.get()>>>(
222 rows.data(),
224 compatibility.indexed_bucket_count,
225 static_cast<uint32_t>(BucketCount),
226 transposed.data()
227 );
228 CUDDL_CUDA_TRY(cudaGetLastError());
229
230 // A small wave-blocked grid sweeps the buckets in rounds so the in-flight buckets'
231 // dense key slices and cursor ranges stay L2-resident; one bucket per resident wave
232 // would leave the SMs idle, and one block per bucket thrashes the cache (the profiled
233 // failure mode of the first bucket-major version).
234 constexpr uint32_t build_wave_blocks = 64;
235 constexpr uint32_t build_bucket_block_size = 1024;
236 auto const bucket_blocks =
237 std::min<uint32_t>(compatibility.indexed_bucket_count, build_wave_blocks);
238 // Both bucket kernels keep a quarter-key table in dynamic shared memory (64 KiB for
239 // 16-bit keys, 32 KiB for 15-bit keys, over the default 48 KiB cap). The required size
240 // depends on this build's key width, so the limit is (re)configured before every launch.
241 auto const key_count = static_cast<uint32_t>(compatibility.key_mask) + 1U;
242 auto const bucket_smem_bytes = static_cast<size_t>(key_count / 4U) * sizeof(uint32_t);
243 CUDDL_CUDA_TRY(cudaFuncSetAttribute(
244 reinterpret_cast<void const*>(detail::count_index_cells_bucket_kernel<Row>),
245 cudaFuncAttributeMaxDynamicSharedMemorySize,
246 static_cast<int>(bucket_smem_bytes)
247 ));
248 CUDDL_CUDA_TRY(cudaFuncSetAttribute(
249 reinterpret_cast<void const*>(detail::scatter_index_postings_bucket_kernel<Row>),
250 cudaFuncAttributeMaxDynamicSharedMemorySize,
251 static_cast<int>(bucket_smem_bytes)
252 ));
253
254 // The count flush writes every cell of every bucket slice exactly once (plain stores
255 // from the owning block), so the offsets array needs no separate zeroing before the
256 // exclusive scan.
257 detail::count_index_cells_bucket_kernel<<<
258 bucket_blocks,
259 build_bucket_block_size,
260 bucket_smem_bytes,
261 stream.get()>>>(
262 transposed.data(),
263 compatibility.indexed_bucket_count,
265 compatibility.key_mask,
266 index.index_offsets_.data()
267 );
268 CUDDL_CUDA_TRY(cudaGetLastError());
269 // CUB allocates and releases scan scratch on this stream through its pooled resource.
271 cub::DeviceScan::ExclusiveSum(
272 index.index_offsets_.data(), static_cast<int64_t>(cell_count + 1U), stream
273 )
274 );
275
276 detail::scatter_index_postings_bucket_kernel<<<
277 bucket_blocks,
278 build_bucket_block_size,
279 bucket_smem_bytes,
280 stream.get()>>>(
281 transposed.data(),
282 compatibility.indexed_bucket_count,
284 compatibility.key_mask,
285 index.index_offsets_.data(),
286 index.index_postings_.data()
287 );
288 CUDDL_CUDA_TRY(cudaGetLastError());
289 // Postings ascend by reference ID within each cell, so a search can restrict a cell to a
290 // reference range with a binary search.
291 auto sorted = CUDDL_CUDA_TRY(
292 cuda::make_device_buffer<uint32_t>(
293 stream, stream.device(), static_cast<size_t>(posting_capacity), cuda::no_init
294 )
295 );
297 cub::DeviceSegmentedSort::SortKeys(
298 index.index_postings_.data(),
299 sorted.data(),
300 static_cast<int64_t>(posting_capacity),
301 static_cast<int64_t>(cell_count),
302 index.index_offsets_.data(),
303 index.index_offsets_.data() + 1,
304 stream
305 )
306 );
307 index.index_postings_ = std::move(sorted);
308 index.indexed_ = true;
309 return Result<reference_index>::ok(std::move(index));
310 }
311
312 template <typename Row>
313 [[nodiscard]] Result<void> build_sparse_index(
314 device_span<Row const> rows,
315 score_compatibility const& compatibility,
316 uint32_t reference_count,
317 cuda::stream_ref stream
318 ) {
319 auto const size =
320 static_cast<size_t>(detail::indexed_posting_count(reference_count, compatibility));
321 index_posting_capacity_ = size;
322 if (size == 0U) {
323 return Ok();
324 }
325 auto const device = stream.device();
326 index_keys_ =
327 CUDDL_CUDA_TRY(cuda::make_device_buffer<uint16_t>(stream, device, size, cuda::no_init));
328 index_postings_ =
329 CUDDL_CUDA_TRY(cuda::make_device_buffer<uint32_t>(stream, device, size, cuda::no_init));
330 auto keys =
331 CUDDL_CUDA_TRY(cuda::make_device_buffer<uint16_t>(stream, device, size, cuda::no_init));
332 auto ids =
333 CUDDL_CUDA_TRY(cuda::make_device_buffer<uint32_t>(stream, device, size, cuda::no_init));
334 auto transposed =
335 CUDDL_CUDA_TRY(cuda::make_device_buffer<Row>(stream, device, size, cuda::no_init));
336 detail::transpose_indexed_scores_kernel<<<
337 dim3((compatibility.indexed_bucket_count + 31U) / 32U, (reference_count + 31U) / 32U),
338 dim3(32U, 32U),
339 0,
340 stream.get()>>>(
341 rows.data(),
343 compatibility.indexed_bucket_count,
344 static_cast<uint32_t>(BucketCount),
345 transposed.data()
346 );
347 CUDDL_CUDA_TRY(cudaGetLastError());
348 auto const mask = compatibility.key_mask;
349 // Zero denotes an empty row. Folded 15-bit keys are shifted by one so that
350 // a nonempty score masked to zero remains distinguishable from an empty row.
352 cub::DeviceTransform::Transform(
353 transposed.data(), keys.data(), size, detail::sparse_index_key{mask}, stream
354 )
355 );
357 cub::DeviceTransform::Transform(
358 cuda::make_counting_iterator(uint32_t{0}),
359 ids.data(),
360 size,
361 detail::sparse_reference_id{reference_count},
362 stream
363 )
364 );
365 auto const segment_offsets = cuda::make_transform_iterator(
366 cuda::make_counting_iterator(uint32_t{0}),
367 detail::sparse_segment_offset{reference_count}
368 );
369 // Stable, so reference IDs, which arrive ascending per bucket, stay ascending per key.
371 cub::DeviceSegmentedSort::StableSortPairs(
372 keys.data(),
373 index_keys_.data(),
374 ids.data(),
375 index_postings_.data(),
376 static_cast<int64_t>(size),
377 static_cast<int64_t>(compatibility.indexed_bucket_count),
378 segment_offsets,
379 segment_offsets + 1,
380 stream
381 )
382 );
383 return build_key_directory(reference_count, compatibility.indexed_bucket_count, stream);
384 }
385
387 [[nodiscard]] Result<void> build_key_directory(
388 uint32_t reference_count,
389 uint32_t indexed_bucket_count,
390 cuda::stream_ref stream
391 ) {
392 auto const size = static_cast<size_t>(detail::sparse_directory_entries(reference_count)) *
393 indexed_bucket_count;
394 if (index_keys_.empty() || size == 0U) {
395 return Ok();
396 }
397 key_directory_ = CUDDL_CUDA_TRY(
398 cuda::make_device_buffer<uint32_t>(stream, stream.device(), size, cuda::no_init)
399 );
400 auto const blocks = static_cast<uint32_t>(
401 std::min<size_t>((size + detail::block_size - 1U) / detail::block_size, 65535U)
402 );
403 detail::build_sparse_directory_kernel<<<blocks, detail::block_size, 0, stream.get()>>>(
404 index_keys_.data(), reference_count, indexed_bucket_count, key_directory_.data()
405 );
406 return cuda_try(cudaGetLastError());
407 }
408
409 [[nodiscard]] Result<void>
410 build_posting_bitmaps(database_type const& database, cuda::stream_ref stream) {
411 auto const references = database.reference_count();
412 auto const buckets = database.metadata().compatibility.indexed_bucket_count;
413 if (references < 8192U || buckets == 0U || buckets > 65535U) {
414 return Ok();
415 }
416 auto const words = detail::candidate_bit_words(references);
417 bitmap_cells_ = CUDDL_CUDA_TRY(
418 cuda::make_device_buffer<uint32_t>(
419 stream, stream.device(), index_posting_capacity_ / words, cuda::no_init
420 )
421 );
422 auto selected = CUDDL_CUDA_TRY(
423 cuda::make_device_buffer<uint32_t>(stream, stream.device(), 1U, cuda::no_init)
424 );
425 auto const* offsets = index_offsets_.data();
426 auto const* keys = index_keys_.empty() ? nullptr : index_keys_.data();
427 auto const cells = keys == nullptr ? index_offsets_.size() - 1U : index_keys_.size();
429 cub::DeviceSelect::If(
430 cuda::make_counting_iterator(uint32_t{0}),
431 bitmap_cells_.data(),
432 selected.data(),
433 static_cast<int64_t>(cells),
434 detail::posting_bitmap_cell{offsets, keys, references, words},
435 stream
436 )
437 );
439 cuda::copy_bytes(stream, selected, cuda::std::span{&bitmap_count_, size_t{1}})
440 );
441 CUDDL_CUDA_TRY(stream.sync());
442 if (bitmap_count_ == 0U) return Ok();
443 posting_bitmaps_ = CUDDL_CUDA_TRY(
444 cuda::make_device_buffer<uint32_t>(
445 stream, stream.device(), static_cast<size_t>(bitmap_count_) * words, cuda::no_init
446 )
447 );
448 CUDDL_CUDA_TRY(cuda::fill_bytes(stream, posting_bitmaps_, 0));
449 detail::
450 build_posting_bitmaps_kernel<<<bitmap_count_, detail::block_size, 0, stream.get()>>>(
451 bitmap_cells_.data(),
452 offsets,
453 keys,
454 references,
455 static_cast<uint32_t>(database.metadata().compatibility.key_mask) + 1U,
456 index_postings_.data(),
457 words,
458 posting_bitmaps_.data()
459 );
460 return cuda_try(cudaGetLastError());
461 }
462
463 std::shared_ptr<char const> identity_;
464 cuda::device_buffer<uint32_t> index_offsets_;
465 cuda::device_buffer<uint32_t> index_postings_;
466 cuda::device_buffer<uint16_t> index_keys_;
467 // Radix directory over index_keys_ for batch range lookups; derived, not persisted.
468 cuda::device_buffer<uint32_t> key_directory_;
469 cuda::device_buffer<uint32_t> bitmap_cells_;
470 cuda::device_buffer<uint32_t> posting_bitmaps_;
471 uint32_t bitmap_count_{};
472 size_t index_posting_capacity_{};
473 // Raw pair-work sum behind pair_fraction_; persisted in index files so loads skip its scan.
474 unsigned long long pair_work_{};
475 double pair_fraction_{};
476 bool indexed_{};
477};
478
479} // namespace cuddl
Move-only owner of one immutable contiguous reference database.
Index-file persistence and the validation boundary for reference_index.
Move-only acceleration data for an immutable reference database.
double pair_fraction() const noexcept
Fraction of all (query, reference, bucket) cells an index lookup visits for queries resembling the re...
reference_index & operator=(reference_index &&other) noexcept
Move-assigns the index, leaving the source empty.
reference_index & operator=(reference_index const &)=delete
index_storage storage() const noexcept
Dense index when offsets cover every cell, sparse when only keys are stored.
size_t persistent_index_bytes() const noexcept
Bytes the saved index file needs for offsets, postings, and keys.
reference_index(reference_index &&other) noexcept
static Result< reference_index > build_async(database_type const &database, cuda::stream_ref stream, index_storage storage=index_storage::automatic)
Build acceleration data without taking ownership of or copying reference rows.
reference_index(reference_index const &)=delete
#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
__global__ uint32_t reference_count
Definition kernels.cuh:437
__global__ uint32_t uint32_t uint64_t * keys
auto const cells
Definition kernels.cuh:1878
__global__ uint32_t uint32_t indexed_bucket_count
Definition kernels.cuh:438
__global__ uint16_t const uint32_t uint32_t uint32_t key_count
Definition kernels.cuh:472
@ host
Any node: for buffers host threads fill.
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
constexpr uint64_t indexed_posting_count(uint32_t reference_count, score_compatibility const &compatibility) noexcept
auto const words
Definition top_k.cuh:204
::cuda::__ensure_current_context device_scope
Makes stream's device current for the enclosing scope and restores the previous context on exit.
__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
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
static __global__ uint32_t const * offsets
Definition kernels.cuh:536
__global__ size_t uint16_t * rows
Definition kernels.cuh:1499
Batch operations over contiguous device rows.
Definition a48.hpp:14
Result< void > Ok() noexcept
Success return for Result<void>; same as return {}.
Definition error.hpp:181
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
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