cuDDL
Loading...
Searching...
No Matches
sequence_encode.cuh
Go to the documentation of this file.
1#pragma once
2
3#include <cuda_runtime.h>
4#include <cuda/atomic>
5#include <cuda/devices>
6#include <cuda/functional>
7#include <cuda/std/algorithm>
8#include <cuda/std/bit>
9#include <cuda/std/cstdint>
10#include <cuda/stream>
11
12#include <cstdint>
13#include <limits>
14
15#include <cub/block/block_reduce.cuh>
16#include <cuddl/blacklist.cuh>
17#include <cuddl/detail/dna.hpp>
18#include <cuddl/detail/hash.cuh>
20#include <cuddl/device_span.cuh>
21#include <cuddl/error.hpp>
22
23namespace cuddl::detail {
24
29__device__ __forceinline__ uint32_t load_shared_relaxed(uint32_t const* address) {
30 uint32_t value;
31 asm volatile("ld.relaxed.cta.shared.u32 %0, [%1];"
32 : "=r"(value)
33 : "r"(static_cast<uint32_t>(__cvta_generic_to_shared(address)))
34 : "memory");
35 return value;
36}
37
43template <size_t BucketCount, typename Layout, bool HasBlacklist = false, bool Naive = false>
44__device__ __forceinline__ void add_sequence_windows(
45 char const* sequence,
46 uint32_t windows,
47 size_t first_tile,
48 size_t tile_stride,
49 uint32_t k,
50 uint32_t* registers,
51 bool packed_input = false,
53) {
54 constexpr uint32_t tile_size = 256 * 8;
55 __shared__ uint32_t cells[tile_size / 8 + 4];
56 __shared__ uint32_t local[BucketCount];
57 for (uint32_t i = threadIdx.x; i < BucketCount; i += blockDim.x) {
58 local[i] = 0;
59 }
60 uint32_t hash_ceiling = UINT32_MAX;
61 size_t tiles = 0;
62 auto const mask = (uint64_t{1} << (2 * k)) - 1;
63 auto const valid_mask = (uint32_t{1} << k) - 1;
64 for (size_t tile = first_tile; tile < windows; tile += tile_stride) {
65 auto const count = cuda::std::min(tile_size, windows - static_cast<uint32_t>(tile));
66 if constexpr (Naive) {
67 // The tiled path's cell fill ends with the barrier that orders the zeroing of
68 // @c local before its first update; this path has no fill, so it waits here.
70 auto const start = threadIdx.x * 8;
71 auto const* const words = reinterpret_cast<uint32_t const*>(sequence);
72 for (uint32_t i = start; i < cuda::std::min(start + 8U, count); ++i) {
73 uint64_t forward = 0, reverse = 0;
74 bool valid = true;
75 for (uint32_t j = 0; valid && j < k; ++j) {
76 auto const at = tile + i + j;
77 // Codes as the tiled path assigns them: A 0, C 1, T 2, G 3; complement ^ 2.
78 uint32_t code = 0;
79 if (packed_input) {
80 code = (words[at / 16] >> (2 * (at % 16))) & 3U;
81 } else {
82 switch (sequence[at]) {
83 case 'A':
84 case 'a':
85 code = 0;
86 break;
87 case 'C':
88 case 'c':
89 code = 1;
90 break;
91 case 'T':
92 case 't':
93 code = 2;
94 break;
95 case 'G':
96 case 'g':
97 code = 3;
98 break;
99 default:
100 valid = false;
101 }
102 }
103 forward = (forward << 2) | code;
104 reverse |= uint64_t{code ^ 2U} << (2 * j);
105 }
106 if (!valid) continue;
107 auto const word = forward > reverse ? forward : reverse;
108 auto const hash = hash_kmer(word);
109 if (static_cast<uint32_t>(hash >> 32) > hash_ceiling) continue;
110 auto const incoming = score<Layout>(hash);
111 auto const bucket = bucket_of<BucketCount>(hash);
112 if constexpr (HasBlacklist) {
113 if (incoming > load_shared_relaxed(&local[bucket]) &&
114 !blacklist.contains(word, hash, bucket)) {
115 atomicMax(&local[bucket], incoming);
116 }
117 } else {
118 atomicMax(&local[bucket], incoming);
119 }
120 }
121 } else {
122 // Four extra cells cover the k-1 halo, including the last partial cell.
123 auto const padded = (count + 7) / 8 + 4;
124 if (packed_input) {
125 // Two-bit words hold sixteen bases, base j in bits 2j, and no ambiguous base. Tiles
126 // start on a multiple of sixteen bases, so a tile's cells are whole halves of
127 // words.
128 auto const* const words = reinterpret_cast<uint32_t const*>(sequence);
129 auto const total = (static_cast<size_t>(windows) + k - 1 + 7) / 8;
130 for (uint32_t cell = threadIdx.x; cell < padded; cell += blockDim.x) {
131 auto const index = tile / 8 + cell;
132 if (index >= total) {
133 cells[cell] = missing_cell;
134 continue;
135 }
136 // A cell holds its first base in its highest bits: reverse the codes' order.
137 auto const reversed = __brev((words[index / 2] >> (16 * (index % 2))) << 16);
138 cells[cell] = ((reversed & 0x5555U) << 1) | ((reversed >> 1) & 0x5555U);
139 }
140 }
141 for (uint32_t cell = threadIdx.x; !packed_input && cell < padded; cell += blockDim.x) {
142 auto const pos = cell * 8;
143 uint32_t ascii[2] = {};
144 if ((reinterpret_cast<uintptr_t>(sequence + tile) & 7U) == 0 &&
145 pos + 7 < count + k - 1) {
146 auto const value = *reinterpret_cast<uint2 const*>(sequence + tile + pos);
147 ascii[0] = value.x;
148 ascii[1] = value.y;
149 } else {
150 _Pragma("unroll")
151 for (uint32_t j = 0; j < 8; ++j) {
152 auto const byte = pos + j < count + k - 1
153 ? static_cast<uint8_t>(sequence[tile + pos + j])
154 : uint8_t{0xFF};
155 ascii[j / 4] |= static_cast<uint32_t>(byte) << (8 * (j % 4));
156 }
157 }
158 uint32_t packed = 0, bad = 0;
159 _Pragma("unroll")
160 for (uint32_t j = 0; j < 2; ++j) {
161 // Only the T code differs in ASCII bits 0 and 4.
162 auto const thymine = (ascii[j] & ~(ascii[j] << 1) & 0x04040404U) >> 2;
163 auto const normalised = (ascii[j] ^ (thymine * 0x11U)) & 0xD9D9D9D9U;
164 auto const bad_bytes = __vsetne4(normalised, 0x41414141U);
165 auto const codes = (ascii[j] >> 1) & 0x03030303U;
166 // Gather four codes in reverse byte order and four invalid flags in byte order.
167 packed = __dp4a(codes, 0x01041040U, packed << 8);
168 bad = __dp4a(bad_bytes, 0x08040201U << (4 * j), bad);
169 }
170 cells[cell] = packed | (bad << 16);
171 }
173 auto const start = threadIdx.x * 8;
174 if (start < count) {
175 auto const cell = start / 8;
176 auto const a = cells[cell], b = cells[cell + 1];
177 auto const c = cells[cell + 2], d = cells[cell + 3];
178 auto high =
179 (uint64_t{a} << 48) | (uint64_t{b & 0xFFFFU} << 32) | (c << 16) | (d & 0xFFFFU);
180 uint64_t low = 0;
181 auto bad = uint64_t{a >> 16} | (uint64_t{b >> 16} << 8) |
182 (uint64_t{c >> 16} << 16) | (uint64_t{d >> 16} << 24);
183 if (k > 25) {
184 auto const e = cells[cell + 4];
185 low = uint64_t{e & 0xFFFFU} << 48;
186 bad |= uint64_t{e >> 16} << 32;
187 }
188 // Reverse the span once, then slide both strands across its eight windows.
189 auto reverse_word = [](uint64_t word) {
190 auto const bits = cuda::std::bit_reverse(word);
191 return (((bits & 0xAAAAAAAAAAAAAAAAULL) >> 1) |
192 ((bits & 0x5555555555555555ULL) << 1)) ^
193 0xAAAAAAAAAAAAAAAAULL;
194 };
195 auto reverse_high = reverse_word(high);
196 auto reverse_low = reverse_word(low);
197 // An invalid base in the common overlap excludes all eight windows.
198 auto const end = (bad & valid_mask & ~uint64_t{0x7F}) == 0
199 ? cuda::std::min(8U, count - start)
200 : 0U;
201 // Specialising on k > 25 keeps the slide branch-free; the compiler does not
202 // unswitch it once the blacklist lookup is present.
203 auto slide = [&]<bool Long>() {
204 _Pragma("unroll")
205 for (uint32_t i = 0; i < end; ++i) {
206 if ((bad & valid_mask) == 0) {
207 auto const forward = high >> (64 - 2 * k);
208 auto const reverse = reverse_high & mask;
209 auto const word = forward > reverse ? forward : reverse;
210 auto const hash = hash_kmer(word);
211 if (static_cast<uint32_t>(hash >> 32) <= hash_ceiling) {
212 auto const incoming = score<Layout>(hash);
213 auto const bucket = bucket_of<BucketCount>(hash);
214 // Look up only scores that can still raise the CTA's register.
215 if constexpr (HasBlacklist) {
216 if (incoming > load_shared_relaxed(&local[bucket]) &&
217 !blacklist.contains(word, hash, bucket)) {
218 atomicMax(&local[bucket], incoming);
219 }
220 } else {
221 atomicMax(&local[bucket], incoming);
222 }
223 }
224 }
225 // Eight overlapping windows fit in one 32-base word when k <= 25.
226 if constexpr (Long) {
227 high = (high << 2) | (low >> 62);
228 low <<= 2;
229 reverse_high = (reverse_high >> 2) | (reverse_low << 62);
230 reverse_low >>= 2;
231 } else {
232 high <<= 2;
233 reverse_high >>= 2;
234 }
235 bad >>= 1;
236 }
237 };
238 if (k > 25) {
239 slide.template operator()<true>();
240 } else {
241 slide.template operator()<false>();
242 }
243 }
244 }
246 // Share accepted winners across CTAs before processing the remaining tiles.
247 if ((++tiles == 4 || tiles % 16 == 0) && tile + tile_stride < windows) {
248 using reduce = cub::BlockReduce<uint32_t, 256>;
249 __shared__ typename reduce::TempStorage exchange_scratch;
250 __shared__ uint32_t exchange_ceiling;
251 uint32_t minimum = 0xffffU;
252 // Each thread owns its buckets here, so the merged value feeds the floor directly.
253 for (uint32_t b = threadIdx.x; b < BucketCount; b += blockDim.x) {
254 auto const value = local[b];
255 auto global =
256 cuda::atomic_ref<uint32_t, cuda::thread_scope_device>{registers[b]}.load(
257 cuda::memory_order_relaxed
258 );
259 if (value > global) global = cuda::std::max(value, atomicMax(&registers[b], value));
260 local[b] = global;
261 minimum = cuda::std::min(minimum, global);
262 }
263 auto const result = reduce(exchange_scratch).Reduce(minimum, cuda::minimum<>{});
264 if (threadIdx.x == 0) {
265 // An inclusive high-word cutoff admits ties for exact per-bucket pruning.
266 exchange_ceiling =
267 result == 0 ? UINT32_MAX : uint32_t(restore<Layout>(result) >> 32);
268 }
270 hash_ceiling = exchange_ceiling;
271 }
272 }
274 for (uint32_t i = threadIdx.x; i < BucketCount; i += blockDim.x) {
275 if (local[i] != 0U) atomicMax(&registers[i], local[i]);
276 }
277}
278
284 char const* bases;
285 size_t block_end;
286 uint32_t genome;
287 uint32_t windows;
288 uint32_t packed; // nonzero: @p bases holds two-bit words, not ASCII
289};
290
291// On sm_90 the unfiltered encoder fits in 32 registers and the cap keeps the filtered one at
292// full occupancy. Other architectures showed no gain (sm_120) or need more registers (sm_8x).
293#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ == 900
294 #define CUDDL_SEQUENCE_BATCH_REGISTER_CAP __maxnreg__(32)
295#else
296 #define CUDDL_SEQUENCE_BATCH_REGISTER_CAP
297#endif
298
303template <size_t BucketCount, typename Layout, bool HasBlacklist = false, bool Naive = false>
305 sequence_batch_chunk const* chunks,
306 size_t chunk_count,
307 size_t block_count,
308 uint32_t k,
309 uint32_t* registers,
311) {
312 __shared__ sequence_batch_chunk chunk;
313 __shared__ size_t first_block;
314 for (size_t block = blockIdx.x; block < block_count; block += gridDim.x) {
315 if (threadIdx.x == 0) {
316 size_t low = 0, high = chunk_count;
317 while (low < high) {
318 auto const middle = low + (high - low) / 2;
319 if (chunks[middle].block_end <= block) {
320 low = middle + 1;
321 } else {
322 high = middle;
323 }
324 }
325 chunk = chunks[low];
326 first_block = low ? chunks[low - 1].block_end : 0;
327 }
329 auto* target = registers + size_t{chunk.genome} * BucketCount;
330 add_sequence_windows<BucketCount, Layout, HasBlacklist, Naive>(
331 chunk.bases,
332 chunk.windows,
333 (block - first_block) * 2048,
334 (chunk.block_end - first_block) * 2048,
335 k,
336 target,
337 chunk.packed != 0,
339 );
341 }
342}
343
344#undef CUDDL_SEQUENCE_BATCH_REGISTER_CAP
345
349template <size_t BucketCount, typename Layout, bool HasBlacklist = false>
351 char const* sequence,
352 uint32_t windows,
353 uint32_t k,
354 uint32_t* registers,
356) {
357 add_sequence_windows<BucketCount, Layout, HasBlacklist>(
358 sequence,
359 windows,
360 size_t{blockIdx.x} * 2048,
361 size_t{gridDim.x} * 2048,
362 k,
363 registers,
364 false,
366 );
367}
368
374template <size_t BucketCount, typename Layout = default_register_layout>
377 uint32_t k,
379 cuda::stream_ref stream,
380 cuda::std::optional<blacklist_view> blacklist = cuda::std::nullopt
381) {
382 if (k < 1 || k > 31) {
383 return Err(Error::invalid_argument("invalid k-mer length for sequence add"));
384 }
385 size_t const size = sequence.size();
386 if (size < k) {
387 return Ok();
388 }
389 size_t const windows_size = size - k + 1;
390 if (windows_size > std::numeric_limits<uint32_t>::max()) {
391 return Err(Error::invalid_argument("sequence chunk exceeds 32-bit window capacity"));
392 }
393 auto const windows = static_cast<uint32_t>(windows_size);
394 auto launch = [&]<bool Filter>() -> Result<void> {
395 return cuda_try([&] {
396 auto const kernel = add_sequence_single_kernel<BucketCount, Layout, Filter>;
397 auto const multiprocessors =
398 stream.device().attribute(cuda::device_attributes::multiprocessor_count);
399 // Fill one resident wave, accounting for the GPU and this sketch's shared memory.
400 if constexpr (Filter) {
401 // Leave L1 room for the blacklist presence map; the default favors shared memory.
402 auto const carveout = cudaFuncSetAttribute(
403 kernel, cudaFuncAttributePreferredSharedMemoryCarveout, 50
404 );
405 if (carveout != cudaSuccess) return carveout;
406 }
407 int blocks_per_sm = 0;
408 auto const occupancy =
409 cudaOccupancyMaxActiveBlocksPerMultiprocessor(&blocks_per_sm, kernel, 256, 0);
410 if (occupancy != cudaSuccess) return occupancy;
411 size_t const need = (static_cast<size_t>(windows) + 2047) / 2048;
412 size_t const capacity = static_cast<size_t>(multiprocessors) * blocks_per_sm;
413 size_t const blocks_size =
414 capacity == 0 ? size_t{1} : (capacity < need ? capacity : need);
415 auto const blocks = static_cast<uint32_t>(blocks_size);
416 kernel<<<blocks, 256, 0, stream.get()>>>(
417 sequence.data(), windows, k, registers.data(), blacklist.value_or(blacklist_view{})
418 );
419 return cudaGetLastError();
420 });
421 };
422 return blacklist ? launch.template operator()<true>() : launch.template operator()<false>();
423}
424
425} // namespace cuddl::detail
_Pragma("unroll") for(uint32_t bit=0U
auto const high
Definition kernels.cuh:940
auto const block
Definition top_k.cuh:236
auto const cells
Definition kernels.cuh:1878
constexpr uint32_t missing_cell
Eight ambiguous bases: an encoder cell past the end of a sequence.
Definition dna.hpp:27
__global__ void add_sequence_batch_kernel(sequence_batch_chunk const *chunks, size_t chunk_count, size_t block_count, uint32_t k, uint32_t *registers, blacklist_view blacklist={})
Batch kernel covering every staged chunk in one launch, including short records.
auto const count
Definition kernels.cuh:1031
__global__ void add_sequence_single_kernel(char const *sequence, uint32_t windows, uint32_t k, uint32_t *registers, blacklist_view blacklist={})
Single-sequence kernel for the public raw-ASCII API.
__global__ size_t uint32_t bool blacklist_view blacklist
Definition kernels.cuh:96
__global__ size_t uint32_t const uint32_t const uint32_t uint32_t uint16_t uint32_t tile
Definition kernels.cuh:922
__device__ __forceinline__ uint32_t load_shared_relaxed(uint32_t const *address)
Relaxed CTA-scope load of a shared word that other threads update atomically.
__host__ Result< void > launch_sequence_add(device_span< char const > sequence, uint32_t k, device_span< uint32_t > registers, cuda::stream_ref stream, cuda::std::optional< blacklist_view > blacklist=cuda::std::nullopt)
Shared launch for one device-resident ASCII chunk.
unsigned long long local
Definition kernels.cuh:478
auto const total
Definition kernels.cuh:443
__global__ size_t uint32_t * registers
Definition kernels.cuh:94
__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
__host__ __device__ constexpr uint64_t hash_kmer(uint64_t packed_kmer) noexcept
Hashes a canonical packed k-mer into the value space used by DDL registers.
Definition hash.cuh:21
auto const words
Definition top_k.cuh:204
__global__ __launch_bounds__(shared_construction_block_size) void add_shared_kernel(uint64_t const *input
Constructs a sketch through a CTA-local shared-memory register array.
__device__ __forceinline__ void add_sequence_windows(char const *sequence, uint32_t windows, size_t first_tile, size_t tile_stride, uint32_t k, uint32_t *registers, bool packed_input=false, blacklist_view blacklist={})
Encodes raw ASCII windows into registers using shared overlapping tiles.
for(auto i=static_cast< size_t >(blockIdx.x) *block_size+threadIdx.x;i< total;i+=static_cast< size_t >(gridDim.x) *block_size)
Definition kernels.cuh:444
auto *const bits
Definition kernels.cuh:1184
auto const tiles
Definition kernels.cuh:937
auto const word
Definition top_k.cuh:241
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
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
#define CUDDL_SEQUENCE_BATCH_REGISTER_CAP
static Error invalid_argument(std::string message)
Definition error.hpp:99
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
__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
One piece of sequence of one genome, and where its bytes are.
A span that is assumed to point to device-accessible memory.