cuDDL
Loading...
Searching...
No Matches
blacklist.cuh
Go to the documentation of this file.
1#pragma once
2
3#include <cuda/buffer>
4#include <cuda/std/algorithm>
5#include <cuda/std/optional>
6#include <cuda/stream>
7
8#include <algorithm>
9#include <bit>
10#include <filesystem>
11#include <fstream>
12#include <functional>
13#include <limits>
14#include <optional>
15#include <span>
16#include <stdexcept>
17#include <vector>
18
19#include <cuddl/detail/dna.hpp>
20#include <cuddl/detail/hash.cuh>
21#include <cuddl/error.hpp>
22#include <cuddl/fastx.hpp>
23
24namespace cuddl {
25
26namespace detail {
34 uint64_t const* keys = nullptr;
35 uint32_t const* offsets = nullptr;
36 uint32_t const* presence = nullptr;
37 uint64_t const* fingerprints = nullptr;
38 uint32_t presence_bits = 0;
39
41 __host__ __device__ static uint32_t fingerprint(uint64_t hash) noexcept {
42 return static_cast<uint32_t>(hash >> 13) & 63U;
43 }
44
45 __host__ __device__ static uint32_t presence_slot(uint64_t key, uint32_t bits) noexcept {
46 return static_cast<uint32_t>((key * 0x9E3779B97F4A7C15ULL) >> (64 - bits));
47 }
48
50 __device__ bool contains(uint64_t key, uint64_t hash, size_t bucket) const noexcept {
51 if ((__ldg(fingerprints + bucket) >> fingerprint(hash) & 1U) == 0) return false;
52 auto const slot = presence_slot(key, presence_bits);
53 if ((__ldg(presence + slot / 32) >> (slot % 32) & 1U) == 0) return false;
54 return search(key, bucket);
55 }
56
57 // Present slots are rare; keeping the search out of line spares the caller's registers.
58 __device__ __noinline__ bool search(uint64_t key, size_t bucket) const noexcept {
59 auto first = offsets[bucket];
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;
65 if (value < key) {
66 first = middle + 1;
67 } else {
68 last = middle;
69 }
70 }
71 return false;
72 }
73};
74} // namespace detail
75
78 public:
79 kmer_blacklist() = default;
80
82 kmer_blacklist(uint32_t k, std::vector<uint64_t> keys) : k_(k), keys_(std::move(keys)) {
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");
85 }
86
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");
90 }
91 }
92
93 std::sort(keys_.begin(), keys_.end());
94 keys_.erase(std::unique(keys_.begin(), keys_.end()), keys_.end());
95
96 if (keys_.empty()) {
97 k_ = 0;
98 return;
99 }
100 // Explicit little-endian words make identity independent of host byte order.
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());
105
106 if constexpr (std::endian::native != std::endian::little) {
107 for (auto& word : words) {
108 word = cuda::std::byteswap(word);
109 }
110 }
111
112 identity_ = detail::xxhash64(
113 reinterpret_cast<uint8_t const*>(words.data()),
114 words.size() * sizeof(uint64_t),
115 0x435544444c424c31ULL // CUDDLBL1
116 );
117
118 if (identity_ == 0) identity_ = 1;
119 }
120
123 [[nodiscard]] static Result<kmer_blacklist>
124 load(std::filesystem::path const& path, uint32_t k) try {
125 if (k < 1 || k > 31) return Err(Error::invalid_argument("invalid blacklist k"));
126
127 std::ifstream input(path);
128 if (!input) return Err(Error::resource("cannot open blacklist: " + path.string()));
129
130 input >> std::ws;
131 auto const first = input.peek();
132 if (first == '>' || first == '@' || first == 0x1f) {
133 auto parsed = parse_fasta_file(path.string(), k, 1);
134 if (!parsed) return Err(parsed.error());
135 return kmer_blacklist(k, std::move(parsed->kmers));
136 }
137 input.clear();
138 input.seekg(0);
139 std::vector<uint64_t> keys;
140 std::string line;
141 size_t number = 0;
142
143 while (std::getline(input, line)) {
144 ++number;
145 if (!line.empty() && line.back() == '\r') line.pop_back();
146 if (line.empty()) continue;
147 if (line.size() != k) {
148 return Err(
150 "invalid blacklist length at line " + std::to_string(number)
151 )
152 );
153 }
154 uint64_t word = 0;
155 for (char base : line) {
156 auto const code = detail::encode_base(base);
157 if (code == 0xffU) {
158 return Err(
160 "invalid blacklist base at line " + std::to_string(number)
161 )
162 );
163 }
164 word = (word << 2) | code;
165 }
166 keys.push_back(canonical(word, k));
167 }
168 if (!input.eof()) return Err(Error::resource("cannot read blacklist: " + path.string()));
169 return kmer_blacklist(k, std::move(keys));
170 } catch (std::invalid_argument const& error) {
171 return Err(Error::invalid_argument(error.what()));
172 } catch (std::bad_alloc const& error) {
173 return Err(Error::resource(error.what()));
174 }
175
176 [[nodiscard]] uint32_t kmer_length() const noexcept {
177 return k_;
178 }
179 [[nodiscard]] uint64_t identity() const noexcept {
180 return identity_;
181 }
182 [[nodiscard]] uint8_t version() const noexcept {
183 return keys_.empty() ? 0U : 1U;
184 }
185 [[nodiscard]] std::vector<uint64_t> const& keys() const noexcept {
186 return keys_;
187 }
188
189 static uint64_t canonical(uint64_t word, uint32_t k) noexcept {
190 return std::max(word, detail::reverse_complement(word, k));
191 }
192
193 private:
194 uint32_t k_ = 0;
195 uint64_t identity_ = 0;
196 std::vector<uint64_t> keys_;
197};
198
202 public:
203 device_blacklist(kmer_blacklist source, size_t buckets, cuda::stream_ref stream)
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");
207 }
208
209 if (source_.keys().empty()) return;
210
211 std::vector<uint32_t> offsets(buckets + 1, 0);
212 std::vector<uint64_t> fingerprints(buckets, 0);
213
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);
219 }
220
221 for (size_t b = 1; b <= buckets; ++b) {
222 offsets[b] += offsets[b - 1];
223 }
224
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;
229 }
230 // 32 to 64 map bits per key hold false positives to 1.5-3% at any list size.
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);
237 }
238 presence_bits_ = presence_bits;
239
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)
245 );
246 stream.sync();
247 }
250 device_blacklist(device_blacklist&&) noexcept = default;
251 device_blacklist& operator=(device_blacklist&&) noexcept = default;
252
253 [[nodiscard]] kmer_blacklist const& source() const noexcept {
254 return source_;
255 }
256 [[nodiscard]] cuda::std::optional<detail::blacklist_view> view() const noexcept {
257 if (!keys_) return cuda::std::nullopt;
259 keys_->data(),
260 offsets_->data(),
261 presence_->data(),
262 fingerprints_->data(),
263 presence_bits_
264 };
265 }
266 [[nodiscard]] Result<void> validate(uint32_t k, size_t buckets, cuda::stream_ref stream) const {
267 if ((!source_.keys().empty() && source_.kmer_length() != k) || buckets != buckets_ ||
268 stream.device().get() != device_) {
269 return Err(
270 Error::invalid_argument("blacklist construction configuration or device mismatch")
271 );
272 }
273 return Ok();
274 }
275
276 private:
277 kmer_blacklist source_;
278 size_t buckets_;
279 int device_;
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_;
285};
286
287} // namespace cuddl
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.
Definition blacklist.cuh:77
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.
Definition blacklist.cuh:82
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.
Definition hash.cuh:63
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.
auto const words
Definition top_k.cuh:204
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.
Definition dna.hpp:16
auto *const bits
Definition kernels.cuh:1184
Batch operations over contiguous device rows.
Definition a48.hpp:14
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.
Definition fastx.hpp:34
Result< void > Ok() noexcept
Success return for Result<void>; same as return {}.
Definition error.hpp:181
cuda::std::unexpected< Error > Err(Error error)
Failure return; converts to any Result<T> via cuda::std::unexpected.
Definition error.hpp:176
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
uint64_t const * fingerprints
Definition blacklist.cuh:37
__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.
Definition blacklist.cuh:41
__device__ bool contains(uint64_t key, uint64_t hash, size_t bucket) const noexcept
hash must be hash_kmer(key); construction already holds it.
Definition blacklist.cuh:50
__device__ __noinline__ bool search(uint64_t key, size_t bucket) const noexcept
Definition blacklist.cuh:58
__host__ static __device__ uint32_t presence_slot(uint64_t key, uint32_t bits) noexcept
Definition blacklist.cuh:45