cuDDL
Loading...
Searching...
No Matches
device_gzip.cuh
Go to the documentation of this file.
1#pragma once
2
3#include <cuda.h>
4#include <cudaTypedefs.h>
5#include <nvcomp/deflate.h>
6#include <nvcomp/gzip.h>
7
8#include <algorithm>
9#include <array>
10#include <atomic>
11#include <bit>
12#include <condition_variable>
13#include <cstdint>
14#include <cstring>
15#include <filesystem>
16#include <functional>
17#include <limits>
18#include <mutex>
19#include <optional>
20#include <span>
21#include <stdexcept>
22#include <string>
23#include <string_view>
24#include <thread>
25#include <vector>
26
27#include <fcntl.h>
28#include <sys/stat.h>
29#include <unistd.h>
30
31#include <cub/block/block_reduce.cuh>
32#include <cub/device/device_copy.cuh>
33#include <cub/device/device_select.cuh>
34#include <cuda/algorithm>
35#include <cuda/buffer>
36#include <cuda/iterator>
37#include <cuda/memory_pool>
38#include <cuda/stream>
39
40#if defined(__x86_64__) || defined(_M_X64)
41 #include <immintrin.h>
42#elif defined(__aarch64__) || defined(_M_ARM64)
43 #include <arm_neon.h>
44#endif
45
49#include <cuddl/device_span.cuh>
50#include <cuddl/error.hpp>
51
52namespace cuddl::detail {
53
57 uint64_t compressed = 0; // file size
58 // Inflated size and CRC32 of the whole file. For one member, its trailer's ISIZE and CRC32;
59 // for BGZF, the sum and combination over its blocks. A plain file's size, without a CRC.
60 uint32_t isize = 0;
61 uint32_t crc = 0;
62 uint32_t members = 0; // gzip members the device inflates: 1, or BGZF's non-empty blocks
63 bool device = false; // a candidate the device may inflate, or plain FASTA
64 bool plain = false; // uncompressed FASTA, which the device normalises as read
65 bool bgzf = false; // blocked gzip, with each block's size in its header
66
69 [[nodiscard]] uint64_t host_bytes() const noexcept {
70 return plain ? uint64_t{isize} + 1 : compressed;
71 }
72};
73
76[[nodiscard]] inline uint32_t bgzf_block_size(unsigned char const* header) noexcept {
77 unsigned char const expected[16] = {0x1f, 0x8b, 8, 4, 0, 0, 0, 0, 0, 0, 6, 0, 'B', 'C', 2, 0};
78 for (size_t i = 0; i < sizeof(expected); ++i) {
79 // Bytes 4-9 are MTIME, XFL and OS, which may hold anything.
80 if ((i < 4 || i > 9) && header[i] != expected[i]) return 0;
81 }
82 return (uint32_t{header[16]} | uint32_t{header[17]} << 8U) + 1U;
83}
84
87[[nodiscard]] inline uint32_t crc32_combine(uint32_t a, uint32_t b, uint64_t length) noexcept {
88 constexpr uint32_t polynomial = 0xedb88320U;
89 auto const multiply = [](uint32_t x, uint32_t y) {
90 uint32_t product = 0;
91 for (uint32_t bit = 1U << 31U; bit != 0; bit >>= 1U) {
92 if ((x & bit) != 0) product ^= y;
93 y = (y & 1U) != 0 ? (y >> 1U) ^ polynomial : y >> 1U;
94 }
95 return product;
96 };
97 // power = x^(2^k) for bit k of 8 * length; x^1 is 1 << 30 in reflected order.
98 uint32_t shift = 1U << 31U, power = 1U << 30U;
99 for (uint64_t bits = length * 8; bits != 0; bits >>= 1U) {
100 if ((bits & 1U) != 0) shift = multiply(power, shift);
101 power = multiply(power, power);
102 }
103 return multiply(shift, a) ^ b;
104}
105
109template <typename Block>
110[[nodiscard]] std::optional<std::pair<uint32_t, uint64_t>>
111walk_bgzf(unsigned char const* bytes, uint64_t size, Block&& block) {
112 uint64_t at = 0, total = 0;
113 uint32_t count = 0;
114 while (at < size) {
115 auto const block_size = size - at >= 18 ? bgzf_block_size(bytes + at) : 0;
116 if (block_size < 26 || block_size > size - at) return std::nullopt;
117 uint32_t inflated = 0;
118 std::memcpy(&inflated, bytes + at + block_size - 4, 4);
119 if (inflated != 0) {
120 block(at + 18, uint64_t{block_size} - 26, inflated);
121 total += inflated;
122 ++count;
123 }
124 at += block_size;
125 }
126 return std::pair{count, total};
127}
128
135[[nodiscard]] inline gzip_file_probe probe_gzip_file(std::filesystem::path const& path) {
136 gzip_file_probe probe;
137 int const fd = ::open(path.c_str(), O_RDONLY);
138 if (fd == -1) return probe;
139 struct stat status{};
140 unsigned char header[18] = {};
141 unsigned char trailer[8 + 18] = {}; // a member's trailer, and the next BGZF header
142 if (::fstat(fd, &status) == 0 && status.st_size >= 4 &&
143 ::pread(fd, header, sizeof(header), 0) >= 4) {
144 auto const size = static_cast<uint64_t>(status.st_size);
145 probe.compressed = size;
146 auto name = path.filename().string();
147 std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) {
148 return static_cast<char>(std::tolower(c));
149 });
150 auto const fastq =
151 name.find(".fq") != std::string::npos || name.find(".fastq") != std::string::npos;
152 if (header[0] == '>') {
153 // Slot offsets count a plain file's bytes and its guard byte in 32 bits.
154 probe.plain = size < std::numeric_limits<uint32_t>::max();
155 probe.isize = static_cast<uint32_t>(probe.plain ? size : 0);
156 probe.device = probe.plain && !fastq;
157 } else if (size >= 26 && bgzf_block_size(header) != 0) {
158 // One read per block: its trailer, and the header of the block after it.
159 uint64_t at = 0, total = 0;
160 uint32_t crc = 0, block_size = bgzf_block_size(header);
161 bool tiled = false;
162 while (block_size >= 26 && block_size <= size - at) {
163 auto const next = at + block_size;
164 auto const wanted = next == size ? 8 : std::min<uint64_t>(26, size - next + 8);
165 if (::pread(fd, trailer, wanted, static_cast<off_t>(next - 8)) !=
166 static_cast<ssize_t>(wanted)) {
167 break;
168 }
169 uint32_t block_crc = 0, inflated = 0;
170 std::memcpy(&block_crc, trailer, 4);
171 std::memcpy(&inflated, trailer + 4, 4);
172 if (inflated != 0) {
173 crc = crc32_combine(crc, block_crc, inflated);
174 total += inflated;
175 ++probe.members;
176 }
177 if (next == size) {
178 tiled = true;
179 break;
180 }
181 block_size = wanted == 26 ? bgzf_block_size(trailer + 8) : 0;
182 at = next;
183 }
184 probe.bgzf = true;
185 probe.crc = crc;
186 probe.isize = static_cast<uint32_t>(total);
187 probe.device =
188 tiled && total != 0 && total < std::numeric_limits<uint32_t>::max() && !fastq;
189 } else if (size >= 18 && ::pread(fd, trailer, 8, static_cast<off_t>(size - 8)) == 8) {
190 std::memcpy(&probe.crc, trailer, 4);
191 std::memcpy(&probe.isize, trailer + 4, 4);
192 probe.members = 1;
193 probe.device = header[0] == 0x1f && header[1] == 0x8b && header[2] == 8 &&
194 (header[3] & 0xe4U) == 0 && probe.isize != 0 && !fastq;
195 }
196 }
197 ::close(fd);
198 return probe;
199}
200
202template <typename Work>
203void parallel_for(size_t count, size_t workers, Work&& work) {
204 std::atomic<size_t> next{0};
205 auto run = [&] {
206 for (size_t index = next++; index < count; index = next++) work(index);
207 };
208 std::vector<std::jthread> threads;
209 auto const helpers = std::min(workers, count) > 0 ? std::min(workers, count) - 1 : 0;
210 threads.reserve(helpers);
211 for (size_t thread = 0; thread < helpers; ++thread) threads.emplace_back(run);
212 run();
213}
214
221[[nodiscard]] inline bool has_gzip_signature_portable(char const* data, size_t size) {
222 for (size_t at = 0; size - at >= 4;) {
223 auto const* const found =
224 static_cast<char const*>(::memmem(data + at, size - at - 1, "\x1f\x8b\x08", 3));
225 if (found == nullptr) return false;
226 if ((static_cast<uint8_t>(found[3]) & 0xe0U) == 0) return true;
227 at = static_cast<size_t>(found - data) + 1;
228 }
229 return false;
230}
231
232#if (defined(__x86_64__) || defined(__i386__)) && (defined(__GNUC__) || defined(__clang__))
234__attribute__((target("avx512f,avx512bw"))) inline bool
235has_gzip_signature_avx512(char const* data, size_t size) {
236 size_t at = 0;
237 for (; at + 67 <= size; at += 64) {
238 auto const first =
239 _mm512_cmpeq_epi8_mask(_mm512_loadu_si512(data + at), _mm512_set1_epi8(0x1f));
240 if (first == 0) continue;
241 auto const second = _mm512_cmpeq_epi8_mask(
242 _mm512_loadu_si512(data + at + 1), _mm512_set1_epi8(static_cast<char>(0x8b))
243 );
244 auto const third =
245 _mm512_cmpeq_epi8_mask(_mm512_loadu_si512(data + at + 2), _mm512_set1_epi8(0x08));
246 auto const flags = _mm512_testn_epi8_mask(
247 _mm512_loadu_si512(data + at + 3), _mm512_set1_epi8(static_cast<char>(0xe0))
248 );
249 if ((first & second & third & flags) != 0) return true;
250 }
251 return has_gzip_signature_portable(data + at, size - at);
252}
253
255__attribute__((target("avx2"))) inline bool has_gzip_signature_avx2(char const* data, size_t size) {
256 size_t at = 0;
257 for (; at + 35 <= size; at += 32) {
258 auto const* const block = reinterpret_cast<__m256i const*>(data + at);
259 auto const first = _mm256_cmpeq_epi8(_mm256_loadu_si256(block), _mm256_set1_epi8(0x1f));
260 if (_mm256_testz_si256(first, first)) continue;
261 auto const second = _mm256_cmpeq_epi8(
262 _mm256_loadu_si256(reinterpret_cast<__m256i const*>(data + at + 1)),
263 _mm256_set1_epi8(static_cast<char>(0x8b))
264 );
265 auto const third = _mm256_cmpeq_epi8(
266 _mm256_loadu_si256(reinterpret_cast<__m256i const*>(data + at + 2)),
267 _mm256_set1_epi8(0x08)
268 );
269 auto const flags = _mm256_cmpeq_epi8(
270 _mm256_and_si256(
271 _mm256_loadu_si256(reinterpret_cast<__m256i const*>(data + at + 3)),
272 _mm256_set1_epi8(static_cast<char>(0xe0))
273 ),
274 _mm256_setzero_si256()
275 );
276 auto const all =
277 _mm256_and_si256(_mm256_and_si256(first, flags), _mm256_and_si256(second, third));
278 if (!_mm256_testz_si256(all, all)) return true;
279 }
280 return has_gzip_signature_portable(data + at, size - at);
281}
282#endif
283
284#if defined(__aarch64__) || defined(_M_ARM64)
286inline bool has_gzip_signature_neon(char const* data, size_t size) {
287 size_t at = 0;
288 auto const* bytes = reinterpret_cast<uint8_t const*>(data);
289 for (; at + 19 <= size; at += 16) {
290 auto const first = vceqq_u8(vld1q_u8(bytes + at), vdupq_n_u8(0x1f));
291 if (vmaxvq_u8(first) == 0) continue;
292 auto const second = vceqq_u8(vld1q_u8(bytes + at + 1), vdupq_n_u8(0x8b));
293 auto const third = vceqq_u8(vld1q_u8(bytes + at + 2), vdupq_n_u8(8));
294 auto const flags =
295 vceqq_u8(vandq_u8(vld1q_u8(bytes + at + 3), vdupq_n_u8(0xe0)), vdupq_n_u8(0));
296 if (vmaxvq_u8(vandq_u8(vandq_u8(first, flags), vandq_u8(second, third))) != 0) {
297 return true;
298 }
299 }
300 return has_gzip_signature_portable(data + at, size - at);
301}
302#endif
303
305[[nodiscard]] inline auto resolve_gzip_signature_scanner() -> bool (*)(char const*, size_t) {
306 if (ablated("simd-parsing")) return has_gzip_signature_portable;
307#if (defined(__x86_64__) || defined(__i386__)) && (defined(__GNUC__) || defined(__clang__))
308 if (__builtin_cpu_supports("avx512bw")) return has_gzip_signature_avx512;
309 if (__builtin_cpu_supports("avx2")) return has_gzip_signature_avx2;
310#endif
311#if defined(__aarch64__) || defined(_M_ARM64)
312 return has_gzip_signature_neon;
313#endif
315}
316
317static bool (*const has_gzip_signature)(char const*, size_t) = resolve_gzip_signature_scanner();
318
319namespace device_gzip {
320
321inline constexpr uint32_t crc_polynomial = 0xEDB88320U;
322inline constexpr uint32_t block_size = 256;
323
324__device__ __forceinline__ bool is_whitespace(char c) {
325 return c == '\n' || c == '\r' || c == ' ' || c == '\t';
326}
327
329struct kept_byte {
330 __device__ bool operator()(char c) const {
331 return !is_whitespace(c);
332 }
333};
334
336__device__ inline uint32_t crc_multiply(uint32_t a, uint32_t b) {
337 uint32_t m = 1U << 31U, p = 0;
338 while (true) {
339 if ((a & m) != 0) {
340 p ^= b;
341 if ((a & (m - 1)) == 0) break;
342 }
343 m >>= 1U;
344 b = (b & 1U) != 0 ? (b >> 1U) ^ crc_polynomial : b >> 1U;
345 }
346 return p;
347}
348
350__device__ inline uint32_t
351crc_combine(uint32_t first, uint32_t second, uint64_t second_bytes, uint32_t const* x2n) {
352 uint32_t shift = 1U << 31U;
353 for (uint32_t k = 3; second_bytes != 0; second_bytes >>= 1U, ++k) {
354 if ((second_bytes & 1U) != 0) shift = crc_multiply(x2n[k & 31U], shift);
355 }
356 return crc_multiply(shift, first) ^ second;
357}
358
360static __global__ void slot_init_kernel(
361 char* buffer,
362 uint64_t const* slots,
363 uint32_t files,
364 unsigned long long* first_header,
365 unsigned long long* header_count
366) {
367 auto const file = blockIdx.x * blockDim.x + threadIdx.x;
368 if (file == 0) *header_count = 0;
369 if (file >= files) return;
370 buffer[slots[file]] = '\n';
371 first_header[file] = slots[file + 1];
372}
373
378static __global__ void slot_crc_kernel(char const* buffer, uint64_t const* slots, uint32_t* crcs) {
379 __shared__ uint32_t table[4][256];
380 __shared__ uint32_t x2n[32];
381 __shared__ uint32_t crc[block_size];
382 __shared__ uint64_t bytes[block_size];
383 auto const t = threadIdx.x;
384 uint32_t entry = t;
385 _Pragma("unroll")
386 for (int bit = 0; bit < 8; ++bit) {
387 entry = (entry & 1U) != 0 ? (entry >> 1U) ^ crc_polynomial : entry >> 1U;
388 }
389 table[0][t] = entry;
391 for (int slice = 1; slice < 4; ++slice) {
392 auto const previous = table[slice - 1][t];
393 table[slice][t] = (previous >> 8U) ^ table[0][previous & 0xFFU];
395 }
396 if (t == 0) {
397 uint32_t power = 1U << 30U;
398 x2n[0] = power;
399 for (int n = 1; n < 32; ++n) x2n[n] = power = crc_multiply(power, power);
400 }
401 auto const* const data = reinterpret_cast<unsigned char const*>(buffer + slots[blockIdx.x] + 1);
402 uint64_t const size = slots[blockIdx.x + 1] - slots[blockIdx.x] - 1;
403 uint64_t const per = ((size + block_size - 1) / block_size + 15) & ~uint64_t{15};
404 uint64_t const begin = cuda::std::min<uint64_t>(size, t * per);
405 uint64_t const end = cuda::std::min<uint64_t>(size, begin + per);
406 uint32_t c = 0xFFFFFFFFU;
407 auto byte = [&](unsigned char value) {
408 c = table[0][(c ^ value) & 0xFFU] ^ (c >> 8U);
409 };
410 uint64_t at = begin;
411 for (; at < end && (reinterpret_cast<uintptr_t>(data + at) & 15U) != 0; ++at) byte(data[at]);
412 for (; at + 16 <= end; at += 16) {
413 auto const words = *reinterpret_cast<uint4 const*>(data + at);
414 uint32_t const lanes[4] = {words.x, words.y, words.z, words.w};
415 _Pragma("unroll")
416 for (uint32_t word : lanes) {
417 c ^= word;
418 c = table[3][c & 0xFFU] ^ table[2][(c >> 8U) & 0xFFU] ^ table[1][(c >> 16U) & 0xFFU] ^
419 table[0][c >> 24U];
420 }
421 }
422 for (; at < end; ++at) byte(data[at]);
423 crc[t] = ~c;
424 bytes[t] = end - begin;
426 for (uint32_t stride = 1; stride < block_size; stride *= 2) {
427 if (t % (2 * stride) == 0) {
428 crc[t] = crc_combine(crc[t], crc[t + stride], bytes[t + stride], x2n);
429 bytes[t] += bytes[t + stride];
430 }
432 }
433 if (t == 0) crcs[blockIdx.x] = crc[0];
434}
435
438static __global__ void header_kernel(
439 char const* buffer,
440 uint64_t size,
441 uint64_t* headers,
442 uint32_t capacity,
443 unsigned long long* count
444) {
445 constexpr uint64_t ones = 0x0101010101010101ULL;
446 auto const has_marker = [](uint64_t word) {
447 auto const x = word ^ (ones * '>');
448 return ((x - ones) & ~x & 0x8080808080808080ULL) != 0;
449 };
450 uint64_t const words = (size + 15) / 16;
451 for (uint64_t w = blockIdx.x * uint64_t{blockDim.x} + threadIdx.x; w < words;
452 w += uint64_t{gridDim.x} * blockDim.x) {
453 auto const value = *reinterpret_cast<ulonglong2 const*>(buffer + w * 16);
454 if (!has_marker(value.x) && !has_marker(value.y)) continue;
455 for (uint32_t j = 0; j < 16; ++j) {
456 auto const position = w * 16 + j;
457 if (position == 0 || position >= size || buffer[position] != '>') continue;
458 auto const previous = buffer[position - 1];
459 if (previous != '\n' && previous != '\r') continue;
460 auto const slot = atomicAdd(count, 1ULL);
461 if (slot < capacity) headers[slot] = position;
462 }
463 }
464}
465
467__device__ __forceinline__ uint32_t
468slot_containing(uint64_t const* slots, uint32_t files, uint64_t position) {
469 uint32_t low = 0, high = files;
470 while (high - low > 1) {
471 auto const middle = (low + high) / 2;
472 if (slots[middle] <= position) {
473 low = middle;
474 } else {
475 high = middle;
476 }
477 }
478 return low;
479}
480
483static __global__ void blank_header_kernel(
484 char* buffer,
485 uint64_t const* headers,
486 unsigned long long const* count,
487 uint32_t capacity,
488 uint64_t const* slots,
489 uint32_t files,
490 unsigned long long* first_header
491) {
492 auto const lane = threadIdx.x % 32U;
493 auto const total = cuda::std::min<unsigned long long>(*count, capacity);
494 auto const warps = uint64_t{gridDim.x} * blockDim.x / 32U;
495 for (uint64_t h = (blockIdx.x * uint64_t{blockDim.x} + threadIdx.x) / 32U; h < total;
496 h += warps) {
497 auto const position = headers[h];
498 auto const low = slot_containing(slots, files, position);
499 if (lane == 0) atomicMin(first_header + low, static_cast<unsigned long long>(position));
500 auto const end = slots[low + 1];
501 for (uint64_t at = position + 1; at < end; at += 32) {
502 auto const index = at + lane;
503 auto const c = index < end ? buffer[index] : '\n';
504 auto const stops = __ballot_sync(0xFFFFFFFFU, c == '\n' || c == '\r');
505 auto const stop = stops != 0 ? static_cast<uint32_t>(__ffs(stops) - 1) : 32U;
506 if (lane < stop) buffer[index] = '\n';
507 if (stops != 0) break;
508 }
509 }
510}
511
513__device__ __forceinline__ uint32_t kept_in_word(uint64_t word, uint32_t first, uint32_t last) {
514 constexpr uint64_t low7 = 0x7F7F7F7F7F7F7F7FULL, ones = 0x0101010101010101ULL;
515 // The high bit of every zero byte of x, exactly: the addition carries within bytes only.
516 auto const zero_bytes = [](uint64_t x) {
517 return ~(((x & low7) + low7) | x | low7);
518 };
519 auto const blank = zero_bytes(word ^ (ones * '\n')) | zero_bytes(word ^ (ones * '\r')) |
520 zero_bytes(word ^ (ones * ' ')) | zero_bytes(word ^ (ones * '\t'));
521 auto const width = last - first;
522 auto const span = (width == 8 ? ~uint64_t{0} : (uint64_t{1} << (8 * width)) - 1) << (8 * first);
523 return static_cast<uint32_t>(__popcll(~blank & span & ~low7));
524}
525
527inline constexpr uint64_t kept_tile = uint64_t{block_size} * 16 * 8;
528
534static __global__ void kept_count_kernel(
535 char const* buffer,
536 uint64_t const* slots,
537 uint32_t files,
538 unsigned long long* kept
539) {
540 using block_reduce = cub::BlockReduce<uint32_t, block_size>;
541 __shared__ typename block_reduce::TempStorage reduce_storage;
542 __shared__ uint32_t tile_slot;
543 constexpr uint32_t straddles = ~uint32_t{0};
544 auto const begin = slots[0], end = slots[files];
545 auto const last_tile = (end + kept_tile - 1) / kept_tile;
546 for (auto tile = begin / kept_tile + blockIdx.x; tile < last_tile; tile += gridDim.x) {
547 auto const low = cuda::std::max(begin, tile * kept_tile);
548 auto const high = cuda::std::min(end, (tile + 1) * kept_tile);
549 if (threadIdx.x == 0) {
550 auto const slot = slot_containing(slots, files, low);
551 tile_slot = slots[slot + 1] >= high ? slot : straddles;
552 }
554 auto const slot = tile_slot;
555 uint32_t count = 0;
556 for (auto at = tile * kept_tile + uint64_t{threadIdx.x} * 16; at < (tile + 1) * kept_tile;
557 at += uint64_t{block_size} * 16) {
558 if (at + 16 <= low || at >= high) continue;
559 auto const words = *reinterpret_cast<ulonglong2 const*>(buffer + at);
560 uint64_t const halves[2] = {words.x, words.y};
561 _Pragma("unroll")
562 for (uint32_t half = 0; half < 2; ++half) {
563 auto const base = at + 8 * half;
564 auto const first =
565 static_cast<uint32_t>(low > base ? cuda::std::min<uint64_t>(low - base, 8) : 0);
566 auto const last = static_cast<uint32_t>(
567 high > base ? cuda::std::min<uint64_t>(high - base, 8) : 0
568 );
569 if (first >= last) continue;
570 if (slot != straddles) {
571 count += kept_in_word(halves[half], first, last);
572 continue;
573 }
574 auto const head = slot_containing(slots, files, base + first);
575 if (head == slot_containing(slots, files, base + last - 1)) {
576 atomicAdd(kept + head, kept_in_word(halves[half], first, last));
577 continue;
578 }
579 for (auto byte = first; byte < last; ++byte) {
580 atomicAdd(
581 kept + slot_containing(slots, files, base + byte),
582 kept_in_word(halves[half], byte, byte + 1)
583 );
584 }
585 }
586 }
587 if (slot != straddles) {
588 auto const sum = block_reduce(reduce_storage).Sum(count);
589 if (threadIdx.x == 0) atomicAdd(kept + slot, sum);
590 }
592 }
593}
594
597static __global__ void slot_finish_kernel(
598 char* buffer,
599 uint64_t const* slots,
600 unsigned long long const* first_header,
601 char* first_char
602) {
603 auto const begin = slots[blockIdx.x] + 1;
604 auto const end = slots[blockIdx.x + 1];
605 if (threadIdx.x == 0) {
606 auto at = begin;
607 while (at < end && (buffer[at] == '\n' || buffer[at] == '\r')) ++at;
608 first_char[blockIdx.x] = at < end ? buffer[at] : '\0';
609 }
611 auto const head = cuda::std::min<uint64_t>(first_header[blockIdx.x], end);
612 for (auto at = begin + threadIdx.x; at < head; at += block_size) buffer[at] = '\n';
613}
614
618inline size_t hardware_inflate_limit(int device) {
619 void* address = nullptr;
620 cudaDriverEntryPointQueryResult found{};
621 if (cudaGetDriverEntryPointByVersion(
622 "cuDeviceGetAttribute", &address, 12000U, cudaEnableDefault, &found
623 ) != cudaSuccess ||
624 found != cudaDriverEntryPointSuccess) {
625 return 0;
626 }
627 int limit = 0;
628 auto const attribute = reinterpret_cast<decltype(&cuDeviceGetAttribute)>(address);
629 auto const status =
630 attribute(&limit, CU_DEVICE_ATTRIBUTE_MEM_DECOMPRESS_MAXIMUM_LENGTH, device);
631 return status == CUDA_SUCCESS && limit > 0 ? static_cast<size_t>(limit) : 0;
632}
633
638[[nodiscard]] inline cuda::device_memory_pool_ref hardware_inflate_pool(cuda::device_ref device) {
639 static std::mutex mutex;
640 static std::vector<cudaMemPool_t> pools(cuda::devices.size());
641 std::lock_guard lock(mutex);
642 auto& pool = pools.at(static_cast<size_t>(device.get()));
643 if (pool == nullptr) {
644 cudaMemPoolProps properties{};
645 properties.allocType = cudaMemAllocationTypePinned;
646 properties.location = {cudaMemLocationTypeDevice, device.get()};
647 properties.usage = cudaMemPoolCreateUsageHwDecompress;
648 CUDDL_CUDA_CALL(cudaMemPoolCreate(&pool, &properties));
649 }
650 return cuda::device_memory_pool_ref(pool);
651}
652
654inline PFN_cuMemBatchDecompressAsync_v12060 batch_decompress_entry() {
655 void* address = nullptr;
656 cudaDriverEntryPointQueryResult found{};
657 if (cudaGetDriverEntryPointByVersion(
658 "cuMemBatchDecompressAsync", &address, 12060U, cudaEnableDefault, &found
659 ) != cudaSuccess ||
660 found != cudaDriverEntryPointSuccess) {
661 return nullptr;
662 }
663 return reinterpret_cast<PFN_cuMemBatchDecompressAsync_v12060>(address);
664}
665
668inline size_t deflate_offset(char const* bytes, size_t size) {
669 auto const flags = static_cast<unsigned char>(bytes[3]);
670 size_t at = 10;
671 // FNAME, then FCOMMENT: zero-terminated strings.
672 for (auto const flag : {8U, 16U}) {
673 if ((flags & flag) == 0) continue;
674 auto const* const end =
675 static_cast<char const*>(std::memchr(bytes + at, '\0', size - std::min(at, size)));
676 if (end == nullptr) return 0;
677 at = static_cast<size_t>(end - bytes) + 1;
678 }
679 if ((flags & 2) != 0) at += 2; // FHCRC
680 return at + 8 < size ? at : 0;
681}
682
683} // namespace device_gzip
684
687 size_t id;
688 char const* bases;
689 size_t size;
690};
691
693 size_t id;
694 std::string_view bytes;
695};
696
712 struct lane_state;
713
714 public:
715 static constexpr size_t lane_count = 6;
716 static constexpr size_t max_files = 4096;
719 static constexpr size_t max_members = 65536;
720
726 cuda::stream_ref stream,
727 size_t slot_capacity,
728 size_t compressed_capacity,
729 size_t workers,
730 bool hardware_inflate
731 )
732 : workers_(std::max<size_t>(1, workers)),
733 compressed_capacity_(compressed_capacity),
734 slot_capacity_(slot_capacity),
735 header_capacity_(
736 static_cast<uint32_t>(std::clamp<size_t>(
737 slot_capacity / 4096,
738 1 << 16,
739 std::numeric_limits<uint32_t>::max()
740 ))
741 ) {
742 auto const options = nvcompBatchedDeflateDecompressDefaultOpts;
743 nvcomp_temp_bytes_ = 0;
744 if (compressed_capacity_ != 0 &&
745 nvcompBatchedDeflateDecompressGetTempSizeAsync(
746 max_members,
747 std::min<size_t>(slot_capacity_, std::numeric_limits<uint32_t>::max()),
748 options,
749 &nvcomp_temp_bytes_,
750 slot_capacity_
751 ) != nvcompSuccess) {
752 throw std::bad_alloc();
753 }
754 select_temp_bytes_ = 0;
755 auto const selected = compressed_capacity_ == 0 ? cub::DeviceSelect::If(
756 nullptr,
757 select_temp_bytes_,
758 static_cast<char*>(nullptr),
759 static_cast<int64_t*>(nullptr),
760 static_cast<int64_t>(slot_capacity_),
762 stream.get()
763 )
764 : cub::DeviceSelect::If(
765 nullptr,
766 select_temp_bytes_,
767 static_cast<char*>(nullptr),
768 static_cast<char*>(nullptr),
769 static_cast<int64_t*>(nullptr),
770 static_cast<int64_t>(slot_capacity_),
772 stream.get()
773 );
774 if (selected != cudaSuccess) throw std::runtime_error(cudaGetErrorString(selected));
775 auto const device = stream.device();
776 if (hardware_inflate) {
777 batch_decompress_ = device_gzip::batch_decompress_entry();
778 if (batch_decompress_ != nullptr) {
779 hardware_limit_ = device_gzip::hardware_inflate_limit(device.get());
780 }
781 }
782 streaming_window_ =
783 compressed_capacity_ == 0
784 ? std::min(
785 slot_capacity_,
786 static_cast<size_t>(
787 device.attribute(cuda::device_attributes::max_access_policy_window_size)
788 )
789 )
790 : size_t{0};
791 for (auto& lane : lanes_) {
792 lane.stream.emplace(device);
793 auto const s = cuda::stream_ref{*lane.stream};
794 lane.uploaded.emplace(s);
795 lane.classified.emplace(s);
796 lane.released.emplace(stream);
797 }
798 // The other lanes allocate on first use, so the device starts on the first batch while
799 // they page-lock their compressed buffers.
800 allocate(lanes_[0]);
801 }
802
803 [[nodiscard]] size_t slot_capacity() const noexcept {
804 return slot_capacity_;
805 }
806
807 [[nodiscard]] size_t compressed_capacity() const noexcept {
808 return compressed_capacity_;
809 }
810
812 [[nodiscard]] size_t hardware_limit() const noexcept {
813 return hardware_limit_;
814 }
815
817 [[nodiscard]] static size_t lane_overhead(size_t slot_capacity) noexcept {
818 return slot_capacity / 4096 * sizeof(uint64_t) + (size_t{64} << 20);
819 }
820
822 [[nodiscard]] size_t batch_files(
823 std::span<size_t const> ids,
824 std::span<gzip_file_probe const> probes
825 ) const noexcept {
826 size_t files = 0, compressed = 0, slots = 0, members = 0;
827 while (files < ids.size() && files < max_files) {
828 auto const& probe = probes[ids[files]];
829 if (compressed + probe.host_bytes() > compressed_capacity_ ||
830 slots + probe.isize + 1 > slot_capacity_ || members + probe.members > max_members) {
831 break;
832 }
833 compressed += probe.host_bytes();
834 slots += probe.isize + 1;
835 members += probe.members;
836 ++files;
837 }
838 return files;
839 }
840
848 [[nodiscard]] Result<size_t> submit(
849 size_t lane_index,
850 std::span<size_t const> ids,
851 std::span<std::filesystem::path const> paths,
852 std::span<gzip_file_probe const> probes,
853 bool direct_reads
854 ) {
855 auto& lane = lanes_[lane_index];
856 CUDDL_TRY(cuda_try([&] { allocate(lane); }));
857 lane.decoded = false;
858 if (ids.empty()) return Err(Error::invalid_argument("an inflate batch needs a file"));
859 auto const files = batch_files(ids, probes);
860 // The pinned bytes are rewritten below, so the previous batch's upload must be done.
861 CUDDL_CUDA_TRY(lane.uploaded->sync());
862 auto table = host_table(lane);
863 // Plain files lie in the page-locked buffer exactly as they will in the slots, which
864 // they lead, so one copy places them all. Compressed bytes follow them.
865 uint64_t plain_at = 0;
866 for (size_t file = 0; file < files; ++file) {
867 auto const& probe = probes[ids[file]];
868 if (probe.plain) plain_at += probe.host_bytes();
869 }
870 uint64_t compressed_at = plain_at, slot_at = 0;
871 for (size_t file = 0; file < files; ++file) {
872 auto const& probe = probes[ids[file]];
873 table.compressed_bytes[file] = probe.compressed;
874 if (probe.plain) {
875 table.compressed_offsets[file] = slot_at + 1;
876 slot_at += probe.host_bytes();
877 } else {
878 table.compressed_offsets[file] = compressed_at;
879 compressed_at += probe.compressed;
880 }
881 }
882 std::atomic<bool> failed{false};
883 readers_.for_each(files, [&](size_t file) {
884 auto const* const path = paths[ids[file]].c_str();
885 auto* const out = lane.pinned->data() + table.compressed_offsets[file];
886 auto const bytes = table.compressed_bytes[file];
887 int const fd = ::open(path, O_RDONLY);
888 // Files lie back to back in the lane, so a direct read goes through a bounce buffer.
889 bool const read = fd != -1 && read_whole_file(fd, out, bytes, direct_reads);
890 auto const done = read ? bytes : 0;
891 if (!read) failed = true;
892 // A matching trailer cannot distinguish repeated first and last members.
893 // A signature inside compressed data is ambiguous too, so use the host parser.
894 // BGZF names every member in its block headers.
895 auto const& probe = probes[ids[file]];
896 lane.possible_members[file] =
897 !probe.plain && !probe.bgzf && done > 10 && has_gzip_signature(out + 10, done - 10);
898 });
899 if (failed) return Err(Error::invalid_argument("cannot read reference inputs"));
900 lane.files.clear();
901 lane.fallback.clear();
902 lane.member_first.clear();
903 engine_params_.clear();
904 slot_at = 0;
905 uint32_t members = 0;
906 auto const accept = [&](size_t file) {
907 table.slots[lane.files.size()] = slot_at;
908 lane.member_first.push_back(members);
909 slot_at += probes[ids[file]].isize + 1;
910 lane.files.push_back(ids[file]);
911 };
912 for (size_t file = 0; file < files; ++file) {
913 if (probes[ids[file]].plain) accept(file);
914 }
915 auto const plain = lane.files.size();
916 for (size_t file = 0; file < files; ++file) {
917 if (!probes[ids[file]].plain && lane.possible_members[file]) {
918 lane.fallback.push_back(ids[file]);
919 }
920 }
921 // One member: raw DEFLATE at @p deflate in the page-locked copy, which the payload buffer
922 // mirrors, inflating to @p size bytes at @p out.
923 auto const add_member = [&](unsigned char const* deflate,
924 uint64_t deflate_bytes,
925 uint32_t size,
926 char* out) {
927 auto* const source = lane.payload->data() +
928 (reinterpret_cast<char const*>(deflate) - lane.pinned->data());
929 table.member_sources[members] = source;
930 table.member_source_bytes[members] = deflate_bytes;
931 table.member_outputs[members] = out;
932 table.member_output_bytes[members] = size;
933 if (hardware_limit_ != 0 && size <= hardware_limit_) {
934 CUmemDecompressParams params{};
935 params.src = source;
936 params.srcNumBytes = deflate_bytes;
937 params.dst = out;
938 params.dstNumBytes = size;
939 // The low half of the member's 64-bit inflated size, zeroed before the batch.
940 params.dstActBytes =
941 reinterpret_cast<cuuint32_t*>(device_tables(lane).member_inflated + members);
942 params.algo = CU_MEM_DECOMPRESS_ALGORITHM_DEFLATE;
943 engine_params_.push_back(params);
944 }
945 ++members;
946 };
947 // Gzip files whose members all fit the decompression engine come first, then those
948 // nvCOMP inflates on the SMs, so each group's members form one range. Both take raw
949 // DEFLATE: the host steps over each member's header in its copy. Without an engine, every
950 // gzip file is in the second group.
951 auto const gzip_pass = [&](bool engine) {
952 for (size_t file = 0; file < files; ++file) {
953 auto const& probe = probes[ids[file]];
954 if (probe.plain || lane.possible_members[file]) continue;
955 bool const fits =
956 hardware_limit_ != 0 && (probe.bgzf || probe.isize <= hardware_limit_);
957 if (fits != engine) continue;
958 auto const* const bytes = reinterpret_cast<unsigned char const*>(
959 lane.pinned->data() + table.compressed_offsets[file]
960 );
961 auto* out = lane.slots->data() + slot_at + 1;
962 if (probe.bgzf) {
963 // The blocks must still be the ones probed, or they would overrun the slot.
964 auto const blocks = walk_bgzf(bytes, probe.compressed, [](auto...) {});
965 if (!blocks || blocks->first != probe.members ||
966 blocks->second != probe.isize) {
967 lane.fallback.push_back(ids[file]);
968 continue;
969 }
970 accept(file);
971 (void)walk_bgzf(
972 bytes, probe.compressed, [&](uint64_t at, uint64_t length, uint32_t size) {
973 add_member(bytes + at, length, size, out);
974 out += size;
975 }
976 );
977 continue;
978 }
979 auto const deflate_at = device_gzip::deflate_offset(
980 reinterpret_cast<char const*>(bytes), probe.compressed
981 );
982 if (deflate_at == 0) {
983 lane.fallback.push_back(ids[file]);
984 continue;
985 }
986 accept(file);
987 add_member(bytes + deflate_at, probe.compressed - deflate_at - 8, probe.isize, out);
988 }
989 };
990 gzip_pass(true);
991 auto const engine_members = members;
992 gzip_pass(false);
993 lane.member_first.push_back(members);
994 table.slots[lane.files.size()] = slot_at;
995 lane.slot_bytes = slot_at;
996 if (lane.files.empty()) return files;
997
998 auto const s = cuda::stream_ref{*lane.stream};
999 auto* const stream = s.get();
1000 auto const device_table = device_tables(lane);
1001 // The slots are rewritten below, so the sketch kernel reading them must be done.
1002 CUDDL_CUDA_TRY(s.wait(*lane.released));
1003 // Plain bytes land in their slots; slot_init_kernel then writes the guard bytes between.
1004 if (plain_at != 0) {
1006 cuda::copy_bytes(
1007 s,
1008 cuda::std::span{lane.pinned->data(), plain_at},
1009 device_span<char>{lane.slots->data(), plain_at}
1010 )
1011 );
1012 }
1013 if (compressed_at != plain_at) {
1015 cuda::copy_bytes(
1016 s,
1017 cuda::std::span{lane.pinned->data() + plain_at, compressed_at - plain_at},
1018 device_span<char>{lane.payload->data() + plain_at, compressed_at - plain_at}
1019 )
1020 );
1021 }
1023 cuda::copy_bytes(
1024 s,
1025 cuda::std::span{lane.host->data(), inputs_bytes()},
1026 device_span<char>{lane.tables->data(), inputs_bytes()}
1027 )
1028 );
1029 CUDDL_CUDA_TRY(lane.uploaded->record(s));
1030 auto const count = static_cast<uint32_t>(lane.files.size());
1031 device_gzip::slot_init_kernel<<<(count + 255) / 256, 256, 0, stream>>>(
1032 lane.slots->data(),
1033 device_table.slots,
1034 count,
1035 device_table.first_header,
1036 device_table.header_count
1037 );
1038 CUDDL_CUDA_TRY(cudaGetLastError());
1039 if (engine_members != 0) {
1040 // The driver takes the parameters from host memory. nvCOMP's own engine path builds
1041 // them on the device and synchronises the stream to read them back, which stalls this
1042 // thread and every lane it drives. The engine reports only inflated sizes, so the
1043 // statuses read as success; finish still checks each size and CRC32.
1044 static_assert(nvcompSuccess == 0);
1045 static_assert(std::endian::native == std::endian::little);
1046 CUDDL_CUDA_TRY(cudaMemsetAsync(
1047 device_table.member_inflated, 0, engine_members * sizeof(uint64_t), stream
1048 ));
1049 CUDDL_CUDA_TRY(cudaMemsetAsync(
1050 device_table.member_statuses, 0, engine_members * sizeof(nvcompStatus_t), stream
1051 ));
1052 size_t error_index = 0;
1053 auto const submitted = batch_decompress_(
1054 engine_params_.data(), engine_params_.size(), 0, &error_index, stream
1055 );
1056 if (submitted != CUDA_SUCCESS) {
1057 return Err(
1058 Error::resource(
1059 "hardware decompression launch failed: " + std::to_string(submitted)
1060 )
1061 );
1062 }
1063 }
1064 if (engine_members != members) {
1065 auto options = nvcompBatchedDeflateDecompressDefaultOpts;
1066 options.backend = NVCOMP_DECOMPRESS_BACKEND_CUDA;
1067 auto const first = engine_members;
1068 auto const inflated = nvcompBatchedDeflateDecompressAsync(
1069 reinterpret_cast<void const* const*>(device_table.member_sources + first),
1070 device_table.member_source_bytes + first,
1071 device_table.member_output_bytes + first,
1072 device_table.member_inflated + first,
1073 members - first,
1074 lane.temp->data(),
1075 nvcomp_temp_bytes_,
1076 reinterpret_cast<void* const*>(device_table.member_outputs + first),
1077 options,
1078 device_table.member_statuses + first,
1079 stream
1080 );
1081 if (inflated != nvcompSuccess) {
1082 return Err(
1083 Error::resource("nvCOMP DEFLATE launch failed: " + std::to_string(inflated))
1084 );
1085 }
1086 }
1087 if (auto const gzip = count - static_cast<uint32_t>(plain); gzip != 0) {
1088 device_gzip::slot_crc_kernel<<<gzip, device_gzip::block_size, 0, stream>>>(
1089 lane.slots->data(), device_table.slots + plain, device_table.crcs + plain
1090 );
1091 CUDDL_CUDA_TRY(cudaGetLastError());
1092 }
1093 CUDDL_TRY(normalise(lane, slot_at));
1094 return files;
1095 }
1096
1097 private:
1098 [[nodiscard]] Result<void> normalise(lane_state& lane, uint64_t slot_at) {
1099 auto const s = cuda::stream_ref{*lane.stream};
1100 auto const stream = s.get();
1101 auto const device_table = device_tables(lane);
1102 auto const count = static_cast<uint32_t>(lane.files.size());
1103 auto const grid = static_cast<uint32_t>(
1104 s.device().attribute(cuda::device_attributes::multiprocessor_count) * 8
1105 );
1106 device_gzip::header_kernel<<<grid, 256, 0, stream>>>(
1107 lane.slots->data(),
1108 slot_at,
1109 lane.headers->data(),
1110 header_capacity_,
1111 device_table.header_count
1112 );
1113 CUDDL_CUDA_TRY(cudaGetLastError());
1114 device_gzip::blank_header_kernel<<<grid, 256, 0, stream>>>(
1115 lane.slots->data(),
1116 lane.headers->data(),
1117 device_table.header_count,
1118 header_capacity_,
1119 device_table.slots,
1120 count,
1121 device_table.first_header
1122 );
1123 CUDDL_CUDA_TRY(cudaGetLastError());
1124 device_gzip::slot_finish_kernel<<<count, device_gzip::block_size, 0, stream>>>(
1125 lane.slots->data(),
1126 device_table.slots,
1127 device_table.first_header,
1128 device_table.first_char
1129 );
1130 CUDDL_CUDA_TRY(cudaGetLastError());
1132 cudaMemsetAsync(device_table.kept, 0, count * sizeof(unsigned long long), stream)
1133 );
1134 device_gzip::kept_count_kernel<<<grid, device_gzip::block_size, 0, stream>>>(
1135 lane.slots->data(), device_table.slots, count, device_table.kept
1136 );
1137 CUDDL_CUDA_TRY(cudaGetLastError());
1138 auto temp_bytes = select_temp_bytes_;
1139 if (lane.decoded) {
1141 cub::DeviceSelect::If(
1142 lane.temp->data(),
1143 temp_bytes,
1144 lane.slots->data(),
1145 device_table.selected,
1146 static_cast<int64_t>(slot_at),
1147 device_gzip::kept_byte{},
1148 stream
1149 )
1150 );
1151 } else {
1152 // Inflation has consumed the compressed bytes; reuse their storage for compacted FASTA.
1154 cub::DeviceSelect::If(
1155 lane.temp->data(),
1156 temp_bytes,
1157 lane.slots->data(),
1158 lane.payload->data(),
1159 device_table.selected,
1160 static_cast<int64_t>(slot_at),
1161 device_gzip::kept_byte{},
1162 stream
1163 )
1164 );
1165 }
1167 cuda::copy_bytes(
1168 s,
1169 cuda::std::span{lane.tables->data() + inputs_bytes(), outputs_bytes()},
1170 device_span<char>{lane.host->data() + inputs_bytes(), outputs_bytes()}
1171 )
1172 );
1173 CUDDL_CUDA_TRY(lane.classified->record(s));
1174 return Ok();
1175 }
1176
1177 public:
1180 [[nodiscard]] Result<void>
1181 submit_decoded(size_t lane_index, std::span<decoded_fasta const> sources) {
1182 if (sources.empty() || sources.size() > max_files || compressed_capacity_ != 0) {
1183 return Err(Error::invalid_argument("invalid decoded FASTA batch"));
1184 }
1185 auto& lane = lanes_[lane_index];
1186 CUDDL_TRY(cuda_try([&] { allocate(lane); }));
1187 lane.uploaded->sync();
1188 lane.decoded = true;
1189 lane.files.clear();
1190 lane.fallback.clear();
1191 auto const table = host_table(lane);
1192 uint64_t bytes = 0;
1193 for (auto const& source : sources) {
1194 if (source.bytes.size() >= slot_capacity_ - bytes) {
1195 return Err(Error::invalid_argument("decoded FASTA batch exceeds its lane"));
1196 }
1197 auto const file = lane.files.size();
1198 table.slots[file] = bytes;
1199 table.compressed_ptrs[file] = const_cast<char*>(source.bytes.data());
1200 table.compressed_bytes[file] = source.bytes.size();
1201 table.output_ptrs[file] = lane.slots->data() + bytes + 1;
1202 bytes += source.bytes.size() + 1;
1203 lane.files.push_back(source.id);
1204 }
1205 table.slots[sources.size()] = bytes;
1206 auto const s = cuda::stream_ref{*lane.stream};
1207 auto const device_table = device_tables(lane);
1208 CUDDL_CUDA_TRY(s.wait(*lane.released));
1210 cuda::copy_bytes(
1211 s,
1212 cuda::std::span{lane.host->data(), inputs_bytes()},
1213 device_span<char>{lane.tables->data(), inputs_bytes()}
1214 )
1215 );
1216 auto const count = static_cast<uint32_t>(sources.size());
1217 device_gzip::slot_init_kernel<<<(count + 255) / 256, 256, 0, s.get()>>>(
1218 lane.slots->data(),
1219 device_table.slots,
1220 count,
1221 device_table.first_header,
1222 device_table.header_count
1223 );
1224 CUDDL_CUDA_TRY(cudaGetLastError());
1225 static_assert(CUDART_VERSION >= 13000, "cuDDL needs CUDA 13.0 or newer");
1226 cudaMemcpyAttributes attributes{};
1227 attributes.srcAccessOrder = cudaMemcpySrcAccessOrderAny;
1228 attributes.srcLocHint = {cudaMemLocationTypeHost, 0};
1229 attributes.flags = cudaMemcpyFlagPreferOverlapWithCompute;
1230 size_t attributes_index = 0;
1231 CUDDL_CUDA_TRY(cudaMemcpyBatchAsync(
1232 reinterpret_cast<void* const*>(table.output_ptrs),
1233 reinterpret_cast<void const* const*>(table.compressed_ptrs),
1234 table.compressed_bytes,
1235 count,
1236 &attributes,
1237 &attributes_index,
1238 1,
1239 s.get()
1240 ));
1241 CUDDL_CUDA_TRY(lane.uploaded->record(s));
1242 return normalise(lane, bytes);
1243 }
1244
1246 [[nodiscard]] Result<void> wait_lanes(cuda::stream_ref consumer) {
1247 for (auto& lane : lanes_) {
1248 CUDDL_CUDA_TRY(lane.stream->sync());
1249 }
1250 CUDDL_CUDA_TRY(consumer.sync());
1251 return Ok();
1252 }
1253
1259 [[nodiscard]] Result<void> finish(
1260 size_t lane_index,
1261 std::span<gzip_file_probe const> probes,
1262 std::vector<inflated_genome>& genomes,
1263 std::vector<size_t>& fallback
1264 ) {
1265 auto& lane = lanes_[lane_index];
1266 fallback.insert(fallback.end(), lane.fallback.begin(), lane.fallback.end());
1267 if (lane.files.empty()) return Ok();
1268 CUDDL_CUDA_TRY(lane.classified->sync());
1269 auto const table = host_table(lane);
1270 auto const overflow = *table.header_count > header_capacity_;
1271 auto const* bases = lane.decoded ? lane.slots->data() : lane.payload->data();
1272 uint64_t offset = 0;
1273 for (size_t file = 0; file < lane.files.size(); ++file) {
1274 auto const id = lane.files[file];
1275 auto const size = table.kept[file];
1276 bool intact = lane.decoded || probes[id].plain;
1277 if (!intact) {
1278 uint64_t inflated = 0;
1279 bool succeeded = true;
1280 for (auto m = lane.member_first[file]; m < lane.member_first[file + 1]; ++m) {
1281 succeeded = succeeded && table.member_statuses[m] == nvcompSuccess;
1282 inflated += table.member_inflated[m];
1283 }
1284 intact =
1285 succeeded && inflated == probes[id].isize && table.crcs[file] == probes[id].crc;
1286 }
1287 auto const fasta =
1288 table.first_char[file] != '@' && table.first_header[file] < table.slots[file + 1];
1289 // Each header contributes one separator, including the first header.
1290 // ponytail: CPU-validate shorter outputs; count per-file headers if this dominates.
1291 auto const has_sequence = size > *table.header_count;
1292 if (!overflow && intact && fasta && has_sequence) {
1293 genomes.push_back({id, bases + offset, size});
1294 } else {
1295 fallback.push_back(id);
1296 }
1297 offset += size;
1298 }
1299 if (static_cast<int64_t>(offset) != *table.selected) {
1300 return Err(Error::resource("device FASTA compaction lost track of its bytes"));
1301 }
1302 return Ok();
1303 }
1304
1306 [[nodiscard]] Result<void> release(size_t lane_index, cuda::stream_ref consumer) {
1307 CUDDL_CUDA_TRY(lanes_[lane_index].released->record(consumer));
1308 return Ok();
1309 }
1310
1311 private:
1313 struct table_view {
1314 uint64_t* compressed_offsets; // host only
1315 char** compressed_ptrs; // decoded input only
1316 uint64_t* compressed_bytes;
1317 char** output_ptrs; // decoded input only
1318 uint64_t* slots; // max_files + 1
1319 // Gzip members, max_members each: raw DEFLATE in, inflated bytes out.
1320 char const** member_sources;
1321 uint64_t* member_source_bytes;
1322 uint64_t* member_output_bytes;
1323 char** member_outputs;
1324 // Outputs.
1325 uint64_t* member_inflated;
1326 nvcompStatus_t* member_statuses;
1327 uint32_t* crcs;
1328 unsigned long long* first_header;
1329 unsigned long long* kept;
1330 unsigned long long* header_count;
1331 int64_t* selected;
1332 char* first_char;
1333 };
1334
1335 static constexpr size_t inputs_bytes() noexcept {
1336 return 5 * max_files * 8 + 8 + 4 * max_members * 8;
1337 }
1338
1339 static constexpr size_t outputs_bytes() noexcept {
1340 return max_members * (8 + sizeof(nvcompStatus_t)) + max_files * (4 + 8 + 8 + 1) + 16;
1341 }
1342
1343 static constexpr size_t host_table_bytes() noexcept {
1344 return inputs_bytes() + outputs_bytes();
1345 }
1346
1347 static table_view view(char* base) {
1348 table_view view{};
1349 auto* at = base;
1350 auto take = [&]<typename T>(T*& field, size_t count) {
1351 field = reinterpret_cast<T*>(at);
1352 at += count * sizeof(T);
1353 };
1354 take(view.compressed_offsets, max_files);
1355 take(view.compressed_ptrs, max_files);
1356 take(view.compressed_bytes, max_files);
1357 take(view.output_ptrs, max_files);
1358 take(view.slots, max_files + 1);
1359 take(view.member_sources, max_members);
1360 take(view.member_source_bytes, max_members);
1361 take(view.member_output_bytes, max_members);
1362 take(view.member_outputs, max_members);
1363 take(view.member_inflated, max_members);
1364 take(view.first_header, max_files);
1365 take(view.kept, max_files);
1366 take(view.header_count, 1);
1367 take(view.selected, 1);
1368 take(view.member_statuses, max_members);
1369 take(view.crcs, max_files);
1370 take(view.first_char, max_files);
1371 return view;
1372 }
1373
1374 struct lane_state {
1375 bool decoded = false;
1376 std::optional<cuda::stream> stream;
1377 std::optional<cuda::event> uploaded, classified, released;
1378 std::optional<cuda::buffer<char, cuda::mr::host_accessible, cuda::mr::device_accessible>>
1379 pinned, host;
1380 std::optional<cuda::device_buffer<char>> payload, slots, tables, temp;
1381 std::optional<cuda::device_buffer<uint64_t>> headers;
1382 std::vector<size_t> files, fallback;
1383 std::vector<uint32_t> member_first; // each file's first member, then the member count
1384 std::array<bool, max_files> possible_members;
1385 uint64_t slot_bytes = 0;
1386 };
1387
1388 static table_view host_table(lane_state& lane) {
1389 return view(lane.host->data());
1390 }
1391
1392 static table_view device_tables(lane_state& lane) {
1393 return view(lane.tables->data());
1394 }
1395
1397 void allocate(lane_state& lane) {
1398 if (lane.slots) return;
1399 auto const s = cuda::stream_ref{*lane.stream};
1400 auto const device = s.device();
1401 // Compressed inputs and inflated outputs: memory the decompression engine accepts on
1402 // devices that have one.
1403 auto const inflate_buffer = [&](size_t bytes) -> cuda::device_buffer<char> {
1404 if (hardware_limit_ == 0) {
1405 return cuda::make_device_buffer<char>(s, device, bytes, cuda::no_init);
1406 }
1407 return cuda::make_buffer<char>(
1408 s, device_gzip::hardware_inflate_pool(device), bytes, cuda::no_init
1409 );
1410 };
1411 if (compressed_capacity_ != 0) {
1412 lane.pinned.emplace(
1413 s,
1414 device_pinned_pool(device, pinned_placement::host),
1415 compressed_capacity_,
1416 cuda::no_init
1417 );
1418 lane.payload.emplace(inflate_buffer(std::max(compressed_capacity_, slot_capacity_)));
1419 }
1420 lane.host.emplace(
1421 s, device_pinned_pool(device, pinned_placement::host), host_table_bytes(), cuda::no_init
1422 );
1423 // nvCOMP's pointer, size and status arrays: the engine needs them in its memory too.
1424 lane.tables.emplace(inflate_buffer(host_table_bytes()));
1425 lane.headers.emplace(
1426 cuda::make_device_buffer<uint64_t>(s, device, header_capacity_, cuda::no_init)
1427 );
1428 lane.temp.emplace(
1429 cuda::make_device_buffer<char>(
1430 s, device, std::max(nvcomp_temp_bytes_, select_temp_bytes_) + 1, cuda::no_init
1431 )
1432 );
1433 // Sixteen bytes of tail padding let the header scan read whole words. Allocated last, so
1434 // a lane with slots has every buffer.
1435 lane.slots.emplace(inflate_buffer(slot_capacity_ + 16));
1436 if (streaming_window_ != 0) {
1437 cudaStreamAttrValue policy{};
1438 policy.accessPolicyWindow.base_ptr = lane.slots->data();
1439 policy.accessPolicyWindow.num_bytes = streaming_window_;
1440 policy.accessPolicyWindow.hitRatio = 0.0f;
1441 policy.accessPolicyWindow.hitProp = cudaAccessPropertyNormal;
1442 policy.accessPolicyWindow.missProp = cudaAccessPropertyStreaming;
1443 auto const status =
1444 cudaStreamSetAttribute(s.get(), cudaStreamAttributeAccessPolicyWindow, &policy);
1445 if (status != cudaSuccess) throw std::runtime_error(cudaGetErrorString(status));
1446 }
1447 }
1448
1449 size_t workers_;
1450 // Lane reads wait on storage, so they keep a parallel file system's depth in flight.
1451 worker_pool readers_{
1452 ablated("read-depth") ? workers_ : std::max(workers_, parallel_read_depth)
1453 };
1454 size_t compressed_capacity_ = 0, slot_capacity_ = 0, streaming_window_ = 0;
1455 size_t hardware_limit_ = 0; // device_gzip::hardware_inflate_limit of the stream's device
1456 PFN_cuMemBatchDecompressAsync_v12060 batch_decompress_ = nullptr;
1457 std::vector<CUmemDecompressParams> engine_params_; // one batch's, reused
1458 size_t nvcomp_temp_bytes_ = 0, select_temp_bytes_ = 0;
1459 uint32_t header_capacity_ = 0;
1460 std::array<lane_state, lane_count> lanes_;
1461};
1462
1463} // namespace cuddl::detail
Normalises FASTA batches supplied compressed, uncompressed, or already inflated.
Result< void > finish(size_t lane_index, std::span< gzip_file_probe const > probes, std::vector< inflated_genome > &genomes, std::vector< size_t > &fallback)
Waits for lane's batch and splits it into genomes the device holds and ids the host loader must take.
static size_t lane_overhead(size_t slot_capacity) noexcept
Device bytes a lane needs besides its slots and compressed bytes.
size_t compressed_capacity() const noexcept
size_t batch_files(std::span< size_t const > ids, std::span< gzip_file_probe const > probes) const noexcept
How many ids from the front of ids one lane holds.
Result< void > release(size_t lane_index, cuda::stream_ref consumer)
Lets lane reuse its slots once consumer reaches this point.
size_t hardware_limit() const noexcept
Largest inflated file the device's decompression engine takes; 0 without one.
size_t slot_capacity() const noexcept
Result< void > wait_lanes(cuda::stream_ref consumer)
Completes lane work before callers release outstanding input buffers.
Result< size_t > submit(size_t lane_index, std::span< size_t const > ids, std::span< std::filesystem::path const > paths, std::span< gzip_file_probe const > probes, bool direct_reads)
Starts inflating files from the front of ids on lane.
device_fasta_pipeline(cuda::stream_ref stream, size_t slot_capacity, size_t compressed_capacity, size_t workers, bool hardware_inflate)
Result< void > submit_decoded(size_t lane_index, std::span< decoded_fasta const > sources)
Copies CPU-inflated FASTA into a lane and enqueues GPU normalisation.
#define CUDDL_CUDA_CALL(err)
Checks a CUDA runtime call and throws std::runtime_error on failure.
#define CUDDL_CUDA_TRY(...)
Propagates a CUDA error wrapped in cuddl::Result<void>.
Definition error.hpp:248
#define CUDDL_TRY(expr)
Propagates a cuddl::Result failure from the enclosing function (GNU statement expression).
Definition error.hpp:225
__device__ __forceinline__ bool is_whitespace(char c)
constexpr uint32_t block_size
size_t hardware_inflate_limit(int device)
Largest output, in bytes, that device's hardware decompression engine (Blackwell B200,...
__device__ uint32_t crc_combine(uint32_t first, uint32_t second, uint64_t second_bytes, uint32_t const *x2n)
crc32(A || B) from crc32(A), crc32(B) and |B| (zlib's crc32_combine).
cuda::device_memory_pool_ref hardware_inflate_pool(cuda::device_ref device)
Device memory device's decompression engine can read and write.
__device__ __forceinline__ uint32_t slot_containing(uint64_t const *slots, uint32_t files, uint64_t position)
The last of files slots that starts at or before position.
constexpr uint64_t kept_tile
Bytes a tile of kept_count_kernel covers: 8 aligned 16-byte loads per thread.
__device__ uint32_t crc_multiply(uint32_t a, uint32_t b)
a * b modulo the CRC-32 polynomial, bit-reflected (zlib's multmodp).
constexpr uint32_t crc_polynomial
__device__ __forceinline__ uint32_t kept_in_word(uint64_t word, uint32_t first, uint32_t last)
How many of word's bytes [first, last) kept_byte keeps.
size_t deflate_offset(char const *bytes, size_t size)
Where the DEFLATE stream starts in the size bytes of a gzip candidate (one member,...
PFN_cuMemBatchDecompressAsync_v12060 batch_decompress_entry()
The driver's cuMemBatchDecompressAsync, or nullptr where the driver lacks it.
_Pragma("unroll") for(uint32_t bit=0U
auto const high
Definition kernels.cuh:940
auto const shift
Definition kernels.cuh:442
auto const block
Definition top_k.cuh:236
@ pinned
Decompress into page-locked buffers, which the transfer engine reads directly.
auto const lane
Definition kernels.cuh:966
auto const count
Definition kernels.cuh:1031
@ all
Every pair of the layout, or with pass bits requested, those meeting the threshold.
@ host
Any node: for buffers host threads fill.
constexpr uint32_t block_size
Threads per CTA for the single-pair and cardinality reduction kernels.
Definition kernels.cuh:28
__global__ uint32_t uint32_t uint64_t uint32_t * ids
gzip_file_probe probe_gzip_file(std::filesystem::path const &path)
Reads the header and trailer of path, and of every block of a BGZF file.
__global__ size_t uint32_t const uint32_t const uint32_t uint32_t uint16_t uint32_t tile
Definition kernels.cuh:922
@ fasta
FASTA (> headers).
@ fastq
FASTQ (@ headers).
__global__ top_k_slots slots
Definition top_k.cuh:195
auto const total
Definition kernels.cuh:443
uint32_t bgzf_block_size(unsigned char const *header) noexcept
A BGZF block's total size from its 18-byte header, or 0 if it is not one: a gzip header with only FEX...
auto resolve_gzip_signature_scanner() -> bool(*)(char const *, size_t)
Selects the widest signature scanner the host supports.
auto const words
Definition top_k.cuh:204
__global__ uint16_t const uint32_t uint32_t uint32_t unsigned long long * work
Definition kernels.cuh:474
uint32_t crc32_combine(uint32_t a, uint32_t b, uint64_t length) noexcept
CRC32 of A followed by B, from CRC32(A), CRC32(B) and B's length, as zlib's crc32_combine: CRC32(A) t...
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.
Definition ablation.hpp:82
bool has_gzip_signature_portable(char const *data, size_t size)
True when data holds what could start another gzip member: 1f 8b 08, then a flag byte with its reserv...
auto const stride
Definition kernels.cuh:578
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 warps
Definition kernels.cuh:2495
auto *const bits
Definition kernels.cuh:1184
std::optional< std::pair< uint32_t, uint64_t > > walk_bgzf(unsigned char const *bytes, uint64_t size, Block &&block)
Calls block(deflate_at, deflate_bytes, inflated) for each non-empty block of the BGZF file in bytes.
auto const width
Definition kernels.cuh:963
auto const word
Definition top_k.cuh:241
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 {}.
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
Fallible API result: cuda::std::expected<T, Error> with cuDDL factories.
Definition error.hpp:118
The bytes a FASTA record keeps: everything but sequence whitespace.
__device__ bool operator()(char c) const
What a gzip file's header and trailer say, or that a file is plain FASTA, read without inflating it.
uint64_t host_bytes() const noexcept
Bytes the file takes in a lane's page-locked buffer.
One genome the device inflated: its compacted bases, resident on the device.
A span that is assumed to point to device-accessible memory.