4#include <cuda/std/algorithm>
5#include <cuda/std/optional>
34 uint64_t
const*
keys =
nullptr;
41 __host__ __device__
static uint32_t
fingerprint(uint64_t hash)
noexcept {
42 return static_cast<uint32_t
>(hash >> 13) & 63U;
46 return static_cast<uint32_t
>((key * 0x9E3779B97F4A7C15ULL) >> (64 -
bits));
50 __device__
bool contains(uint64_t key, uint64_t hash,
size_t bucket)
const noexcept {
53 if ((__ldg(
presence + slot / 32) >> (slot % 32) & 1U) == 0)
return false;
54 return search(key, bucket);
58 __device__ __noinline__
bool search(uint64_t key,
size_t bucket)
const noexcept {
60 auto last =
offsets[bucket + 1];
61 while (first < last) {
62 auto const middle = first + (last - first) / 2;
63 auto const value = __ldg(
keys + middle);
64 if (value == key)
return true;
83 if (k < 1 || k > 31 || keys_.size() > std::numeric_limits<uint32_t>::max()) {
84 throw std::invalid_argument(
"invalid blacklist k or entry count");
87 for (
auto key : keys_) {
88 if ((key >> (2 * k)) != 0 ||
canonical(key, k) != key) {
89 throw std::invalid_argument(
"blacklist words must be canonical packed k-mers");
93 std::sort(keys_.begin(), keys_.end());
94 keys_.erase(std::unique(keys_.begin(), keys_.end()), keys_.end());
101 std::vector<uint64_t> words;
102 words.reserve(keys_.size() + 3);
103 words.insert(words.end(), {uint64_t{1}, k, keys_.size()});
104 words.insert(words.end(), keys_.begin(), keys_.end());
106 if constexpr (std::endian::native != std::endian::little) {
107 for (
auto& word : words) {
108 word = cuda::std::byteswap(word);
113 reinterpret_cast<uint8_t const*
>(
words.data()),
114 words.size() *
sizeof(uint64_t),
115 0x435544444c424c31ULL
118 if (identity_ == 0) identity_ = 1;
123 [[nodiscard]]
static Result<kmer_blacklist>
124 load(std::filesystem::path
const& path, uint32_t k)
try {
127 std::ifstream input(path);
131 auto const first = input.peek();
132 if (first ==
'>' || first ==
'@' || first == 0x1f) {
134 if (!parsed)
return Err(parsed.error());
139 std::vector<uint64_t> keys;
143 while (std::getline(input, line)) {
145 if (!line.empty() && line.back() ==
'\r') line.pop_back();
146 if (line.empty())
continue;
147 if (line.size() != k) {
150 "invalid blacklist length at line " + std::to_string(number)
155 for (
char base : line) {
160 "invalid blacklist base at line " + std::to_string(number)
164 word = (word << 2) | code;
166 keys.push_back(canonical(word, k));
168 if (!input.eof())
return Err(
Error::resource(
"cannot read blacklist: " + path.string()));
170 }
catch (std::invalid_argument
const& error) {
172 }
catch (std::bad_alloc
const& error) {
182 [[nodiscard]] uint8_t
version() const noexcept {
183 return keys_.empty() ? 0U : 1U;
185 [[nodiscard]] std::vector<uint64_t>
const&
keys() const noexcept {
189 static uint64_t
canonical(uint64_t word, uint32_t k)
noexcept {
195 uint64_t identity_ = 0;
196 std::vector<uint64_t> keys_;
204 : source_(std::move(source)), buckets_(buckets), device_(stream.device().get()) {
205 if (buckets < 2048 || buckets > 8192 || !std::has_single_bit(buckets)) {
206 throw std::invalid_argument(
"invalid blacklist bucket count");
209 if (source_.keys().empty())
return;
211 std::vector<uint32_t> offsets(buckets + 1, 0);
212 std::vector<uint64_t> fingerprints(buckets, 0);
214 for (
auto key : source_.keys()) {
215 auto const hash = detail::hash_kmer(key);
216 auto const bucket = hash & (buckets - 1);
217 ++offsets[bucket + 1];
218 fingerprints[bucket] |= uint64_t{1} << detail::blacklist_view::fingerprint(hash);
221 for (
size_t b = 1; b <= buckets; ++b) {
222 offsets[b] += offsets[b - 1];
225 auto cursor = offsets;
226 std::vector<uint64_t> keys(source_.keys().size());
227 for (
auto key : source_.keys()) {
228 keys[cursor[detail::hash_kmer(key) & (buckets - 1)]++] = key;
231 auto const presence_bits =
232 std::max(10U,
static_cast<uint32_t
>(std::bit_width(keys.size())) + 5U);
233 std::vector<uint32_t> presence((
size_t{1} << presence_bits) / 32);
234 for (
auto key : keys) {
235 auto const slot = detail::blacklist_view::presence_slot(key, presence_bits);
236 presence[slot / 32] |= uint32_t{1} << (slot % 32);
238 presence_bits_ = presence_bits;
240 keys_.emplace(cuda::make_device_buffer<uint64_t>(stream, stream.device(), keys));
241 offsets_.emplace(cuda::make_device_buffer<uint32_t>(stream, stream.device(), offsets));
242 presence_.emplace(cuda::make_device_buffer<uint32_t>(stream, stream.device(), presence));
243 fingerprints_.emplace(
244 cuda::make_device_buffer<uint64_t>(stream, stream.device(), fingerprints)
256 [[nodiscard]] cuda::std::optional<detail::blacklist_view>
view() const noexcept {
257 if (!keys_)
return cuda::std::nullopt;
262 fingerprints_->data(),
267 if ((!source_.keys().empty() && source_.kmer_length() != k) || buckets != buckets_ ||
268 stream.device().get() != device_) {
270 Error::invalid_argument(
"blacklist construction configuration or device mismatch")
280 uint32_t presence_bits_ = 0;
281 std::optional<cuda::device_buffer<uint64_t>> keys_;
282 std::optional<cuda::device_buffer<uint32_t>> offsets_;
283 std::optional<cuda::device_buffer<uint32_t>> presence_;
284 std::optional<cuda::device_buffer<uint64_t>> fingerprints_;
Reusable, move-only device lookup.
Result< void > validate(uint32_t k, size_t buckets, cuda::stream_ref stream) const
device_blacklist & operator=(device_blacklist const &)=delete
device_blacklist(kmer_blacklist source, size_t buckets, cuda::stream_ref stream)
cuda::std::optional< detail::blacklist_view > view() const noexcept
device_blacklist(device_blacklist &&) noexcept=default
device_blacklist(device_blacklist const &)=delete
Immutable canonical k-mers excluded from construction, independent of sketch size.
uint32_t kmer_length() const noexcept
static uint64_t canonical(uint64_t word, uint32_t k) noexcept
std::vector< uint64_t > const & keys() const noexcept
uint64_t identity() const noexcept
kmer_blacklist(uint32_t k, std::vector< uint64_t > keys)
Validates packed canonical words and normalizes their order and duplicates.
uint8_t version() const noexcept
static Result< kmer_blacklist > load(std::filesystem::path const &path, uint32_t k)
Reads BBTools DDL FASTA/FASTQ, including gzip and fused records, or exact DNA lines.
__host__ __device__ uint64_t xxhash64(const uint8_t *bytes, uint64_t size, uint64_t seed)
Computes xxHash-64 over a byte range without allocating or copying it.
constexpr uint64_t reverse_complement(uint64_t packed, uint32_t k) noexcept
Reverse-complements a packed k-mer (2 bits per base) with bit-parallel reversal.
constexpr uint8_t encode_base(char base) noexcept
Maps one DNA byte to its 2-bit symbol (A=0, C=1, T=2, G=3) or 0xff when invalid.
Batch operations over contiguous device rows.
Result< fasta_parse_result > parse_fasta_file(std::string const &path, uint32_t k, unsigned threads=std::thread::hardware_concurrency())
Parses a FASTA/FASTQ file's sequence bases into packed canonical k-mers of length k.
Result< void > Ok() noexcept
Success return for Result<void>; same as return {}.
cuda::std::unexpected< Error > Err(Error error)
Failure return; converts to any Result<T> via cuda::std::unexpected.
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.
uint64_t const * fingerprints
__host__ static __device__ uint32_t fingerprint(uint64_t hash) noexcept
Bits 13-18 sit above every supported bucket index and below the scored high word.
__device__ bool contains(uint64_t key, uint64_t hash, size_t bucket) const noexcept
hash must be hash_kmer(key); construction already holds it.
__device__ __noinline__ bool search(uint64_t key, size_t bucket) const noexcept
uint32_t const * presence
__host__ static __device__ uint32_t presence_slot(uint64_t key, uint32_t bits) noexcept