cuDDL
Loading...
Searching...
No Matches
database_staging.cuh
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm>
4#include <atomic>
5#include <chrono>
6#include <cstdint>
7#include <cstring>
8#include <deque>
9#include <filesystem>
10#include <future>
11#include <limits>
12#include <memory>
13#include <mutex>
14#include <optional>
15#include <span>
16#include <string_view>
17#include <thread>
18#include <utility>
19#include <vector>
20
21#include <unistd.h>
22
23#include <cuda/algorithm>
24#include <cuda/buffer>
25#include <cuda/devices>
26#include <cuda/memory_pool>
27#include <cuda/stream>
28
29#ifndef CUDDL_HAS_NVCOMP
30 #define CUDDL_HAS_NVCOMP 0
31#endif
32
33#if CUDDL_HAS_NVCOMP
35#endif
40#include <cuddl/device_span.cuh>
41#include <cuddl/error.hpp>
42
43namespace cuddl::detail {
44
46enum class decompression_backend : uint8_t {
49 hybrid,
51 cpu,
53 gpu,
57};
58
60enum class transfer_mode : uint8_t {
69 pinned,
73 staged,
74};
75
80[[nodiscard]] inline bool device_reads_pageable_memory(cuda::device_ref device) noexcept {
81 return device.attribute(cuda::device_attributes::pageable_memory_access) != 0 &&
82 device.attribute(
83 cuda::device_attributes::pageable_memory_access_uses_host_page_tables
84 ) != 0;
85}
86
90[[nodiscard]] inline bool stages_in_place(transfer_mode mode, cuda::device_ref device) noexcept {
91 return mode == transfer_mode::in_place ||
92 (mode == transfer_mode::automatic && device_reads_pageable_memory(device));
93}
94
99[[nodiscard]] inline bool pages_locked(transfer_mode mode, cuda::device_ref device) noexcept {
100 if (mode == transfer_mode::pinned) return true;
101 if (mode != transfer_mode::automatic) return false;
102 return !device_reads_pageable_memory(device);
103}
104
107 size_t group = 0; // genomes whose register rows stay resident at once
108 size_t staging = 0; // arena bytes
109 size_t max_piece = 0; // bytes one piece may occupy
110 size_t max_pieces = 0; // descriptors one batch may hold
111};
112
117inline constexpr size_t default_arena_ceiling = size_t{128} << 20;
118
121inline constexpr size_t stager_arenas = 4;
122
127[[nodiscard]] inline Result<size_t> available_device_bytes(cuda::stream_ref stream) {
128 size_t free_bytes = 0, device_bytes = 0;
129 CUDDL_CUDA_TRY(cudaMemGetInfo(&free_bytes, &device_bytes));
130 auto const& device_pool = cuda::device_default_memory_pool(stream.device());
131 auto const pool_reserved =
132 device_pool.attribute(cuda::memory_pool_attributes::reserved_mem_current);
133 auto const pool_used = device_pool.attribute(cuda::memory_pool_attributes::used_mem_current);
134 return free_bytes + pool_reserved - std::min(pool_reserved, pool_used);
135}
136
142template <uint32_t K, size_t BucketCount>
144 size_t references,
145 uint64_t staged_ceiling,
146 std::optional<size_t> staging_bytes,
147 cuda::stream_ref stream
148) {
149 size_t const available = CUDDL_TRY(available_device_bytes(stream));
150 size_t const usable = available - available / 10;
151 size_t const row_bytes = BucketCount * sizeof(uint32_t);
152 // Rows are small next to the input, so the whole collection stays resident whenever that
153 // costs at most a quarter of what is affordable; otherwise rows stream back one group at a
154 // time as each group completes.
155 size_t const group = std::min(references, std::max<size_t>(1, usable / (4 * row_bytes)));
156 // Four fifths of what is affordable goes to the arena, which leaves room for the row store,
157 // the descriptors and whatever the caller keeps on the device. On a coherent CPU/GPU system
158 // the free memory reported here is host memory, so a share of host memory bounds it as well.
159 auto const pages = ::sysconf(_SC_PHYS_PAGES);
160 auto const page = ::sysconf(_SC_PAGE_SIZE);
161 size_t const host_share = pages > 0 && page > 0
162 ? static_cast<size_t>(pages) * static_cast<size_t>(page) / 8
163 : std::numeric_limits<size_t>::max();
164 size_t const arena_ceiling = static_cast<size_t>(std::min(
165 {staged_ceiling,
166 static_cast<uint64_t>(usable - usable / 5),
167 static_cast<uint64_t>(host_share),
168 static_cast<uint64_t>(default_arena_ceiling)}
169 ));
170 size_t const row_store_bytes = group * row_bytes;
171 size_t const after_rows = usable > row_store_bytes ? usable - row_store_bytes : 0;
172 if (staging_bytes.has_value() && *staging_bytes < K + 1) {
173 return Err(Error::invalid_argument("an explicit staging arena must hold one window"));
174 }
175 // Every arena gets the same share: one fills while kernels read the others.
176 size_t const staging = staging_bytes.has_value()
177 ? *staging_bytes
178 : std::min(after_rows / stager_arenas, arena_ceiling);
179 // One piece of at least k bases plus its k - 1 byte overlap has to fit.
180 if (staging < K + 1 || staging > after_rows / stager_arenas) {
181 return Err(
183 "insufficient free device memory for reference staging (free " +
184 std::to_string(available >> 20) + " MiB)"
185 )
186 );
187 }
188 // One descriptor per staged piece; the cap keeps a corpus of very short records from
189 // demanding a descriptor per record.
190 size_t const max_pieces = std::min<size_t>(size_t{1} << 20, std::max<size_t>(64, staging / K));
191 // Pieces are a quarter of the arena at most, so a batch always holds several, and short
192 // enough that a piece's window count stays representable.
193 size_t const max_piece = std::min(
194 {std::max<size_t>(K, staging / 4),
195 static_cast<size_t>(std::numeric_limits<uint32_t>::max()) - K + 1}
196 );
197 return staging_plan{group, staging, max_piece, max_pieces};
198}
199
204inline constexpr size_t stager_hold_slots = 16;
205
208inline constexpr size_t packed_buffer_bytes = size_t{2} << 20;
209
221template <uint32_t K, size_t BucketCount, typename Layout = default_register_layout>
223 struct resident_event_span {
224 cudaEvent_t start = nullptr;
225 cudaEvent_t stop = nullptr;
226
227 resident_event_span() = default;
228 resident_event_span(resident_event_span const&) = delete;
229 resident_event_span& operator=(resident_event_span const&) = delete;
230 resident_event_span(resident_event_span&& other) noexcept
231 : start(std::exchange(other.start, nullptr)),
232 stop(std::exchange(other.stop, nullptr)) {}
233 resident_event_span& operator=(resident_event_span&& other) noexcept {
234 if (this == &other) return *this;
235 if (start != nullptr) CUDDL_CUDA_ABORT(cudaEventDestroy(start));
236 if (stop != nullptr) CUDDL_CUDA_ABORT(cudaEventDestroy(stop));
237 start = std::exchange(other.start, nullptr);
238 stop = std::exchange(other.stop, nullptr);
239 return *this;
240 }
241 ~resident_event_span() {
242 if (start != nullptr) CUDDL_CUDA_ABORT(cudaEventDestroy(start));
243 if (stop != nullptr) CUDDL_CUDA_ABORT(cudaEventDestroy(stop));
244 }
245 };
246
247 public:
248 static constexpr size_t hold_slots = stager_hold_slots;
249
257 cuda::stream_ref stream,
258 staging_plan bounds,
259 reference_build_statistics* statistics = nullptr,
260 bool direct = false,
261 cuda::std::optional<blacklist_view> blacklist = cuda::std::nullopt
262 )
263 : stream_(stream),
264 bounds_(bounds),
265 statistics_(statistics),
266 direct_(direct),
267 blacklist_(blacklist),
268 rows_(
269 cuda::make_device_buffer<uint32_t>(
270 stream,
271 stream.device(),
272 bounds.group * BucketCount,
273 cuda::no_init
274 )
275 ),
276 copy_stream_(stream.device()),
277 kernel_stream_(stream.device(), greatest_stream_priority()),
278 // The copy stream fills one arena while kernels read the others.
279 arena_(
280 cuda::make_device_buffer<char>(
281 stream,
282 stream.device(),
283 direct ? bounds.staging : stager_arenas * arena_stride(bounds.staging),
284 cuda::no_init
285 )
286 ),
287 descriptors_(
288 cuda::make_device_buffer<sequence_batch_chunk>(
289 stream,
290 stream.device(),
291 bounds.max_pieces,
292 cuda::no_init
293 )
294 ),
295 held_(hold_slots),
296 sm_(static_cast<size_t>(
297 stream.device().attribute(cuda::device_attributes::multiprocessor_count)
298 )),
299 max_grid_(
300 static_cast<size_t>(
301 stream.device().attribute(cuda::device_attributes::max_grid_dim_x)
302 )
303 ) {
304 held_consumed_.reserve(hold_slots);
305 for (size_t slot = 0; slot < hold_slots; ++slot) {
306 held_consumed_.emplace_back(stream);
307 }
308 staged_chunks_.reserve(std::min<size_t>(bounds_.max_pieces, size_t{1} << 16));
309 copied_chunks_.reserve(staged_chunks_.capacity());
310 // The arenas are allocated in order on the main stream.
311 copy_stream_.wait(stream_);
312 }
315 CUDDL_CUDA_ABORT(cudaEventRecord(copies_done_.get(), copy_stream_.get()));
316 CUDDL_CUDA_ABORT(cudaStreamWaitEvent(stream_.get(), copies_done_.get()));
317 }
320
327 size_t genome,
328 std::span<fastx_sequence_extent const> records,
329 char const* pinned_base = nullptr,
330 size_t pinned_size = 0,
331 std::unique_ptr<fastx_sequence_file> holder = {}
332 ) {
333 auto const record_size = [&](size_t index) {
334 return static_cast<size_t>(records[index].end - records[index].begin);
335 };
336 size_t record = 0;
337 while (record < records.size()) {
338 // Staged in place, the arena is not the limit; the descriptors and the piece size are.
339 auto const room =
340 direct_ ? std::numeric_limits<size_t>::max() : bounds_.staging - arena_used_;
341 size_t run = record;
342 while (run < records.size()) {
343 auto const run_bytes =
344 static_cast<size_t>(records[run].end - records[record].begin);
345 if (run_bytes > room || record_size(run) > bounds_.max_piece ||
346 staged_chunks_.size() + (run - record) >= bounds_.max_pieces) {
347 break;
348 }
349 ++run;
350 }
351 if (run == record) {
352 if (arena_used_ != 0) {
353 CUDDL_TRY(flush());
354 continue;
355 }
356 // One record larger than the whole arena: split it, and give each piece the
357 // k - 1 bases of overlap its kmers need.
358 auto const size = record_size(record);
359 size_t offset = 0;
360 while (offset < size) {
361 auto const overlap = offset == 0 ? size_t{0} : std::min(offset, size_t{K - 1});
362 auto const bases = std::min(size - offset, bounds_.max_piece);
363 auto const span = overlap + bases;
364 if (span < K) break;
365 if (arena_used_ + span > bounds_.staging ||
366 staged_chunks_.size() >= bounds_.max_pieces) {
367 CUDDL_TRY(flush());
368 }
369 auto const* const source = records[record].begin + offset - overlap;
370 CUDDL_TRY(stage(
371 source,
372 span,
373 genome,
374 static_cast<uint32_t>(span - K + 1),
375 pinned_base,
376 pinned_size
377 ));
378 offset += bases;
379 }
380 ++record;
381 continue;
382 }
383 // Copy the run in one transfer, then describe each record inside it. Staged in
384 // place, the records keep their own addresses and there is nothing to copy.
385 auto const* const source = records[record].begin;
386 auto const run_bytes = static_cast<size_t>(records[run - 1].end - source);
387 account(source, run_bytes, pinned_base, pinned_size);
388 if (!direct_) {
390 cuda::copy_bytes(
391 copy_stream_,
392 cuda::std::span{source, run_bytes},
393 device_span<char>{arena() + arena_used_, run_bytes}
394 )
395 );
396 }
397 for (size_t index = record; index < run; ++index) {
398 auto const size = record_size(index);
399 if (size < K) continue; // no window to record
400 auto const* const bases =
401 direct_ ? records[index].begin
402 : arena() + arena_used_ +
403 static_cast<size_t>(records[index].begin - source);
404 describe(bases, static_cast<uint32_t>(size - K + 1), genome);
405 }
406 if (direct_) {
407 // Staged in place the arena never fills, so the bytes held for the device are
408 // what bounds a batch. Half the arena, because a batch is still held while the
409 // next one fills: the two together then cost what the copies would have.
410 held_bytes_ += run_bytes;
411 if (held_bytes_ >= bounds_.staging / 2) {
412 CUDDL_TRY(flush());
413 }
414 } else {
415 arena_used_ += run_bytes;
416 ++transfers_;
417 }
418 record = run;
419 }
420 if (holder) hold(std::move(holder));
421 return Ok();
422 }
423
429 size_t genome,
430 std::unique_ptr<fastx_sequence_file> holder,
431 char const* pinned_base = nullptr,
432 size_t pinned_size = 0
433 ) {
434 constexpr size_t word_bases = 16;
435 auto const& file = *holder;
436 auto const& runs = file.packed;
437 auto const words = [&](size_t index) {
438 return (runs[index].bases + word_bases - 1) / word_bases;
439 };
440 size_t run = 0;
441 while (run < runs.size()) {
442 auto const room = direct_ ? std::numeric_limits<size_t>::max() : arena_room_words();
443 size_t last = run;
444 while (last < runs.size()) {
445 auto const span_words = runs[last].word + words(last) - runs[run].word;
446 if (span_words > room || words(last) * sizeof(uint32_t) > bounds_.max_piece ||
447 staged_chunks_.size() + (last - run) >= bounds_.max_pieces) {
448 break;
449 }
450 ++last;
451 }
452 if (last == run) {
453 if (arena_used_ != 0 || !staged_chunks_.empty()) {
454 CUDDL_TRY(flush());
455 continue;
456 }
457 auto const halo = (size_t{K} - 1 + word_bases - 1) / word_bases;
458 auto const capacity =
459 std::min(bounds_.max_piece, bounds_.staging) / sizeof(uint32_t);
460 if (capacity <= halo) {
461 return Err(Error::resource("staging arena too small for a packed run"));
462 }
463 auto const step = (capacity - halo) * word_bases;
464 auto const total = runs[run].bases - K + 1;
465 for (size_t start = 0; start < total; start += step) {
466 auto const windows = std::min(step, total - start);
467 auto const first = runs[run].word + start / word_bases;
468 auto const count = (start + windows + K - 1 + word_bases - 1) / word_bases -
469 start / word_bases;
470 if ((!direct_ && count > arena_room_words()) ||
471 staged_chunks_.size() >= bounds_.max_pieces) {
472 CUDDL_TRY(flush());
473 }
474 auto const* const bases =
475 CUDDL_TRY(place_words(file.words + first, count, pinned_base, pinned_size));
476 describe(bases, static_cast<uint32_t>(windows), genome, true);
477 }
478 ++run;
479 continue;
480 }
481 auto const first = runs[run].word;
482 auto const count = runs[last - 1].word + words(last - 1) - first;
483 auto const* const base =
484 CUDDL_TRY(place_words(file.words + first, count, pinned_base, pinned_size));
485 for (size_t index = run; index < last; ++index) {
486 describe(
487 base + (runs[index].word - first) * sizeof(uint32_t),
488 static_cast<uint32_t>(runs[index].bases - K + 1),
489 genome,
490 true
491 );
492 }
493 if (direct_ && held_bytes_ >= bounds_.staging / 2) CUDDL_TRY(flush());
494 run = last;
495 }
496 hold(std::move(holder));
497 return Ok();
498 }
499
504 [[nodiscard]] Result<void> add_resident(size_t genome, char const* bases, size_t size) {
505 if (size < K) return Ok();
506 if (staged_chunks_.size() >= bounds_.max_pieces) CUDDL_TRY(flush());
507 describe(bases, static_cast<uint32_t>(size - K + 1), genome);
508 return Ok();
509 }
510
512 [[nodiscard]] Result<void> begin_group(size_t genomes) {
513 auto const resident = CUDDL_TRY(begin_resident(stream_));
515 cuda::fill_bytes(
516 stream_, cuda::std::span{rows_.data(), genomes * BucketCount}, uint32_t{0}
517 )
518 );
519 CUDDL_TRY(end_resident(resident, stream_));
520 return Ok();
521 }
522
529 [[nodiscard]] Result<device_span<uint32_t const>> end_group(size_t genomes) {
530 CUDDL_TRY(flush());
531 return device_span<uint32_t const>{rows_.data(), genomes * BucketCount};
532 }
533
535 [[nodiscard]] Result<void> finish() {
536 CUDDL_CUDA_TRY(stream_.sync());
537 CUDDL_CUDA_TRY(copy_stream_.sync());
538 if (statistics_ != nullptr) {
539 statistics_->staging_bytes = bounds_.staging;
540 statistics_->batches = batches_;
541 statistics_->transfers = transfers_;
542 for (auto const& span : resident_spans_) {
543 float elapsed_ms = 0;
544 CUDDL_CUDA_TRY(cudaEventElapsedTime(&elapsed_ms, span.start, span.stop));
545 statistics_->resident_compute_ms += elapsed_ms;
546 }
547 }
548 return Ok();
549 }
550
551 private:
553 [[nodiscard]] static constexpr size_t arena_stride(size_t staging) noexcept {
554 return (staging + 15) & ~size_t{15};
555 }
556
557 [[nodiscard]] static int greatest_stream_priority() {
558 int least = 0, greatest = 0;
559 auto const status = cudaDeviceGetStreamPriorityRange(&least, &greatest);
560 if (status != cudaSuccess) throw std::runtime_error(cudaGetErrorString(status));
561 return greatest;
562 }
563
564 [[nodiscard]] Result<size_t> begin_resident(cuda::stream_ref stream) {
565 if (statistics_ == nullptr || !statistics_->measure_resident) {
566 return std::numeric_limits<size_t>::max();
567 }
568 resident_event_span span;
569 CUDDL_CUDA_TRY(cudaEventCreate(&span.start));
570 CUDDL_CUDA_TRY(cudaEventCreate(&span.stop));
571 CUDDL_CUDA_TRY(cudaEventRecord(span.start, stream.get()));
572 resident_spans_.push_back(std::move(span));
573 return resident_spans_.size() - 1;
574 }
575
576 [[nodiscard]] Result<void> end_resident(size_t index, cuda::stream_ref stream) {
577 if (index == std::numeric_limits<size_t>::max()) return Ok();
578 CUDDL_CUDA_TRY(cudaEventRecord(resident_spans_[index].stop, stream.get()));
579 return Ok();
580 }
581
583 void describe(char const* bases, uint32_t windows, size_t genome, bool packed = false) {
584 auto const blocks = std::min(sm_ * 2, (static_cast<size_t>(windows) + 2047) / 2048);
585 block_end_ += blocks;
586 staged_chunks_.push_back(
587 {bases, block_end_, static_cast<uint32_t>(genome), windows, packed ? 1U : 0U}
588 );
589 }
590
592 [[nodiscard]] size_t arena_room_words() const noexcept {
593 auto const aligned = (arena_used_ + 3) & ~size_t{3};
594 return aligned < bounds_.staging ? (bounds_.staging - aligned) / sizeof(uint32_t) : 0;
595 }
596
598 [[nodiscard]] Result<char const*>
599 place_words(uint32_t const* words, size_t count, char const* pinned_base, size_t pinned_size) {
600 auto const* const source = reinterpret_cast<char const*>(words);
601 auto const bytes = count * sizeof(uint32_t);
602 account(source, bytes, pinned_base, pinned_size);
603 if (direct_) {
604 held_bytes_ += bytes;
605 return source;
606 }
607 arena_used_ = (arena_used_ + 3) & ~size_t{3};
608 auto* const target = arena() + arena_used_;
610 cuda::copy_bytes(
611 copy_stream_, cuda::std::span{source, bytes}, device_span<char>{target, bytes}
612 )
613 );
614 arena_used_ += bytes;
615 ++transfers_;
616 return static_cast<char const*>(target);
617 }
618
619 void account(char const* source, size_t size, char const* pinned_base, size_t pinned_size) {
620 if (statistics_ == nullptr) return;
621 auto const direct = pinned_base != nullptr && source >= pinned_base &&
622 source + size <= pinned_base + pinned_size;
623 if (direct) {
624 statistics_->direct_bytes += size;
625 ++statistics_->direct_chunks;
626 } else {
627 statistics_->staged_bytes += size;
628 ++statistics_->staged_chunks;
629 }
630 }
631
636 [[nodiscard]] Result<void> stage(
637 char const* source,
638 size_t size,
639 size_t genome,
640 uint32_t windows,
641 char const* pinned_base,
642 size_t pinned_size
643 ) {
644 account(source, size, pinned_base, pinned_size);
645 if (direct_) {
646 // The device reads the caller's bytes where they are, so nothing is copied and the
647 // arena stays empty; the hold keeps those bytes alive until the batch has run.
648 describe(source, windows, genome);
649 return Ok();
650 }
652 cuda::copy_bytes(
653 copy_stream_,
654 cuda::std::span{source, size},
655 device_span<char>{arena() + arena_used_, size}
656 )
657 );
658 describe(arena() + arena_used_, windows, genome);
659 arena_used_ += size;
660 ++transfers_;
661 return Ok();
662 }
663
664 public:
669 [[nodiscard]] Result<void> flush() {
670 if (staged_chunks_.empty()) {
671 // Records shorter than k stage bytes but record no window, so a flush can arrive
672 // with nothing to launch. The offsets still have to reset, or a caller that flushed
673 // to make room would ask again for the same room.
674 arena_used_ = 0;
675 block_end_ = 0;
676 return Ok();
677 }
678 copied_consumed_.sync();
679 copied_chunks_.swap(staged_chunks_);
680 // The sketch kernel runs ahead of lower-priority work sharing the device, such as
681 // nvCOMP inflation: the copies that feed the next batch wait on it.
682 queued_.record(stream_);
683 kernel_stream_.wait(queued_);
684 if (!direct_) {
685 copies_done_.record(copy_stream_);
686 kernel_stream_.wait(copies_done_);
687 }
689 cuda::copy_bytes(
690 kernel_stream_,
691 cuda::std::span{copied_chunks_.data(), copied_chunks_.size()},
692 device_span<sequence_batch_chunk>{descriptors_.data(), copied_chunks_.size()}
693 )
694 );
695 copied_consumed_.record(kernel_stream_);
696 auto const grid = std::min(block_end_, max_grid_);
697 if (grid != 0) {
698 auto const resident = CUDDL_TRY(begin_resident(kernel_stream_));
699 // The kernel keeps the default carveout: a 50% L1 split sped up filtered sketching on
700 // an RTX 5090, but made `gpu`-backend builds 2.6 times slower beside nvCOMP's kernels.
701 auto launch = [&]<bool Filter, bool Naive>() {
702 add_sequence_batch_kernel<BucketCount, Layout, Filter, Naive>
703 <<<static_cast<uint32_t>(grid), 256, 0, kernel_stream_.get()>>>(
704 descriptors_.data(),
705 copied_chunks_.size(),
706 block_end_,
707 K,
708 rows_.data(),
709 blacklist_.value_or(blacklist_view{})
710 );
711 };
712 auto const encode = [&]<bool Naive>() {
713 if (blacklist_) {
714 launch.template operator()<true, Naive>();
715 } else {
716 launch.template operator()<false, Naive>();
717 }
718 };
719 // Only ablation builds compile the naive encoder.
720 if constexpr (ablations_enabled) {
721 if (ablated("kmer-encoding")) {
722 encode.template operator()<true>();
723 } else {
724 encode.template operator()<false>();
725 }
726 } else {
727 encode.template operator()<false>();
728 }
729 CUDDL_CUDA_TRY(cudaGetLastError());
730 CUDDL_TRY(end_resident(resident, kernel_stream_));
731 }
732 kernel_done_.record(kernel_stream_);
733 stream_.wait(kernel_done_);
734 if (direct_ && !batch_files_.empty()) {
735 // The launch above is what reads those buffers, so they wait on this batch's event.
736 released_.push_back({cuda::event{stream_}, std::move(batch_files_)});
737 batch_files_.clear();
738 released_.back().first.record(stream_);
739 while (released_.size() > held_batches) {
740 released_.front().first.sync();
741 released_.pop_front();
742 }
743 }
744 if (!direct_) {
745 // The next batch fills the other arena once the kernel that last read it is done.
746 arena_done_[arena_index_].record(stream_);
747 arena_index_ = (arena_index_ + 1) % stager_arenas;
748 copy_stream_.wait(arena_done_[arena_index_]);
749 }
750 ++batches_;
751 staged_chunks_.clear();
752 arena_used_ = 0;
753 held_bytes_ = 0;
754 block_end_ = 0;
755 return Ok();
756 }
757
763 if (direct_) return;
764 auto const slot = held_count_ % held_.size();
765 if (held_[slot]) held_consumed_[slot].sync();
766 }
767
768 private:
769 [[nodiscard]] char* arena() noexcept {
770 return arena_.data() + arena_index_ * arena_stride(bounds_.staging);
771 }
772
779 void hold(std::unique_ptr<fastx_sequence_file> holder) {
780 if (direct_) {
781 batch_files_.push_back(std::move(holder));
782 return;
783 }
784 auto const slot = held_count_++ % held_.size();
785 if (held_[slot]) {
786 held_consumed_[slot].sync();
787 held_[slot].reset();
788 }
789 held_consumed_[slot].record(copy_stream_);
790 held_[slot] = std::move(holder);
791 }
792
793 cuda::stream_ref stream_;
794 staging_plan bounds_;
795 reference_build_statistics* statistics_;
796 bool direct_ = false;
797 cuda::std::optional<blacklist_view> blacklist_;
798 cuda::device_buffer<uint32_t> rows_;
799 cuda::stream copy_stream_;
800 cuda::stream kernel_stream_;
801 cuda::device_buffer<char> arena_;
802 cuda::event copies_done_{stream_};
803 cuda::event queued_{stream_};
804 cuda::event kernel_done_{stream_};
805 std::array<cuda::event, stager_arenas> arena_done_ =
806 [&]<size_t... I>(std::index_sequence<I...>) {
807 return std::array<cuda::event, stager_arenas>{((void)I, cuda::event{stream_})...};
808 }(std::make_index_sequence<stager_arenas>{});
809 size_t arena_index_ = 0;
810 cuda::device_buffer<sequence_batch_chunk> descriptors_;
812 static constexpr size_t held_batches = 1;
813 std::deque<std::pair<cuda::event, std::vector<std::unique_ptr<fastx_sequence_file>>>> released_;
814 std::vector<std::unique_ptr<fastx_sequence_file>> batch_files_;
815 std::vector<std::optional<std::unique_ptr<fastx_sequence_file>>> held_;
816 std::vector<cuda::event> held_consumed_;
817 cuda::event copied_consumed_{stream_};
818 std::vector<sequence_batch_chunk> staged_chunks_, copied_chunks_;
819 std::vector<resident_event_span> resident_spans_;
820 size_t const sm_, max_grid_;
821 size_t arena_used_ = 0, block_end_ = 0, batches_ = 0, transfers_ = 0, held_count_ = 0;
822 size_t held_bytes_ = 0;
823};
824
830 std::string_view bases;
831};
832
835 std::span<sequence_record const> records;
836 std::string_view name; // copied into the labels; may be empty
837};
838
843template <size_t BucketCount>
846 std::span<uint16_t> scores,
847 cuda::stream_ref stream
848) {
849 auto const genomes = scores.size() / BucketCount;
850 if (genomes == 0) return Ok();
851 auto device_scores =
852 cuda::make_device_buffer<uint16_t>(stream, stream.device(), scores.size(), cuda::no_init);
853 batch_scores_kernel<BucketCount>
854 <<<static_cast<uint32_t>(genomes), block_size, 0, stream.get()>>>(
855 store.data(), static_cast<uint32_t>(genomes), device_scores.data()
856 );
857 CUDDL_CUDA_TRY(cudaGetLastError());
858 CUDDL_CUDA_TRY(cuda::copy_bytes(stream, device_scores, scores));
859 return Ok();
860}
861
862// Reusable sequence storage handed to loader threads. A lease returns to the pool when the
863// parsed file holding it dies. Copies are complete by then: the stager drops a held file only
864// after its copy event, and the device normaliser's lanes after their batch finished. Kernels
865// that read a buffer in place may still be queued, so then releasing records that point on the
866// stream, and the next loader to take the slot waits for it there.
868 struct slot;
869
870 public:
876 cuda::stream_ref stream,
877 size_t buffers,
878 size_t limit,
879 bool page_locked,
880 bool in_place
881 )
882 : stream_(stream),
883 limit_(limit),
884 pinned_pool_(
885 page_locked
886 ? std::optional{device_pinned_pool(stream.device(), pinned_placement::host)}
887 : std::nullopt
888 ),
889 in_place_(in_place) {
890 slots_.reserve(buffers);
891 }
892
900 void set_capacity(size_t buffers, size_t reserve_each = 0) {
901 capacity_ = std::max<size_t>(1, buffers);
902 if (pinned_pool_ && reserve_each != 0 && !ablated("pinned-reservation")) {
903 cuda::buffer<char, cuda::mr::host_accessible, cuda::mr::device_accessible> reserved(
904 stream_, *pinned_pool_, capacity_ * reserve_each, cuda::no_init
905 );
906 }
907 }
908
910 [[nodiscard]] size_t buffers() const noexcept {
911 return slots_.size();
912 }
913
915 [[nodiscard]] decompression_target acquire(size_t bytes) {
916 if (bytes == 0 || bytes > limit_) return {};
917 slot* owner = nullptr;
918 {
919 std::lock_guard lock(mutex_);
920 owner = claim();
921 if (owner == nullptr) return {};
922 }
923 // Wait on the loader thread rather than the build loop: the slot is already reserved,
924 // so this blocks only the genome that needs it. A failing wait throws out of the loader,
925 // which reports it through the pool's error slot like any other load failure.
926 if (in_place_) owner->consumed.sync();
927 if (owner->size < bytes) {
928 auto grown = size_t{1} << 16;
929 while (grown < bytes) {
930 grown *= 2;
931 }
932 owner->resize(stream_, grown, pinned_pool_);
933 }
934 return lease(owner);
935 }
936
937 private:
943 [[nodiscard]] slot* claim() {
944 if (free_head_ != nullptr) {
945 auto* owner = free_head_;
946 free_head_ = owner->next_free;
947 return owner;
948 }
949 if (slots_.size() >= capacity_) return nullptr;
950 slots_.push_back(std::make_unique<slot>(stream_));
951 return slots_.back().get();
952 }
953
954 private:
955 struct slot {
956 std::optional<cuda::buffer<char, cuda::mr::host_accessible, cuda::mr::device_accessible>>
957 pinned;
958 std::unique_ptr<char[]> heap;
959 size_t size = 0;
960 cuda::event consumed;
961 slot* next_free = nullptr;
962
963 explicit slot(cuda::stream_ref stream) : consumed(stream) {}
964
965 void resize(
966 cuda::stream_ref stream,
967 size_t bytes,
968 std::optional<cuda::pinned_memory_pool_ref> const& pinned_pool
969 ) {
970 if (pinned_pool) {
971 pinned.emplace(stream, *pinned_pool, bytes, cuda::no_init);
972 } else {
973 heap = std::make_unique_for_overwrite<char[]>(bytes);
974 }
975 size = bytes;
976 }
977
978 char* data() {
979 return heap ? heap.get() : pinned->data();
980 }
981 };
982
983 [[nodiscard]] decompression_target lease(slot* owner) {
984 auto* pool = this;
985 return {owner->data(), owner->size, std::shared_ptr<void>(owner, [pool, owner](void*) {
986 std::lock_guard lock(pool->mutex_);
987 // Record before publishing the slot: a loader that takes it must see the
988 // event of every kernel the previous lease fed.
989 if (pool->in_place_) owner->consumed.record(pool->stream_);
990 owner->next_free = nullptr;
991 if (pool->free_head_ == nullptr) {
992 pool->free_head_ = owner;
993 } else {
994 pool->free_tail_->next_free = owner;
995 }
996 pool->free_tail_ = owner;
997 })};
998 }
999
1000 cuda::stream_ref stream_;
1001 size_t limit_;
1002 std::optional<cuda::pinned_memory_pool_ref> pinned_pool_;
1003 bool in_place_;
1004 size_t capacity_{1};
1005 std::vector<std::unique_ptr<slot>> slots_;
1006 slot* free_head_ = nullptr;
1007 slot* free_tail_ = nullptr;
1008 std::mutex mutex_;
1009};
1010
1012[[nodiscard]] inline decompression_target acquire_sequence_target(void* context, size_t bytes) {
1013 return static_cast<sequence_buffer_pool*>(context)->acquire(bytes);
1014}
1015
1023 public:
1033 cuda::stream_ref stream,
1034 std::span<std::filesystem::path const> paths,
1035 unsigned parser_workers,
1036 bool page_locked,
1037 bool in_place,
1038 uint32_t pack_k,
1039 bool direct_reads
1040 )
1041 : workers_(worker_count(paths.size(), parser_workers)),
1042 buffers_(stream, workers_, size_t{32} << 20, page_locked, in_place),
1043 page_locked_(page_locked),
1044 pack_k_(pack_k) {
1045 // One buffer per in-flight file plus the stager's hold ring, plus headroom: a worker
1046 // asks for its next file while every loaded file still holds a lease. A pool smaller
1047 // than the window makes the remaining files allocate their own buffers.
1048 auto const window = workers_ * 4;
1049 buffers_.set_capacity(
1050 window + stager_hold_slots + 2, pack_k != 0 ? packed_buffer_bytes : 0
1051 );
1052 source_ = {acquire_sequence_target, &buffers_, direct_reads};
1053 if (workers_ > 1) pool_.emplace(paths, workers_, source_, window, false, false, pack_k);
1054 }
1055
1059 [[nodiscard]] size_t workers() const noexcept {
1060 return workers_;
1061 }
1062
1064 [[nodiscard]] size_t page_locked_buffers() const noexcept {
1065 return page_locked_ ? buffers_.buffers() : 0;
1066 }
1067
1070 take(std::span<std::filesystem::path const> paths, size_t id) {
1071 if (pool_.has_value()) return pool_->take(id);
1072 auto loaded = load_fastx_sequence_file(paths[id].string(), source_, false, false, pack_k_);
1073 if (pack_k_ != 0 && loaded) pack_fastx_sequence_file(**loaded, pack_k_);
1074 return loaded;
1075 }
1076
1078 void reset() noexcept {
1079 pool_.reset();
1080 }
1081
1084 static size_t worker_count(size_t paths, unsigned parser_workers) noexcept {
1085 auto const hardware = std::max(1U, std::thread::hardware_concurrency());
1086 auto const loaders = static_cast<size_t>(std::min(parser_workers, hardware));
1087 return std::max<size_t>(1, std::min(paths, loaders));
1088 }
1089
1090 private:
1091 size_t workers_ = 1;
1092 sequence_buffer_pool buffers_;
1093 bool page_locked_ = false;
1094 uint32_t pack_k_ = 0;
1095 decompression_source source_{};
1096 std::optional<fastx_load_pool> pool_;
1097};
1098
1105template <
1106 uint32_t K,
1107 size_t BucketCount,
1109 typename Fill,
1110 typename Sink>
1112 size_t genomes,
1113 uint64_t staged_ceiling,
1114 std::optional<size_t> staging_bytes,
1115 cuda::stream_ref stream,
1116 reference_build_statistics* statistics,
1117 Fill&& fill,
1118 Sink&& sink,
1119 bool in_place = false,
1120 cuda::std::optional<blacklist_view> blacklist = cuda::std::nullopt
1121) {
1122 auto const plan =
1123 CUDDL_TRY((plan_staging<K, BucketCount>(genomes, staged_ceiling, staging_bytes, stream)));
1124 database_stager<K, BucketCount, Layout> stager(stream, plan, statistics, in_place, blacklist);
1125 size_t base = 0;
1126 while (base < genomes) {
1127 auto const count = std::min(plan.group, genomes - base);
1128 CUDDL_TRY(stager.begin_group(count));
1129 CUDDL_TRY(fill(stager, base, count));
1130 auto const rows = CUDDL_TRY(stager.end_group(count));
1131 CUDDL_TRY(sink(rows, base, count));
1132 base += count;
1133 }
1134 return stager.finish();
1135}
1136
1137#if CUDDL_HAS_NVCOMP
1140template <uint32_t K, size_t BucketCount, typename Layout>
1141[[nodiscard]] Result<void> stage_host_loaded(
1142 database_stager<K, BucketCount, Layout>& stager,
1143 std::span<std::filesystem::path const> paths,
1144 std::span<size_t const> ids,
1145 size_t base,
1146 size_t workers,
1147 sequence_buffer_pool& buffers,
1148 bool page_locked,
1149 bool direct_reads
1150) {
1151 if (ids.empty()) return Ok();
1152 workers = std::min(workers, ids.size());
1153 std::vector<std::filesystem::path> subset;
1154 subset.reserve(ids.size());
1155 for (auto const id : ids) subset.push_back(paths[id]);
1156 fastx_load_pool pool(
1157 subset, workers, {acquire_sequence_target, &buffers, direct_reads}, workers * 4
1158 );
1159 for (size_t index = 0; index < ids.size(); ++index) {
1160 auto sequence = CUDDL_TRY(pool.take(index));
1161 auto const& extents = sequence->extents;
1162 auto const* pinned_base = page_locked ? sequence->decompressed_target : nullptr;
1163 auto const pinned_size = page_locked ? sequence->decompressed_size : 0;
1164 CUDDL_TRY(stager.add_genome(
1165 ids[index] - base, extents, pinned_base, pinned_size, std::move(sequence)
1166 ));
1167 }
1168 return Ok();
1169}
1170
1172template <uint32_t K, size_t BucketCount, typename Layout, typename Sink>
1173[[nodiscard]] Result<void> stage_paths_normalising(
1174 std::span<std::filesystem::path const> paths,
1175 cuda::stream_ref stream,
1176 std::optional<size_t> staging_bytes,
1177 unsigned parser_workers,
1178 transfer_mode transfer,
1179 bool direct_reads,
1180 reference_build_statistics* statistics,
1181 Sink&& sink,
1182 cuda::std::optional<blacklist_view> blacklist = cuda::std::nullopt
1183) {
1184 auto const workers = path_loaders::worker_count(paths.size(), parser_workers);
1185 auto const in_place = stages_in_place(transfer, stream.device());
1186 auto const page_locked = pages_locked(transfer, stream.device());
1187 auto const window = workers * 8;
1188 auto const batch_limit = std::min(workers * 4, device_fasta_pipeline::max_files);
1189 sequence_buffer_pool buffers(stream, workers, size_t{32} << 20, page_locked, in_place);
1190 buffers.set_capacity(
1191 window + batch_limit * device_fasta_pipeline::lane_count + stager_hold_slots + 2
1192 );
1193 std::optional<device_fasta_pipeline> normaliser;
1194 struct loaded_fasta {
1195 size_t id;
1196 std::unique_ptr<fastx_sequence_file> sequence;
1197 };
1198 std::array<std::vector<loaded_fasta>, device_fasta_pipeline::lane_count> batches;
1199 for (auto& batch : batches) {
1200 batch.reserve(batch_limit);
1201 }
1202 auto fill = [&](database_stager<K, BucketCount, Layout>& stager,
1203 size_t base,
1204 size_t count) -> Result<void> {
1205 if (!normaliser) {
1206 auto const available = CUDDL_TRY(available_device_bytes(stream));
1207 auto const budget = (available - available / 5) / device_fasta_pipeline::lane_count;
1208 auto const wanted = staging_bytes.value_or(default_arena_ceiling);
1209 auto const overhead = device_fasta_pipeline::lane_overhead(wanted);
1210 auto const capacity = std::min(wanted, budget > overhead ? budget - overhead : 0);
1211 if (capacity < 2) return Err(Error::resource("no room for FASTA normalisation"));
1212 CUDDL_TRY(cuda_try([&] { normaliser.emplace(stream, capacity, 0, workers, false); }));
1213 }
1214 fastx_load_pool pool(
1215 paths.subspan(base, count),
1216 workers,
1217 {acquire_sequence_target, &buffers, direct_reads},
1218 window,
1219 true,
1220 true
1221 );
1222 std::vector<decoded_fasta> sources;
1223 sources.reserve(batch_limit);
1224 std::vector<inflated_genome> genomes;
1225 std::vector<size_t> fallback;
1226 auto stage_host = [&](size_t id,
1227 std::unique_ptr<fastx_sequence_file> sequence) -> Result<void> {
1228 if (!sequence->input.empty()) {
1229 CUDDL_TRY(parse_fastx_sequence_file(*sequence, paths[base + id].string()));
1230 }
1231 auto const& extents = sequence->extents;
1232 auto const* pinned_base = page_locked ? sequence->decompressed_target : nullptr;
1233 auto const pinned_size = page_locked ? sequence->decompressed_size : 0;
1234 return stager.add_genome(id, extents, pinned_base, pinned_size, std::move(sequence));
1235 };
1236 auto finish_lane = [&](size_t lane) -> Result<void> {
1237 auto& batch = batches[lane];
1238 if (batch.empty()) return Ok();
1239 genomes.clear();
1240 fallback.clear();
1241 CUDDL_TRY(normaliser->finish(lane, {}, genomes, fallback));
1242 for (auto const& genome : genomes) {
1243 CUDDL_TRY(stager.add_resident(genome.id, genome.bases, genome.size));
1244 }
1245 size_t rejected = 0;
1246 for (auto& loaded : batch) {
1247 if (rejected < fallback.size() && loaded.id == fallback[rejected]) {
1248 CUDDL_TRY(stage_host(loaded.id, std::move(loaded.sequence)));
1249 ++rejected;
1250 }
1251 }
1252 CUDDL_TRY(stager.flush());
1253 CUDDL_TRY(normaliser->release(lane, stream));
1254 batch.clear();
1255 return Ok();
1256 };
1257 size_t next = 0, lane = 0;
1258 std::optional<loaded_fasta> pending;
1259 while (next < count || pending) {
1260 CUDDL_TRY(finish_lane(lane));
1261 sources.clear();
1262 size_t bytes = 0;
1263 auto& batch = batches[lane];
1264 while (next < count || pending) {
1265 if (!pending) {
1266 auto loaded = CUDDL_TRY(pool.take_ready());
1267 pending.emplace(loaded_fasta{loaded.first, std::move(loaded.second)});
1268 ++next;
1269 }
1270 auto const input = pending->sequence->input;
1271 if (input.empty() || input.size() >= normaliser->slot_capacity()) {
1272 CUDDL_TRY(stage_host(pending->id, std::move(pending->sequence)));
1273 pending.reset();
1274 continue;
1275 }
1276 if (batch.size() == batch_limit ||
1277 input.size() >= normaliser->slot_capacity() - bytes)
1278 break;
1279 bytes += input.size() + 1;
1280 sources.push_back({pending->id, input});
1281 batch.push_back(std::move(*pending));
1282 pending.reset();
1283 }
1284 if (!batch.empty()) CUDDL_TRY(normaliser->submit_decoded(lane, sources));
1285 lane = (lane + 1) % batches.size();
1286 }
1287 for (size_t offset = 0; offset < batches.size(); ++offset) {
1288 CUDDL_TRY(finish_lane((lane + offset) % batches.size()));
1289 }
1290 return Ok();
1291 };
1292 // Capture errors before draining GPU work so pending input owners remain alive.
1293 std::exception_ptr failure;
1294 auto staged = [&]() -> Result<void> {
1295 try {
1296 return stage_groups<K, BucketCount, Layout>(
1297 paths.size(),
1299 staging_bytes,
1300 stream,
1301 statistics,
1302 fill,
1303 sink,
1304 in_place,
1305 blacklist
1306 );
1307 } catch (...) {
1308 failure = std::current_exception();
1309 return Ok();
1310 }
1311 }();
1312 if (normaliser) CUDDL_TRY(normaliser->wait_lanes(stream));
1313 if (failure) std::rethrow_exception(failure);
1314 if (!staged) return staged;
1315 if (statistics != nullptr) {
1316 statistics->workers = static_cast<unsigned>(workers);
1317 statistics->in_place = in_place;
1318 statistics->pinned_buffers = page_locked ? buffers.buffers() : 0;
1319 }
1320 return Ok();
1321}
1322
1329template <uint32_t K, size_t BucketCount, typename Layout, typename Sink>
1330[[nodiscard]] Result<void> stage_paths_inflating(
1331 std::span<std::filesystem::path const> paths,
1332 cuda::stream_ref stream,
1333 std::optional<size_t> staging_bytes,
1334 unsigned parser_workers,
1335 transfer_mode transfer,
1336 bool direct_reads,
1337 bool hardware_inflate,
1338 reference_build_statistics* statistics,
1339 Sink&& sink,
1340 cuda::std::optional<blacklist_view> blacklist = cuda::std::nullopt
1341) {
1342 auto const workers = path_loaders::worker_count(paths.size(), parser_workers);
1343 bool const in_place = stages_in_place(transfer, stream.device());
1344 bool const page_locked = pages_locked(transfer, stream.device());
1345 std::vector<gzip_file_probe> probes(paths.size());
1346 // Each probe reads a file's header and trailer, two waits on a parallel file system, and
1347 // moves almost no bytes, so it keeps more in flight than whole-file reads: 20,000 genomes no
1348 // A100 node had cached built in 13.9-15.6 s with 64 probes in flight and 9.6-10.5 s with 256.
1349 auto const probe_depth = ablated("probe-depth") ? parallel_read_depth : 4 * parallel_read_depth;
1350 parallel_for(paths.size(), std::max(workers, probe_depth), [&](size_t id) {
1351 probes[id] = probe_gzip_file(paths[id]);
1352 });
1353 std::optional<device_fasta_pipeline> inflater;
1354 sequence_buffer_pool host_buffers(stream, workers, size_t{32} << 20, page_locked, in_place);
1355 host_buffers.set_capacity(workers * 4 + stager_hold_slots + 2, packed_buffer_bytes);
1356 auto const fits = [&](gzip_file_probe const& probe) {
1357 return probe.device && probe.isize + size_t{1} <= inflater->slot_capacity() &&
1358 probe.host_bytes() <= inflater->compressed_capacity();
1359 };
1360 // Sized once the stager holds its rows and arena, from what is left and what the inputs need.
1361 auto const make_inflater = [&]() -> Result<void> {
1362 size_t slots = 0, compressed = 0;
1363 for (auto const& probe : probes) {
1364 if (!probe.device) continue;
1365 slots += probe.isize + size_t{1};
1366 compressed += probe.host_bytes();
1367 }
1368 auto const available = CUDDL_TRY(available_device_bytes(stream));
1369 auto const budget = (available - available / 5) / device_fasta_pipeline::lane_count;
1370 auto const overhead = device_fasta_pipeline::lane_overhead(budget);
1371 // A lane holds one batch, not a share of the device: its compressed bytes are page-locked
1372 // host memory, which pins at about 2.7 GB/s on one thread. Sized from a B300's 275 GB,
1373 // six lanes pinned 48 GB in 17 s, and 10,000 genomes made only three batches. Smaller
1374 // lanes also let the host loaders take a fairer share: on an A100, 10,000 genomes took
1375 // 1.79 s with 2 GiB lanes and 1.53 s with 1 GiB at 112 loaders, and 2.12-2.17 s with
1376 // either at 16; 512 MiB lanes took 2.48 s at 16. Large inputs need larger batches,
1377 // though: nvCOMP inflates one file per thread block, so a batch fills the SMs only with
1378 // enough files. On a B300 with 16 loaders, all 115,223 genomes built in 24.7-24.9 s with
1379 // 1 GiB lanes and 16.7-17.6 s with 4 GiB, so a lane takes a 64th of the input. Their BGZF
1380 // copy took 16.6-17.8 s with 1 GiB and 14.7-17.2 s with 4 GiB.
1381 auto const lane_bytes =
1382 ablated("input-lanes")
1383 ? size_t{1} << 30
1384 : std::clamp((slots + compressed) / 64, size_t{1} << 30, size_t{4} << 30);
1385 auto const usable = std::min(budget > overhead ? budget - overhead : 0, lane_bytes);
1386 auto const compressed_capacity =
1387 slots + compressed == 0
1388 ? size_t{0}
1389 : std::min(
1390 compressed,
1391 static_cast<size_t>(
1392 static_cast<long double>(usable) * compressed / (slots + compressed)
1393 )
1394 );
1395 auto const room = std::min(
1396 usable / 2,
1397 budget > compressed_capacity + overhead ? budget - compressed_capacity - overhead : 0
1398 );
1399 return cuda_try([&] {
1400 inflater.emplace(
1401 stream, std::min(room, slots), compressed_capacity, workers, hardware_inflate
1402 );
1403 });
1404 };
1405 CUDDL_TRY((stage_groups<K, BucketCount, Layout>(
1406 paths.size(),
1408 staging_bytes,
1409 stream,
1410 statistics,
1411 [&](database_stager<K, BucketCount, Layout>& stager, size_t base, size_t count)
1412 -> Result<void> {
1413 if (!inflater) CUDDL_TRY(make_inflater());
1414 // Host loaders take `order` from the front: files the device cannot hold, then the
1415 // device candidates from the back. nvCOMP takes candidates from the front and caps
1416 // the loaders before each batch, so whichever side is faster on this host takes more.
1417 // Candidates run smallest first: one thread block inflates a whole file, so large
1418 // files suit the CPU, and a batch of similar sizes has no straggler holding it open.
1419 std::vector<size_t> device_ids, order, handed_back;
1420 for (size_t id = base; id < base + count; ++id) {
1421 (fits(probes[id]) ? device_ids : order).push_back(id);
1422 }
1423 std::ranges::stable_sort(device_ids, {}, [&](size_t id) {
1424 return probes[id].compressed;
1425 });
1426 // With a decompression engine, whole batches for the engine alternate with whole
1427 // batches of larger files in proportion to their gzip bytes, so the engine and the
1428 // SMs inflate at the same time in different lanes. Smallest first overall would run
1429 // every engine batch before any SM batch; mixing within a batch leaves too few large
1430 // files per launch to fill the SMs, since nvCOMP inflates one file per thread block.
1431 if (auto const limit = inflater->hardware_limit(); limit != 0) {
1432 auto const larger = std::ranges::stable_partition(device_ids, [&](size_t id) {
1433 return probes[id].bgzf || probes[id].isize <= limit;
1434 });
1435 std::vector<size_t> const engine(device_ids.begin(), larger.begin());
1436 std::vector<size_t> const cores(larger.begin(), larger.end());
1437 auto const bytes = [&](std::span<size_t const> ids) {
1438 long double total = 0;
1439 for (auto id : ids) total += static_cast<long double>(probes[id].compressed);
1440 return total;
1441 };
1442 auto const engine_total = bytes(engine), cores_total = bytes(cores);
1443 long double engine_taken = 0, cores_taken = 0;
1444 std::span<size_t const> engine_left{engine}, cores_left{cores};
1445 device_ids.clear();
1446 while (!engine_left.empty() || !cores_left.empty()) {
1447 bool const take_engine = cores_left.empty() || (!engine_left.empty() &&
1448 engine_taken * cores_total <=
1449 cores_taken * engine_total);
1450 auto& left = take_engine ? engine_left : cores_left;
1451 auto const batch =
1452 left.first(std::max<size_t>(1, inflater->batch_files(left, probes)));
1453 device_ids.insert(device_ids.end(), batch.begin(), batch.end());
1454 (take_engine ? engine_taken : cores_taken) += bytes(batch);
1455 left = left.subspan(batch.size());
1456 }
1457 }
1458 order.insert(order.end(), device_ids.rbegin(), device_ids.rend());
1459 std::vector<std::filesystem::path> host_paths;
1460 host_paths.reserve(order.size());
1461 for (auto id : order) host_paths.push_back(paths[id]);
1462 // Gzip bytes of the first n files of `order`, to tell how much the host has left.
1463 std::vector<uint64_t> order_bytes(order.size() + 1);
1464 for (size_t index = 0; index < order.size(); ++index) {
1465 order_bytes[index + 1] = order_bytes[index] + probes[order[index]].compressed;
1466 }
1467 auto const started = std::chrono::steady_clock::now();
1468 std::atomic<uint64_t> host_done{0};
1469 fastx_load_pool host_pool(
1470 host_paths,
1471 std::min(workers, host_paths.size()),
1472 decompression_source{acquire_sequence_target, &host_buffers, direct_reads},
1473 workers * 4,
1474 false,
1475 true,
1476 K
1477 );
1478 // The host path stages on its own thread: it waits on copies over the bus, which
1479 // must not hold up the nvCOMP lanes this thread drives. The stager is shared.
1480 std::mutex staging;
1481 auto host_staged = std::async(std::launch::async, [&]() -> Result<void> {
1482 CUDDL_CUDA_TRY(cudaSetDevice(stream.device().get()));
1483 while (true) {
1484 auto loaded = CUDDL_TRY(host_pool.take_ready_until_limit());
1485 if (!loaded) return Ok();
1486 auto& [index, sequence] = *loaded;
1487 auto const* pinned_base = page_locked ? sequence->decompressed_target : nullptr;
1488 auto const pinned_size = page_locked ? sequence->decompressed_size : 0;
1489 auto const genome = order[index] - base;
1490 host_done += probes[order[index]].compressed;
1491 stager.wait_hold_slot();
1492 std::lock_guard lock(staging);
1493 if (sequence->words != nullptr) {
1494 CUDDL_TRY(stager.add_packed_genome(
1495 genome, std::move(sequence), pinned_base, pinned_size
1496 ));
1497 continue;
1498 }
1499 auto const& extents = sequence->extents;
1500 CUDDL_TRY(stager.add_genome(
1501 genome, extents, pinned_base, pinned_size, std::move(sequence)
1502 ));
1503 }
1504 });
1505 // On an early return, stop the loaders at what they claimed so the host thread ends.
1506 struct stop_loaders {
1507 fastx_load_pool& pool;
1508 ~stop_loaders() {
1509 pool.cap(0);
1510 }
1511 } stop{host_pool};
1512 std::vector<inflated_genome> genomes;
1513 std::array<bool, device_fasta_pipeline::lane_count> busy{};
1514 std::array<uint64_t, device_fasta_pipeline::lane_count> lane_bytes{};
1515 size_t next = 0, lane = 0;
1516 uint64_t device_done = 0, device_queued = 0;
1517 std::chrono::steady_clock::time_point device_started;
1518 bool device_open = !device_ids.empty();
1519 // Whether the device, at the rate it has inflated so far, finishes what it holds plus
1520 // @p batch before the host loaders, at theirs, finish everything else. Each lane holds
1521 // hundreds of megabytes, so claiming whenever a lane was free left nvCOMP 70% of the
1522 // files on an A100 with 112 loaders: 1.9 s, while the loaders alone took 1.3 s.
1523 auto const device_keeps_up = [&](std::span<size_t const> batch) {
1524 if (ablated("co-scheduling") || device_done == 0 || host_done == 0) return true;
1525 uint64_t bytes = 0;
1526 for (auto id : batch) bytes += probes[id].compressed;
1527 auto const now = std::chrono::steady_clock::now();
1528 auto const host_rate = static_cast<double>(host_done) /
1529 std::chrono::duration<double>(now - started).count();
1530 auto const device_rate =
1531 static_cast<double>(device_done) /
1532 std::chrono::duration<double>(now - device_started).count();
1533 auto const host_left = static_cast<double>(order_bytes[order.size() - next]) -
1534 static_cast<double>(host_done + bytes);
1535 return static_cast<double>(device_queued + bytes) / device_rate <=
1536 host_left / host_rate;
1537 };
1538 // Cycle through lanes, submitting a batch before consuming the oldest one.
1539 while (device_open || std::ranges::any_of(busy, [](bool value) { return value; })) {
1540 if (device_open && !busy[lane]) {
1541 auto const remaining = std::span{device_ids}.subspan(next);
1542 auto const wanted = inflater->batch_files(remaining, probes);
1543 if (device_keeps_up(remaining.first(wanted))) {
1544 auto const limit = host_pool.cap(order.size() - next - wanted);
1545 auto const files = order.size() - next - limit;
1546 device_open = files == wanted && next + files < device_ids.size();
1547 if (files != 0) {
1548 if (device_queued == 0 && device_done == 0) {
1549 device_started = std::chrono::steady_clock::now();
1550 }
1551 lane_bytes[lane] = 0;
1552 for (auto id : remaining.first(files)) {
1553 lane_bytes[lane] += probes[id].compressed;
1554 }
1555 device_queued += lane_bytes[lane];
1556 next += CUDDL_TRY(inflater->submit(
1557 lane, remaining.first(files), paths, probes, direct_reads
1558 ));
1559 busy[lane] = true;
1560 }
1561 } else if (std::ranges::none_of(busy, [](bool value) { return value; })) {
1562 // The loaders, which may take every file the device has not, finish first.
1563 device_open = false;
1564 }
1565 }
1566 auto const other = (lane + 1) % busy.size();
1567 if (busy[other]) {
1568 genomes.clear();
1569 CUDDL_TRY(inflater->finish(other, probes, genomes, handed_back));
1570 std::lock_guard lock(staging);
1571 for (auto const& genome : genomes) {
1572 CUDDL_TRY(stager.add_resident(genome.id - base, genome.bases, genome.size));
1573 }
1574 if (statistics != nullptr) statistics->device_files += genomes.size();
1575 CUDDL_TRY(stager.flush());
1576 CUDDL_TRY(inflater->release(other, stream));
1577 busy[other] = false;
1578 device_done += lane_bytes[other];
1579 device_queued -= lane_bytes[other];
1580 }
1581 lane = other;
1582 }
1583 CUDDL_TRY(host_staged.get());
1584 // ponytail: one fallback pool per group; pipeline it if fallback-heavy inputs dominate.
1585 return stage_host_loaded<K, BucketCount, Layout>(
1586 stager, paths, handed_back, base, workers, host_buffers, page_locked, direct_reads
1587 );
1588 },
1589 sink,
1590 in_place,
1591 blacklist
1592 )));
1593 if (statistics != nullptr) {
1594 statistics->workers = static_cast<unsigned>(workers);
1595 statistics->in_place = in_place;
1596 statistics->pinned_buffers = page_locked ? host_buffers.buffers() : 0;
1597 }
1598 return Ok();
1599}
1600
1601#endif
1602
1611template <uint32_t K, size_t BucketCount, typename Layout = default_register_layout, typename Sink>
1613 std::span<std::filesystem::path const> paths,
1614 cuda::stream_ref stream,
1615 std::optional<size_t> staging_bytes,
1616 unsigned parser_workers,
1617 transfer_mode transfer,
1618 decompression_backend decompression,
1619 bool direct_reads,
1620 reference_build_statistics* statistics,
1621 Sink&& sink,
1622 cuda::std::optional<blacklist_view> blacklist = cuda::std::nullopt
1623) {
1624 // Loaders and inflation reuse pinned buffers through the build; none outlive it.
1625 pinned_pool_trim const release_pinned{stream.device(), pinned_placement::host};
1626 if (parser_workers == 0) {
1627 return Err(Error::invalid_argument("a path build needs at least one loader"));
1628 }
1629#if !CUDDL_HAS_NVCOMP
1630 if (decompression == decompression_backend::gpu ||
1631 decompression == decompression_backend::gpu_cores ||
1632 decompression == decompression_backend::hybrid) {
1633 return Err(
1634 Error::invalid_argument("requested decompression backend requires nvCOMP support")
1635 );
1636 }
1637 decompression = decompression_backend::cpu;
1638#endif
1639 if (transfer == transfer_mode::in_place && !device_reads_pageable_memory(stream.device())) {
1640 return Err(
1642 "in-place staging needs a device that reads pageable host memory"
1643 )
1644 );
1645 }
1646 if (paths.empty()) return Ok();
1647 // A coherent GPU reads the loaders' buffers in place, so CPU inflation feeds it best.
1648 // Elsewhere nvCOMP wins: 100,000 genomes on an RTX 5090 with 120 loaders built in 7.4 s with
1649 // gpu and 10.2 s with cpu, and the CPU-inflating backends kept the GPU 60% idle.
1650 if (decompression == decompression_backend::automatic) {
1651 if (device_reads_pageable_memory(stream.device())) {
1652 decompression = path_loaders::worker_count(paths.size(), parser_workers) > 1
1653 ? decompression_backend::hybrid
1654 : decompression_backend::cpu;
1655 } else {
1656 decompression = decompression_backend::gpu;
1657 }
1658 }
1659#if CUDDL_HAS_NVCOMP
1660 if (decompression == decompression_backend::hybrid) {
1661 return stage_paths_normalising<K, BucketCount, Layout>(
1662 paths,
1663 stream,
1664 staging_bytes,
1665 parser_workers,
1666 transfer,
1667 direct_reads,
1668 statistics,
1669 std::forward<Sink>(sink),
1670 blacklist
1671 );
1672 }
1673 if (decompression != decompression_backend::cpu) {
1674 return stage_paths_inflating<K, BucketCount, Layout>(
1675 paths,
1676 stream,
1677 staging_bytes,
1678 parser_workers,
1679 transfer,
1680 direct_reads,
1681 decompression == decompression_backend::gpu,
1682 statistics,
1683 std::forward<Sink>(sink),
1684 blacklist
1685 );
1686 }
1687#endif
1688 auto const in_place = stages_in_place(transfer, stream.device());
1689 auto const page_locked = pages_locked(transfer, stream.device());
1690 // Copied bases cross the bus, and packing them first moves a quarter of the bytes. Unpacked,
1691 // an A100 took 2.0-2.2 s for 10,000 genomes from 32 to 112 loaders, held by the copies;
1692 // packed, 1.8 s with 32 and 1.3 s with 64 or more. Packing costs the loaders time, so with
1693 // 16 it took 3.4 s against 2.8 s. A device that reads the bases in place gains nothing from
1694 // it: 16 loaders on a GH200 took 3.0 s packed against 2.7 s without.
1695 path_loaders loaders(
1696 stream,
1697 paths,
1698 parser_workers,
1699 page_locked,
1700 in_place,
1701 in_place || ablated("base-packing") ? 0 : K,
1702 direct_reads
1703 );
1704 // The corpus size only bounds a default arena, and only below its ceiling, so probing stops
1705 // there: a corpus of any size then costs at most a few dozen file opens here.
1706 uint64_t staged_ceiling = 0;
1707 for (auto const& path : paths) {
1708 if (staging_bytes.has_value() || staged_ceiling >= default_arena_ceiling) break;
1709 auto const decompressed = gzip_decompressed_size(path.string());
1710 if (decompressed != 0) {
1711 staged_ceiling += decompressed;
1712 continue;
1713 }
1714 std::error_code error;
1715 auto const size = std::filesystem::file_size(path, error);
1716 if (!error) staged_ceiling += size;
1717 }
1718 CUDDL_TRY((stage_groups<K, BucketCount, Layout>(
1719 paths.size(),
1720 staged_ceiling,
1721 staging_bytes,
1722 stream,
1723 statistics,
1724 [&](database_stager<K, BucketCount, Layout>& stager, size_t base, size_t count)
1725 -> Result<void> {
1726 for (size_t id = base; id < base + count; ++id) {
1727 auto sequence = CUDDL_TRY(loaders.take(paths, id));
1728 // Read the loaded file's parts before the move below: argument order is
1729 // unspecified, so a moved-from Result must not be dereferenced.
1730 auto const* const pinned_base =
1731 page_locked ? sequence->decompressed_target : nullptr;
1732 auto const pinned_size = page_locked ? sequence->decompressed_size : 0;
1733 auto const& extents = sequence->extents;
1734 if (sequence->words != nullptr) {
1735 CUDDL_TRY(stager.add_packed_genome(
1736 id - base, std::move(sequence), pinned_base, pinned_size
1737 ));
1738 continue;
1739 }
1740 CUDDL_TRY(stager.add_genome(
1741 id - base, extents, pinned_base, pinned_size, std::move(sequence)
1742 ));
1743 }
1744 return Ok();
1745 },
1746 sink,
1747 in_place,
1748 blacklist
1749 )));
1750 if (statistics != nullptr) {
1751 statistics->pinned_buffers = loaders.page_locked_buffers();
1752 statistics->workers = static_cast<unsigned>(loaders.workers());
1753 statistics->in_place = in_place;
1754 }
1755 loaders.reset();
1756 return Ok();
1757}
1758
1760template <uint32_t K, size_t BucketCount, typename Layout = default_register_layout, typename Sink>
1762 std::span<sequence_genome const> genomes,
1763 cuda::stream_ref stream,
1764 std::optional<size_t> staging_bytes,
1765 reference_build_statistics* statistics,
1766 Sink&& sink,
1767 cuda::std::optional<blacklist_view> blacklist = cuda::std::nullopt
1768) {
1769 if (genomes.empty()) return Ok();
1770 pinned_pool_trim const release_pinned{stream.device(), pinned_placement::host};
1771 // The caller knows exactly what the corpus holds, so the ceiling is the sum of its bases
1772 // rather than an estimate read out of file headers.
1773 uint64_t staged_ceiling = 0;
1774 for (auto const& genome : genomes) {
1775 for (auto const& record : genome.records) staged_ceiling += record.bases.size();
1776 }
1777 std::vector<fastx_sequence_extent> records;
1778 return stage_groups<K, BucketCount, Layout>(
1779 genomes.size(),
1780 staged_ceiling,
1781 staging_bytes,
1782 stream,
1783 statistics,
1785 size_t base,
1786 size_t count) -> Result<void> {
1787 for (size_t id = base; id < base + count; ++id) {
1788 auto const& genome = genomes[id];
1789 records.clear();
1790 records.reserve(genome.records.size());
1791 for (auto const& record : genome.records) {
1792 if (record.bases.empty()) continue;
1793 records.push_back(
1794 {record.bases.data(), record.bases.data() + record.bases.size()}
1795 );
1796 }
1797 CUDDL_TRY(stager.add_genome(id - base, records));
1798 }
1799 return Ok();
1800 },
1801 sink,
1802 false,
1803 blacklist
1804 );
1805}
1806
1807} // namespace cuddl::detail
Encodes genome records into register rows through a device arena.
Result< void > add_packed_genome(size_t genome, std::unique_ptr< fastx_sequence_file > holder, char const *pinned_base=nullptr, size_t pinned_size=0)
Stages one genome that pack_fastx_sequence_file packed into two-bit runs.
~database_stager()
Frees the arenas only after every copy into them.
Result< device_span< uint32_t const > > end_group(size_t genomes)
Launches everything staged and returns the group's rows on the device.
database_stager & operator=(database_stager const &)=delete
Result< void > add_resident(size_t genome, char const *bases, size_t size)
Stages one genome's bases that already sit on the device, without a copy.
Result< void > flush()
One launch for everything staged so far.
static constexpr size_t hold_slots
void wait_hold_slot()
Waits until the next genome staged by copy can be held without blocking.
Result< void > add_genome(size_t genome, std::span< fastx_sequence_extent const > records, char const *pinned_base=nullptr, size_t pinned_size=0, std::unique_ptr< fastx_sequence_file > holder={})
Stages one genome.
database_stager(cuda::stream_ref stream, staging_plan bounds, reference_build_statistics *statistics=nullptr, bool direct=false, cuda::std::optional< blacklist_view > blacklist=cuda::std::nullopt)
database_stager(database_stager const &)=delete
Result< void > begin_group(size_t genomes)
Clears the register rows of the next group of genomes.
Result< void > finish()
Waits for the device and publishes the statistics.
static size_t lane_overhead(size_t slot_capacity) noexcept
Device bytes a lane needs besides its slots and compressed bytes.
static constexpr size_t max_files
static constexpr size_t lane_count
Loader state a build fed by paths keeps for its whole run.
Result< std::unique_ptr< fastx_sequence_file > > take(std::span< std::filesystem::path const > paths, size_t id)
Loads genome id, through the pool when the build runs more than one loader.
void reset() noexcept
Joins the loaders. They hold leases, so pool state is only readable after this.
size_t workers() const noexcept
Loaders this build runs.
path_loaders(cuda::stream_ref stream, std::span< std::filesystem::path const > paths, unsigned parser_workers, bool page_locked, bool in_place, uint32_t pack_k, bool direct_reads)
static size_t worker_count(size_t paths, unsigned parser_workers) noexcept
Loaders to run: at most one per input, at most the requested workers, at most the hardware,...
size_t page_locked_buffers() const noexcept
Page-locked buffers the loaders held, for the statistics.
Returns the free pages of a device_pinned_pool to the OS when it leaves scope.
void set_capacity(size_t buffers, size_t reserve_each=0)
Number of buffers the pool may grow to.
decompression_target acquire(size_t bytes)
Returns reusable bytes, or a null target when the pool cannot serve.
sequence_buffer_pool(cuda::stream_ref stream, size_t buffers, size_t limit, bool page_locked, bool in_place)
limit bounds one buffer; larger genomes stay on the loader's own growing buffer.
size_t buffers() const noexcept
Buffers allocated so far. Read once the loaders have stopped.
#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
#define CUDDL_CUDA_ABORT(expr)
Checks a CUDA call and aborts on failure (destructors and RAII only).
Definition error.hpp:245
auto *const heap
Definition top_k.cuh:200
__global__ size_t uint32_t bool blacklist_view bool Layout
Definition kernels.cuh:155
transfer_mode
How a build gets CPU-parsed sequence bytes to the device.
@ pinned
Decompress into page-locked buffers, which the transfer engine reads directly.
@ in_place
Decompress into a heap buffer and let the kernels read it there.
constexpr size_t stager_arenas
Staging arenas the copy stream rotates through, so copies run ahead of the kernels that read earlier ...
Result< void > stage_sequences(std::span< sequence_genome const > genomes, cuda::stream_ref stream, std::optional< size_t > staging_bytes, reference_build_statistics *statistics, Sink &&sink, cuda::std::optional< blacklist_view > blacklist=cuda::std::nullopt)
Stages bases the caller already holds, handing each group's device rows to sink.
bool pages_locked(transfer_mode mode, cuda::device_ref device) noexcept
Whether a build in mode decompresses into page-locked buffers.
auto const lane
Definition kernels.cuh:966
auto const count
Definition kernels.cuh:1031
bool stages_in_place(transfer_mode mode, cuda::device_ref device) noexcept
Whether a build in mode stages the caller's bytes in place.
__global__ size_t uint32_t bool blacklist_view blacklist
Definition kernels.cuh:96
constexpr size_t parallel_read_depth
Reads a parallel file system needs in flight to give one node its bandwidth: on MOGON's Lustre,...
constexpr size_t default_arena_ceiling
Default arena ceiling.
pinned_placement
Where a device_pinned_pool takes its pages.
@ host
Any node: for buffers host threads fill.
void pack_fastx_sequence_file(fastx_sequence_file &result, uint32_t k)
Packs a parsed file's records in place into two-bit words, run by run.
Result< staging_plan > plan_staging(size_t references, uint64_t staged_ceiling, std::optional< size_t > staging_bytes, cuda::stream_ref stream)
Sizes the arena and the row store from free device memory and the corpus.
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__ top_k_slots slots
Definition top_k.cuh:195
auto const total
Definition kernels.cuh:443
Result< void > parse_fastx_sequence_file(fastx_sequence_file &result, std::string const &path)
__global__ uint32_t const uint32_t const uint32_t const uint32_t uint32_t size_t size_t uint32_t SearchResult * packed
Definition kernels.cuh:1791
Result< std::unique_ptr< fastx_sequence_file > > load_fastx_sequence_file(std::string const &path, decompression_source source={}, bool defer_fasta=false, bool read=false, uint32_t pack_k=0)
Result< size_t > available_device_bytes(cuda::stream_ref stream)
Device bytes an allocation on stream can still get.
constexpr bool staged
Definition kernels.cuh:1860
decompression_backend
Where inputs are decompressed; sketch construction always runs on the GPU.
@ gpu
nvCOMP for eligible gzip inputs; unsupported formats use host loaders.
@ gpu_cores
As gpu, with nvCOMP inflating on the SMs even where the device has a decompression engine: the baseli...
@ cpu
CPU inflation and normalisation.
@ hybrid
CPU inflation, followed by GPU FASTA normalisation of the inflated bytes.
auto const words
Definition top_k.cuh:204
constexpr bool ablations_enabled
Definition ablation.hpp:79
decompression_target acquire_sequence_target(void *context, size_t bytes)
decompression_source trampoline for sequence_buffer_pool.
Result< void > stage_groups(size_t genomes, uint64_t staged_ceiling, std::optional< size_t > staging_bytes, cuda::stream_ref stream, reference_build_statistics *statistics, Fill &&fill, Sink &&sink, bool in_place=false, cuda::std::optional< blacklist_view > blacklist=cuda::std::nullopt)
Runs the staging loop over a collection, filling one group at a time.
constexpr size_t packed_buffer_bytes
Bytes reserved per loader buffer that holds packed bases: a bacterial genome's two-bit words,...
void parallel_for(size_t count, size_t workers, Work &&work)
Runs work(index) for every index below count on workers threads.
constexpr uint32_t group
Definition kernels.cuh:1858
constexpr bool ablated(std::string_view) noexcept
Always false: this build compiles the ablation switches out.
Definition ablation.hpp:82
Result< void > stage_paths(std::span< std::filesystem::path const > paths, cuda::stream_ref stream, std::optional< size_t > staging_bytes, unsigned parser_workers, transfer_mode transfer, decompression_backend decompression, bool direct_reads, reference_build_statistics *statistics, Sink &&sink, cuda::std::optional< blacklist_view > blacklist=cuda::std::nullopt)
Stages a path collection, handing each group's device rows to sink.
bool device_reads_pageable_memory(cuda::device_ref device) noexcept
Whether device reads pageable host memory itself.
constexpr size_t stager_hold_slots
Host memory the device still has to read must stay alive: the transfer engine's read of a pageable bu...
uint64_t gzip_decompressed_size(std::string const &path)
Uncompressed size the gzip trailer advertises, or 0 when it cannot be read.
Result< void > download_store(device_span< uint32_t const > store, std::span< uint16_t > scores, cuda::stream_ref stream)
Copies the winner scores of device store rows into host score rows.
auto const word
Definition top_k.cuh:241
__global__ size_t uint16_t * rows
Definition kernels.cuh:1499
cuda::pinned_memory_pool_ref device_pinned_pool(cuda::device_ref device, pinned_placement placement)
A page-locked memory pool accessible from device only.
Result< void > Ok() noexcept
Success return for Result<void>; same as return {}.
Definition error.hpp:181
register_layout< 6, 10 > default_register_layout
Definition register.cuh:22
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
Exact membership within the candidate's destination bucket.
Definition blacklist.cuh:33
Supplies storage for one file, or a null target when none is available.
Host-visible buffer a caller offers for decompressed bytes.
One piece of sequence of one genome, and where its bytes are.
One genome as its records, in order. No records keeps the genome's ID with a zero row.
std::span< sequence_record const > records
One genome of bases the caller already holds.
Sizing a build resolves before its first genome.
A span that is assumed to point to device-accessible memory.
How a build moved sequence bytes to the device.
bool measure_resident
Measure GPU row reset and sketch construction, excluding input transfers and readback.