7#include <condition_variable>
18#include <system_error>
23#include <libdeflate.h>
26#include <cub/device/device_reduce.cuh>
27#include <cuda/iterator>
28#include <cuda/std/functional>
41 if constexpr (std::endian::native == std::endian::little) {
45 for (
size_t i = 0; i <
sizeof(T); ++i) {
46 result =
static_cast<T
>((result << 8U) | (value & 0xffU));
61 size >
static_cast<size_t>(std::numeric_limits<std::streamsize>::max())) {
65 input.read(
static_cast<char*
>(destination),
static_cast<std::streamsize
>(size));
83 if constexpr (std::endian::native != std::endian::little) {
84 for (
auto& item :
items) {
106 if (std::fwrite(source, 1, size,
output) != size) {
114 template <
typename T>
117 return bytes(&item,
sizeof(item));
120 template <
typename T>
122 if constexpr (std::endian::native == std::endian::little) {
125 for (
auto item :
items) {
133template <
typename IO>
143 CUDDL_TRY(io.value(c.blacklist_identity));
167 auto const hardware = std::thread::hardware_concurrency();
168 return hardware == 0U ? 8U : hardware;
193 std::optional<std::reference_wrapper<device_blacklist const>>
blacklist = std::nullopt;
202 std::optional<std::reference_wrapper<device_blacklist const>>
blacklist = std::nullopt;
215 std::filesystem::path
const& directory
217 std::vector<std::filesystem::path> paths;
218 for (
auto const& entry : std::filesystem::recursive_directory_iterator(
219 directory, std::filesystem::directory_options::skip_permission_denied
221 if (!entry.is_symlink() && !entry.is_regular_file())
continue;
222 auto name = entry.path().filename().string();
223 std::ranges::transform(name, name.begin(), [](
unsigned char c) {
224 return static_cast<char>(std::tolower(c));
226 auto extension = std::filesystem::path(name).extension().string();
227 if (extension ==
".gz" || extension ==
".bgz" || extension ==
".bgzf") {
228 extension = std::filesystem::path(name).stem().extension().string();
230 if (extension ==
".fa" || extension ==
".fna" || extension ==
".fasta" ||
231 extension ==
".ffn" || extension ==
".frn" || extension ==
".fq" ||
232 extension ==
".fastq") {
233 paths.push_back(entry.path());
236 std::ranges::sort(paths);
238}
catch (std::filesystem::filesystem_error
const& error) {
245 std::filesystem::path
const& input
247 if (std::filesystem::is_directory(input))
return genome_paths(input);
248 std::ifstream list(input);
250 std::vector<std::filesystem::path> paths;
251 for (std::string line; std::getline(list, line);) {
252 if (!line.empty()) paths.emplace_back(line);
257namespace detail {
struct reference_file_assembly; }
269 static constexpr uint16_t database_file_version = 1U;
281 [[nodiscard]] std::span<std::string const>
names() const noexcept {
285 [[nodiscard]] std::span<uint16_t const>
rows() const noexcept {
298 template <u
int32_t K,
size_t BucketCount,
typename Layout = default_register_layout>
300 std::span<std::filesystem::path const> paths,
301 cuda::stream_ref stream,
308 if (paths.size() > std::numeric_limits<uint32_t>::max() ||
310 std::numeric_limits<size_t>::max() / (BucketCount *
sizeof(uint32_t))) {
313 reference_database_file result;
315 score_compatibility::current<K, BucketCount, Layout>(),
316 static_cast<uint32_t
>(paths.size())
318 CUDDL_TRY((result.set_blacklist<K, BucketCount>(options.blacklist, stream)));
319 result.rows_.resize(paths.size() * BucketCount);
320 result.names_.reserve(paths.size());
322 if (paths.empty())
return result;
323 CUDDL_TRY((detail::stage_paths<K, BucketCount, Layout>(
326 options.staging_bytes,
327 options.parser_workers,
329 options.decompression,
330 options.direct_reads,
332 result.download_rows<BucketCount>(stream),
333 options.blacklist ? options.blacklist->get().view() :
cuda::std::nullopt
335 for (
auto const& path : paths) result.names_.push_back(path.string());
337 static_assert(
sizeof(database_type) > 0);
340 }
catch (cuda::cuda_error
const& error) {
341 return Err(
Error::cuda(
static_cast<cudaError_t
>(error.status())));
342 }
catch (std::system_error
const& error) {
344 }
catch (std::bad_alloc
const& error) {
356 template <u
int32_t K,
size_t BucketCount,
typename Layout = default_register_layout>
358 std::span<sequence_genome const> genomes,
359 cuda::stream_ref stream,
364 if (genomes.size() > std::numeric_limits<uint32_t>::max() ||
366 std::numeric_limits<size_t>::max() / (BucketCount *
sizeof(uint32_t))) {
369 reference_database_file result;
371 score_compatibility::current<K, BucketCount, Layout>(),
372 static_cast<uint32_t
>(genomes.size())
374 CUDDL_TRY((result.set_blacklist<K, BucketCount>(options.blacklist, stream)));
375 result.rows_.resize(genomes.size() * BucketCount);
376 result.names_.reserve(genomes.size());
377 if (genomes.empty())
return result;
378 CUDDL_TRY((detail::stage_sequences<K, BucketCount, Layout>(
381 options.staging_bytes,
383 result.download_rows<BucketCount>(stream),
384 options.blacklist ? options.blacklist->get().view() :
cuda::std::nullopt
386 for (
auto const& genome : genomes) result.names_.emplace_back(genome.name);
387 static_assert(
sizeof(database_type) > 0);
390 }
catch (cuda::cuda_error
const& error) {
391 return Err(
Error::cuda(
static_cast<cudaError_t
>(error.status())));
392 }
catch (std::system_error
const& error) {
394 }
catch (std::bad_alloc
const& error) {
408 template <u
int32_t K,
size_t BucketCount,
typename Layout = default_register_layout>
411 std::span<std::string const>
names,
412 cuda::stream_ref stream,
416 if (store.size() % BucketCount != 0) {
419 size_t const count = store.size() / BucketCount;
420 if (!
names.empty() &&
names.size() != count) {
423 if (count != 0 && store.data() ==
nullptr) {
428 score_compatibility::current<K, BucketCount, Layout>(),
static_cast<uint32_t
>(count)
436 result.rows_.resize(count * BucketCount);
437 if (count == 0)
return result;
438 CUDDL_TRY(detail::download_store<BucketCount>(store, result.rows_, stream));
440 result.names_.assign(
names.begin(),
names.end());
443 }
catch (cuda::cuda_error
const& error) {
444 return Err(
Error::cuda(
static_cast<cudaError_t
>(error.status())));
445 }
catch (std::bad_alloc
const& error) {
450 template <u
int32_t K,
size_t BucketCount,
typename Layout = default_register_layout>
452 cuda::stream_ref stream
454 CUDDL_TRY((detail::validate_indexed_score_compatibility<K, BucketCount, Layout>(
458 CUDDL_CUDA_TRY(cuda::make_device_buffer<uint16_t>(stream, stream.device(), rows_));
462 database.names_ = names_;
464 return std::move(database);
477 writer.
temporary = path.string() +
".tmp.XXXXXX";
478 auto fd = ::mkstemp(writer.
temporary.data());
483 writer.
output = ::fdopen(fd,
"wb");
497 auto blacklist_count =
static_cast<uint32_t
>(blacklist_.
keys().size());
500 auto const prefix = ::ftello(writer.
output);
501 auto const limit =
static_cast<uint64_t
>(std::numeric_limits<off_t>::max());
502 if (prefix < 0 ||
static_cast<uint64_t
>(prefix) > limit -
sizeof(uint32_t)) {
505 uint64_t bytes =
static_cast<uint64_t
>(prefix) +
sizeof(uint32_t);
506 if (rows_.size() > (limit - bytes) /
sizeof(uint16_t)) {
509 bytes += rows_.size() *
sizeof(uint16_t);
510 for (
auto const& name : names_) {
511 if (name.size() > std::numeric_limits<uint32_t>::max()) {
514 if (bytes > limit -
sizeof(uint32_t) ||
515 name.size() > limit - bytes -
sizeof(uint32_t)) {
518 bytes +=
sizeof(uint32_t) + name.size();
522 auto const allocation = ::posix_fallocate(fd, 0,
static_cast<off_t
>(bytes));
523 if (allocation != 0 && allocation != EOPNOTSUPP && allocation != ENOSYS) {
526 "cannot allocate database file: " +
527 std::generic_category().message(allocation)
531 for (
auto const& name : names_) {
537 auto closed = std::fclose(std::exchange(writer.
output,
nullptr));
539 std::filesystem::rename(writer.
temporary, path);
543 }
catch (std::filesystem::filesystem_error
const& error) {
545 }
catch (std::bad_alloc
const& error) {
551 std::filesystem::path
const& path
555 std::ifstream(path, std::ios::binary | std::ios::ate)
557 auto size = reader.
input.tellg();
561 reader.remaining =
static_cast<uint64_t
>(size);
562 reader.input.seekg(0);
564 CUDDL_TRY(reader.bytes(magic,
sizeof(magic)));
565 if (std::string_view(magic,
sizeof(magic)) != std::string_view(
"CUDDLDB\0", 8)) {
570 if (version != database_file_version) {
574 uint32_t blacklist_count = 0;
577 if (c.kmer_length < 1 || c.kmer_length > 31 || c.bucket_count < 2048 ||
578 c.bucket_count > 8192 || !std::has_single_bit(c.bucket_count) ||
579 (c.key_mask != 0xffffU && c.key_mask != 0x7fffU) || c.exponent_bits == 0 ||
580 c.exponent_bits >= 16) {
583 c.indexed_bucket_count = c.bucket_count;
584 c.mantissa_bits = 16U - c.exponent_bits;
585 c.score_encoder_identity = 1U;
586 c.hash_identity = 1U;
588 c.canonicalisation_policy = 1U;
589 c.blacklist_version = blacklist_count == 0 ? 0U : 1U;
590 if (blacklist_count > reader.remaining /
sizeof(uint64_t)) {
593 std::vector<uint64_t> keys(blacklist_count);
595 if (!std::is_sorted(keys.begin(), keys.end()) ||
596 std::adjacent_find(keys.begin(), keys.end()) != keys.end()) {
599 result.blacklist_ =
kmer_blacklist(c.kmer_length, std::move(keys));
600 if (result.blacklist_.
identity() != c.blacklist_identity ||
601 result.blacklist_.
version() != c.blacklist_version) {
605 uint64_t
const words = count * c.bucket_count;
606 uint64_t
const payload_bytes = words *
sizeof(uint16_t);
607 if (words > std::numeric_limits<size_t>::max() /
sizeof(uint16_t) ||
608 payload_bytes + count *
sizeof(uint32_t) +
sizeof(uint32_t) > reader.remaining) {
611 result.names_.reserve(count);
612 for (uint64_t
id = 0;
id < count; ++id) {
615 auto const required =
616 payload_bytes + (count -
id - 1) *
sizeof(uint32_t) +
sizeof(uint32_t);
617 if (required > reader.remaining || length > reader.remaining - required) {
620 std::string name(length,
'\0');
621 CUDDL_TRY(reader.bytes(name.data(), name.size()));
622 result.names_.push_back(std::move(name));
624 if (reader.remaining != payload_bytes +
sizeof(uint32_t)) {
627 result.rows_.resize(words);
629 auto const expected_checksum =
static_cast<uint32_t
>(reader.checksum);
632 if (checksum != expected_checksum) {
637 }
catch (std::invalid_argument
const& error) {
639 }
catch (std::bad_alloc
const& error) {
650 template <
size_t BucketCount>
651 [[nodiscard]]
auto download_rows(cuda::stream_ref stream) {
653 return detail::download_store<BucketCount>(
654 group, std::span{rows_}.subspan(base * BucketCount, count * BucketCount), stream
659 template <u
int32_t K,
size_t BucketCount>
660 Result<void> set_blacklist(
661 std::optional<std::reference_wrapper<device_blacklist const>> filter,
662 cuda::stream_ref stream
664 if (!filter)
return Ok();
665 CUDDL_TRY(filter->get().validate(K, BucketCount, stream));
666 blacklist_ = filter->get().source();
672 kmer_blacklist blacklist_;
673 reference_database_metadata metadata_{};
674 std::vector<std::string> names_;
675 std::vector<uint16_t> rows_;
684template <u
int32_t K,
size_t BucketCount,
typename Layout = default_register_layout>
686 std::span<std::filesystem::path const> paths,
687 cuda::stream_ref stream,
690 return [&]() -> Result<cuda::device_buffer<uint32_t>> {
691 if (options.blacklist)
CUDDL_TRY(options.blacklist->get().validate(K, BucketCount, stream));
693 auto store = cuda::make_device_buffer<uint32_t>(
694 stream, stream.device(), paths.size() * BucketCount, cuda::no_init
697 CUDDL_TRY((detail::stage_paths<K, BucketCount, Layout>(
700 options.staging_bytes,
701 options.parser_workers,
703 options.decompression,
704 options.direct_reads,
706 [&](device_span<uint32_t const> group,
size_t base,
size_t count) -> Result<void> {
711 device_span<uint32_t>{
712 store.data() + base * BucketCount, count * BucketCount
718 options.blacklist ? options.blacklist->get().view() : cuda::std::nullopt
722}
catch (cuda::cuda_error
const& error) {
723 return Err(Error::cuda(
static_cast<cudaError_t
>(error.status())));
724}
catch (std::system_error
const& error) {
725 return Err(Error::resource(error.what()));
726}
catch (std::bad_alloc
const& error) {
727 return Err(Error::resource(error.what()));
Immutable canonical k-mers excluded from construction, independent of sketch size.
uint32_t kmer_length() const noexcept
std::vector< uint64_t > const & keys() const noexcept
uint64_t identity() const noexcept
uint8_t version() const noexcept
Host-owned reference sketches and labels, ready for binary storage or GPU upload.
static Result< reference_database_file > from_store(device_span< uint32_t const > store, std::span< std::string const > names, cuda::stream_ref stream, kmer_blacklist const &blacklist)
Adopts a device store as the host rows this form saves.
kmer_blacklist const & blacklist() const noexcept
Result< reference_database< K, BucketCount, Layout > > upload(cuda::stream_ref stream) const
Uploads reference score rows and labels without constructing an index.
Result< void > save(std::filesystem::path const &path) const
Writes a versioned, little-endian, CRC32-checked binary file.
std::span< std::string const > names() const noexcept
Reference labels in reference-ID order.
std::span< uint16_t const > rows() const noexcept
Winner scores in reference-ID order, one bucket row per genome.
static Result< reference_database_file > build_from_sequences(std::span< sequence_genome const > genomes, cuda::stream_ref stream, sequence_build_options options={})
Builds one sketch per genome from bases the caller already holds.
static Result< reference_database_file > load(std::filesystem::path const &path)
Reads and validates a binary database without requiring a GPU.
reference_database_metadata metadata() const noexcept
Compatibility metadata and reference count.
static Result< reference_database_file > build(std::span< std::filesystem::path const > paths, cuda::stream_ref stream, path_build_options options={})
Builds one sketch per plain or gzip/BGZF FASTA/FASTQ file on the GPU.
Move-only owner of one immutable contiguous reference database.
Index-file persistence and the validation boundary for reference_index.
#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).
T database_file_little_endian(T value)
transfer_mode
How a build gets CPU-parsed sequence bytes to the device.
constexpr uint64_t seed
Domain separation constant folded into every packed k-mer before hashing.
decompression_backend
Where inputs are decompressed; sketch construction always runs on the GPU.
Result< void > database_file_metadata(IO &io, reference_database_metadata &metadata, uint32_t &blacklist_count)
Batch operations over contiguous device rows.
Result< void > Ok() noexcept
Success return for Result<void>; same as return {}.
Result< std::vector< std::filesystem::path > > list_genomes(std::filesystem::path const &input)
Paths from a directory (see genome_paths) or from a text file with one path per line,...
cuda::std::unexpected< Error > Err(Error error)
Failure return; converts to any Result<T> via cuda::std::unexpected.
Result< std::vector< std::filesystem::path > > genome_paths(std::filesystem::path const &directory)
The FASTA/FASTQ files under directory, searched recursively and sorted by path, which is the order th...
unsigned default_parser_workers()
Loaders a path build runs unless the caller asks for another count.
Result< cuda::device_buffer< uint32_t > > build_sketch_store(std::span< std::filesystem::path const > paths, cuda::stream_ref stream, path_build_options options={})
Sketches one genome per plain or gzip/BGZF FASTA/FASTQ file into a device store.
static Error cuda(cudaError_t code, std::source_location location=std::source_location::current())
static Error invalid_argument(std::string message)
static Error resource(std::string message)
Fallible API result: cuda::std::expected<T, Error> with cuDDL factories.
Result< void > value(T &item)
Result< void > bytes(void *destination, size_t size)
Result< void > words(std::vector< T > &items)
Result< void > value(T item)
Result< void > words(std::vector< T > const &items)
Result< void > bytes(void const *source, size_t size)
Builds a reference_database_file from gathered parts; the file's members are private.
One genome as its records, in order. No records keeps the genome's ID with a zero row.
One genome of bases the caller already holds.
A span that is assumed to point to device-accessible memory.
Knobs for a build the caller feeds with FASTA/FASTQ paths.
transfer_mode transfer
How the build moves bytes.
decompression_backend decompression
std::optional< size_t > staging_bytes
Arena bytes.
unsigned parser_workers
Loaders to run.
std::optional< std::reference_wrapper< device_blacklist const > > blacklist
Reusable lookup; keep alive through this synchronous build.
reference_build_statistics * statistics
bool direct_reads
Read the files with O_DIRECT, past the page cache.
How a build moved sequence bytes to the device.
uint64_t blacklist_identity
uint8_t blacklist_version
Knobs for a build the caller feeds with bases it already holds.
reference_build_statistics * statistics
std::optional< std::reference_wrapper< device_blacklist const > > blacklist
std::optional< size_t > staging_bytes
Arena bytes.