23#include <cuda/algorithm>
25#include <cuda/devices>
26#include <cuda/memory_pool>
29#ifndef CUDDL_HAS_NVCOMP
30 #define CUDDL_HAS_NVCOMP 0
81 return device.attribute(cuda::device_attributes::pageable_memory_access) != 0 &&
83 cuda::device_attributes::pageable_memory_access_uses_host_page_tables
91 return mode == transfer_mode::in_place ||
100 if (mode == transfer_mode::pinned)
return true;
101 if (mode != transfer_mode::automatic)
return false;
128 size_t free_bytes = 0, device_bytes = 0;
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);
142template <u
int32_t K,
size_t BucketCount>
145 uint64_t staged_ceiling,
146 std::optional<size_t> staging_bytes,
147 cuda::stream_ref stream
150 size_t const usable = available - available / 10;
151 size_t const row_bytes = BucketCount *
sizeof(uint32_t);
155 size_t const group = std::min(references, std::max<size_t>(1, usable / (4 * row_bytes)));
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(
166 static_cast<uint64_t
>(usable - usable / 5),
167 static_cast<uint64_t
>(host_share),
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) {
176 size_t const staging = staging_bytes.has_value()
180 if (staging < K + 1 || staging > after_rows /
stager_arenas) {
183 "insufficient free device memory for reference staging (free " +
184 std::to_string(available >> 20) +
" MiB)"
190 size_t const max_pieces = std::min<size_t>(
size_t{1} << 20, std::max<size_t>(64, staging / K));
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}
221template <u
int32_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;
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;
237 start = std::exchange(other.start,
nullptr);
238 stop = std::exchange(other.stop,
nullptr);
241 ~resident_event_span() {
257 cuda::stream_ref stream,
261 cuda::std::optional<blacklist_view>
blacklist = cuda::std::nullopt
265 statistics_(statistics),
269 cuda::make_device_buffer<uint32_t>(
272 bounds.
group * BucketCount,
276 copy_stream_(stream.device()),
277 kernel_stream_(stream.device(), greatest_stream_priority()),
280 cuda::make_device_buffer<char>(
283 direct ? bounds.staging :
stager_arenas * arena_stride(bounds.staging),
296 sm_(static_cast<size_t>(
297 stream.device().attribute(
cuda::device_attributes::multiprocessor_count)
301 stream.device().attribute(
cuda::device_attributes::max_grid_dim_x)
305 for (
size_t slot = 0; slot <
hold_slots; ++slot) {
306 held_consumed_.emplace_back(stream);
308 staged_chunks_.reserve(std::min<size_t>(bounds_.
max_pieces,
size_t{1} << 16));
309 copied_chunks_.reserve(staged_chunks_.capacity());
311 copy_stream_.wait(stream_);
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 = {}
333 auto const record_size = [&](
size_t index) {
334 return static_cast<size_t>(records[index].end - records[index].begin);
337 while (record < records.size()) {
340 direct_ ? std::numeric_limits<size_t>::max() : bounds_.staging - arena_used_;
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) {
352 if (arena_used_ != 0) {
358 auto const size = record_size(record);
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;
365 if (arena_used_ + span > bounds_.
staging ||
366 staged_chunks_.size() >= bounds_.
max_pieces) {
369 auto const*
const source = records[record].begin + offset - overlap;
374 static_cast<uint32_t
>(span - K + 1),
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);
392 cuda::std::span{source, run_bytes},
393 device_span<char>{arena() + arena_used_, run_bytes}
397 for (
size_t index = record; index < run; ++index) {
398 auto const size = record_size(index);
399 if (size < K)
continue;
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);
410 held_bytes_ += run_bytes;
411 if (held_bytes_ >= bounds_.
staging / 2) {
415 arena_used_ += run_bytes;
420 if (holder) hold(std::move(holder));
430 std::unique_ptr<fastx_sequence_file> holder,
431 char const* pinned_base =
nullptr,
432 size_t pinned_size = 0
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;
441 while (run < runs.size()) {
442 auto const room = direct_ ? std::numeric_limits<size_t>::max() : arena_room_words();
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) {
453 if (arena_used_ != 0 || !staged_chunks_.empty()) {
457 auto const halo = (
size_t{K} - 1 + word_bases - 1) / word_bases;
458 auto const capacity =
460 if (capacity <= halo) {
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 -
470 if ((!direct_ &&
count > arena_room_words()) ||
471 staged_chunks_.size() >= bounds_.
max_pieces) {
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);
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) {
487 base + (runs[index].
word - first) *
sizeof(uint32_t),
488 static_cast<uint32_t
>(runs[index].bases - K + 1),
496 hold(std::move(holder));
505 if (size < K)
return Ok();
507 describe(bases,
static_cast<uint32_t
>(size - K + 1), genome);
513 auto const resident =
CUDDL_TRY(begin_resident(stream_));
516 stream_, cuda::std::span{rows_.data(), genomes * BucketCount}, uint32_t{0}
519 CUDDL_TRY(end_resident(resident, stream_));
538 if (statistics_ !=
nullptr) {
540 statistics_->
batches = batches_;
542 for (
auto const& span : resident_spans_) {
543 float elapsed_ms = 0;
544 CUDDL_CUDA_TRY(cudaEventElapsedTime(&elapsed_ms, span.start, span.stop));
553 [[nodiscard]]
static constexpr size_t arena_stride(
size_t staging)
noexcept {
554 return (staging + 15) & ~size_t{15};
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));
564 [[nodiscard]] Result<size_t> begin_resident(cuda::stream_ref stream) {
566 return std::numeric_limits<size_t>::max();
568 resident_event_span span;
572 resident_spans_.push_back(std::move(span));
573 return resident_spans_.size() - 1;
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()));
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}
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;
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);
604 held_bytes_ += bytes;
607 arena_used_ = (arena_used_ + 3) & ~
size_t{3};
608 auto*
const target = arena() + arena_used_;
611 copy_stream_, cuda::std::span{source, bytes}, device_span<char>{target, bytes}
614 arena_used_ += bytes;
616 return static_cast<char const*
>(target);
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;
636 [[nodiscard]] Result<void> stage(
641 char const* pinned_base,
644 account(source, size, pinned_base, pinned_size);
648 describe(source, windows, genome);
654 cuda::std::span{source, size},
655 device_span<char>{arena() + arena_used_, size}
658 describe(arena() + arena_used_, windows, genome);
670 if (staged_chunks_.empty()) {
678 copied_consumed_.sync();
679 copied_chunks_.swap(staged_chunks_);
682 queued_.record(stream_);
683 kernel_stream_.wait(queued_);
685 copies_done_.record(copy_stream_);
686 kernel_stream_.wait(copies_done_);
691 cuda::std::span{copied_chunks_.data(), copied_chunks_.size()},
695 copied_consumed_.record(kernel_stream_);
696 auto const grid = std::min(block_end_, max_grid_);
698 auto const resident =
CUDDL_TRY(begin_resident(kernel_stream_));
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()>>>(
705 copied_chunks_.size(),
712 auto const encode = [&]<
bool Naive>() {
714 launch.template operator()<
true, Naive>();
716 launch.template operator()<
false, Naive>();
721 if (
ablated(
"kmer-encoding")) {
722 encode.template operator()<
true>();
724 encode.template operator()<
false>();
727 encode.template operator()<
false>();
730 CUDDL_TRY(end_resident(resident, kernel_stream_));
732 kernel_done_.record(kernel_stream_);
733 stream_.wait(kernel_done_);
734 if (direct_ && !batch_files_.empty()) {
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();
746 arena_done_[arena_index_].record(stream_);
748 copy_stream_.wait(arena_done_[arena_index_]);
751 staged_chunks_.clear();
764 auto const slot = held_count_ % held_.size();
765 if (held_[slot]) held_consumed_[slot].sync();
769 [[nodiscard]]
char* arena() noexcept {
770 return arena_.data() + arena_index_ * arena_stride(bounds_.
staging);
779 void hold(std::unique_ptr<fastx_sequence_file> holder) {
781 batch_files_.push_back(std::move(holder));
784 auto const slot = held_count_++ % held_.size();
786 held_consumed_[slot].sync();
789 held_consumed_[slot].record(copy_stream_);
790 held_[slot] = std::move(holder);
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;
843template <
size_t BucketCount>
846 std::span<uint16_t> scores,
847 cuda::stream_ref stream
849 auto const genomes = scores.size() / BucketCount;
850 if (genomes == 0)
return Ok();
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()
876 cuda::stream_ref stream,
890 slots_.reserve(buffers);
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
910 [[nodiscard]]
size_t buffers() const noexcept {
911 return slots_.size();
916 if (bytes == 0 || bytes > limit_)
return {};
917 slot* owner =
nullptr;
919 std::lock_guard lock(mutex_);
921 if (owner ==
nullptr)
return {};
926 if (in_place_) owner->consumed.sync();
927 if (owner->size < bytes) {
928 auto grown =
size_t{1} << 16;
929 while (grown < bytes) {
932 owner->resize(stream_, grown, pinned_pool_);
943 [[nodiscard]] slot* claim() {
944 if (free_head_ !=
nullptr) {
945 auto* owner = free_head_;
946 free_head_ = owner->next_free;
949 if (slots_.size() >= capacity_)
return nullptr;
950 slots_.push_back(std::make_unique<slot>(stream_));
951 return slots_.back().get();
956 std::optional<cuda::buffer<char, cuda::mr::host_accessible, cuda::mr::device_accessible>>
958 std::unique_ptr<char[]> heap;
960 cuda::event consumed;
961 slot* next_free =
nullptr;
963 explicit slot(cuda::stream_ref stream) : consumed(stream) {}
966 cuda::stream_ref stream,
968 std::optional<cuda::pinned_memory_pool_ref>
const& pinned_pool
971 pinned.emplace(stream, *pinned_pool, bytes, cuda::no_init);
973 heap = std::make_unique_for_overwrite<char[]>(bytes);
983 [[nodiscard]] decompression_target lease(slot* owner) {
985 return {owner->data(), owner->size, std::shared_ptr<void>(owner, [pool, owner](
void*) {
986 std::lock_guard lock(pool->mutex_);
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;
994 pool->free_tail_->next_free = owner;
996 pool->free_tail_ = owner;
1000 cuda::stream_ref stream_;
1002 std::optional<cuda::pinned_memory_pool_ref> pinned_pool_;
1004 size_t capacity_{1};
1005 std::vector<std::unique_ptr<slot>> slots_;
1006 slot* free_head_ =
nullptr;
1007 slot* free_tail_ =
nullptr;
1033 cuda::stream_ref stream,
1034 std::span<std::filesystem::path const> paths,
1035 unsigned parser_workers,
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),
1048 auto const window = workers_ * 4;
1049 buffers_.set_capacity(
1053 if (workers_ > 1) pool_.emplace(paths, workers_, source_, window,
false,
false, pack_k);
1065 return page_locked_ ? buffers_.buffers() : 0;
1070 take(std::span<std::filesystem::path const> paths,
size_t id) {
1071 if (pool_.has_value())
return pool_->take(
id);
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));
1091 size_t workers_ = 1;
1093 bool page_locked_ =
false;
1094 uint32_t pack_k_ = 0;
1096 std::optional<fastx_load_pool> pool_;
1113 uint64_t staged_ceiling,
1114 std::optional<size_t> staging_bytes,
1115 cuda::stream_ref stream,
1120 cuda::std::optional<blacklist_view>
blacklist = cuda::std::nullopt
1123 CUDDL_TRY((plan_staging<K, BucketCount>(genomes, staged_ceiling, staging_bytes, stream)));
1126 while (base < genomes) {
1127 auto const count = std::min(plan.group, genomes - base);
1140template <u
int32_t K,
size_t BucketCount,
typename Layout>
1142 database_stager<K, BucketCount, Layout>& stager,
1143 std::span<std::filesystem::path const> paths,
1144 std::span<size_t const>
ids,
1147 sequence_buffer_pool& buffers,
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(
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;
1165 ids[index] - base, extents, pinned_base, pinned_size, std::move(sequence)
1172template <u
int32_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,
1180 reference_build_statistics* statistics,
1182 cuda::std::optional<blacklist_view>
blacklist = cuda::std::nullopt
1186 auto const page_locked =
pages_locked(transfer, stream.device());
1187 auto const window = workers * 8;
1189 sequence_buffer_pool buffers(stream, workers,
size_t{32} << 20, page_locked,
in_place);
1190 buffers.set_capacity(
1193 std::optional<device_fasta_pipeline> normaliser;
1194 struct loaded_fasta {
1196 std::unique_ptr<fastx_sequence_file> sequence;
1199 for (
auto& batch : batches) {
1200 batch.reserve(batch_limit);
1202 auto fill = [&](database_stager<K, BucketCount, Layout>& stager,
1204 size_t count) -> Result<void> {
1210 auto const capacity = std::min(wanted, budget > overhead ? budget - overhead : 0);
1212 CUDDL_TRY(
cuda_try([&] { normaliser.emplace(stream, capacity, 0, workers,
false); }));
1214 fastx_load_pool pool(
1215 paths.subspan(base,
count),
1217 {acquire_sequence_target, &buffers, direct_reads},
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()) {
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));
1236 auto finish_lane = [&](
size_t lane) -> Result<void> {
1237 auto& batch = batches[
lane];
1238 if (batch.empty())
return Ok();
1242 for (
auto const& genome : genomes) {
1243 CUDDL_TRY(stager.add_resident(genome.id, genome.bases, genome.size));
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)));
1257 size_t next = 0,
lane = 0;
1258 std::optional<loaded_fasta> pending;
1259 while (next <
count || pending) {
1263 auto& batch = batches[
lane];
1264 while (next <
count || pending) {
1266 auto loaded =
CUDDL_TRY(pool.take_ready());
1267 pending.emplace(loaded_fasta{loaded.first, std::move(loaded.second)});
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)));
1276 if (batch.size() == batch_limit ||
1277 input.size() >= normaliser->slot_capacity() - bytes)
1279 bytes += input.size() + 1;
1280 sources.push_back({pending->id, input});
1281 batch.push_back(std::move(*pending));
1284 if (!batch.empty())
CUDDL_TRY(normaliser->submit_decoded(
lane, sources));
1285 lane = (
lane + 1) % batches.size();
1287 for (
size_t offset = 0; offset < batches.size(); ++offset) {
1293 std::exception_ptr failure;
1294 auto staged = [&]() -> Result<void> {
1296 return stage_groups<K, BucketCount, Layout>(
1308 failure = std::current_exception();
1312 if (normaliser)
CUDDL_TRY(normaliser->wait_lanes(stream));
1313 if (failure) std::rethrow_exception(failure);
1315 if (statistics !=
nullptr) {
1316 statistics->workers =
static_cast<unsigned>(workers);
1318 statistics->pinned_buffers = page_locked ? buffers.buffers() : 0;
1329template <u
int32_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,
1337 bool hardware_inflate,
1338 reference_build_statistics* statistics,
1340 cuda::std::optional<blacklist_view>
blacklist = cuda::std::nullopt
1344 bool const page_locked =
pages_locked(transfer, stream.device());
1345 std::vector<gzip_file_probe> probes(paths.size());
1350 parallel_for(paths.size(), std::max(workers, probe_depth), [&](
size_t id) {
1351 probes[id] = probe_gzip_file(paths[id]);
1353 std::optional<device_fasta_pipeline> inflater;
1354 sequence_buffer_pool host_buffers(stream, workers,
size_t{32} << 20, page_locked,
in_place);
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();
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();
1381 auto const lane_bytes =
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
1391 static_cast<size_t>(
1392 static_cast<long double>(usable) * compressed / (
slots + compressed)
1395 auto const room = std::min(
1397 budget > compressed_capacity + overhead ? budget - compressed_capacity - overhead : 0
1401 stream, std::min(room,
slots), compressed_capacity, workers, hardware_inflate
1405 CUDDL_TRY((stage_groups<K, BucketCount, Layout>(
1411 [&](database_stager<K, BucketCount, Layout>& stager,
size_t base,
size_t count)
1413 if (!inflater) CUDDL_TRY(make_inflater());
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);
1423 std::ranges::stable_sort(device_ids, {}, [&](
size_t id) {
1424 return probes[id].compressed;
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;
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);
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};
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;
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());
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]);
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;
1467 auto const started = std::chrono::steady_clock::now();
1468 std::atomic<uint64_t> host_done{0};
1469 fastx_load_pool host_pool(
1471 std::min(workers, host_paths.size()),
1472 decompression_source{acquire_sequence_target, &host_buffers, direct_reads},
1481 auto host_staged = std::async(std::launch::async, [&]() -> Result<void> {
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) {
1495 genome, std::move(sequence), pinned_base, pinned_size
1499 auto const& extents = sequence->extents;
1501 genome, extents, pinned_base, pinned_size, std::move(sequence)
1506 struct stop_loaders {
1507 fastx_load_pool& 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();
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;
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;
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();
1548 if (device_queued == 0 && device_done == 0) {
1549 device_started = std::chrono::steady_clock::now();
1551 lane_bytes[
lane] = 0;
1552 for (
auto id : remaining.first(files)) {
1553 lane_bytes[
lane] += probes[id].compressed;
1555 device_queued += lane_bytes[
lane];
1557 lane, remaining.first(files), paths, probes, direct_reads
1561 }
else if (std::ranges::none_of(busy, [](
bool value) {
return value; })) {
1563 device_open =
false;
1566 auto const other = (
lane + 1) % busy.size();
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));
1574 if (statistics !=
nullptr) statistics->device_files += genomes.size();
1576 CUDDL_TRY(inflater->release(other, stream));
1577 busy[other] =
false;
1578 device_done += lane_bytes[other];
1579 device_queued -= lane_bytes[other];
1585 return stage_host_loaded<K, BucketCount, Layout>(
1586 stager, paths, handed_back, base, workers, host_buffers, page_locked, direct_reads
1593 if (statistics !=
nullptr) {
1594 statistics->workers =
static_cast<unsigned>(workers);
1596 statistics->pinned_buffers = page_locked ? host_buffers.buffers() : 0;
1611template <u
int32_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,
1622 cuda::std::optional<blacklist_view>
blacklist = cuda::std::nullopt
1626 if (parser_workers == 0) {
1629#if !CUDDL_HAS_NVCOMP
1630 if (decompression == decompression_backend::gpu ||
1631 decompression == decompression_backend::gpu_cores ||
1632 decompression == decompression_backend::hybrid) {
1637 decompression = decompression_backend::cpu;
1642 "in-place staging needs a device that reads pageable host memory"
1646 if (paths.empty())
return Ok();
1650 if (decompression == decompression_backend::automatic) {
1653 ? decompression_backend::hybrid
1654 : decompression_backend::cpu;
1656 decompression = decompression_backend::gpu;
1660 if (decompression == decompression_backend::hybrid) {
1661 return stage_paths_normalising<K, BucketCount, Layout>(
1669 std::forward<Sink>(sink),
1673 if (decompression != decompression_backend::cpu) {
1674 return stage_paths_inflating<K, BucketCount, Layout>(
1681 decompression == decompression_backend::gpu,
1683 std::forward<Sink>(sink),
1689 auto const page_locked =
pages_locked(transfer, stream.device());
1706 uint64_t staged_ceiling = 0;
1707 for (
auto const& path : paths) {
1710 if (decompressed != 0) {
1711 staged_ceiling += decompressed;
1714 std::error_code error;
1715 auto const size = std::filesystem::file_size(path, error);
1716 if (!error) staged_ceiling += size;
1718 CUDDL_TRY((stage_groups<K, BucketCount, Layout>(
1726 for (size_t id = base; id < base + count; ++id) {
1727 auto sequence = CUDDL_TRY(loaders.take(paths, id));
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
1740 CUDDL_TRY(stager.add_genome(
1741 id - base, extents, pinned_base, pinned_size, std::move(sequence)
1750 if (statistics !=
nullptr) {
1751 statistics->pinned_buffers = loaders.page_locked_buffers();
1752 statistics->workers =
static_cast<unsigned>(loaders.workers());
1760template <u
int32_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,
1767 cuda::std::optional<blacklist_view> blacklist = cuda::std::nullopt
1769 if (genomes.empty())
return Ok();
1770 pinned_pool_trim const release_pinned{stream.device(), pinned_placement::host};
1773 uint64_t staged_ceiling = 0;
1774 for (
auto const& genome : genomes) {
1775 for (
auto const& record : genome.records) staged_ceiling += record.bases.size();
1777 std::vector<fastx_sequence_extent> records;
1778 return stage_groups<K, BucketCount, Layout>(
1787 for (size_t id = base; id < base + count; ++id) {
1788 auto const& genome = genomes[id];
1790 records.reserve(genome.records.size());
1791 for (auto const& record : genome.records) {
1792 if (record.bases.empty()) continue;
1794 {record.bases.data(), record.bases.data() + record.bases.size()}
1797 CUDDL_TRY(stager.add_genome(id - base, records));
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>.
#define CUDDL_TRY(expr)
Propagates a cuddl::Result failure from the enclosing function (GNU statement expression).
#define CUDDL_CUDA_ABORT(expr)
Checks a CUDA call and aborts on failure (destructors and RAII only).
__global__ size_t uint32_t bool blacklist_view bool Layout
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.
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
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.
__global__ uint32_t uint32_t uint64_t uint32_t * ids
__global__ top_k_slots slots
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
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.
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.
constexpr bool ablations_enabled
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 bool ablated(std::string_view) noexcept
Always false: this build compiles the ablation switches out.
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.
__global__ size_t uint16_t * rows
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 {}.
register_layout< 6, 10 > default_register_layout
cuda::std::unexpected< Error > Err(Error error)
Failure return; converts to any Result<T> via cuda::std::unexpected.
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.
static Error invalid_argument(std::string message)
static Error resource(std::string message)
Fallible API result: cuda::std::expected<T, Error> with cuDDL factories.
Exact membership within the candidate's destination bucket.
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.
double resident_compute_ms