4#include <cudaTypedefs.h>
5#include <nvcomp/deflate.h>
6#include <nvcomp/gzip.h>
12#include <condition_variable>
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>
36#include <cuda/iterator>
37#include <cuda/memory_pool>
40#if defined(__x86_64__) || defined(_M_X64)
41 #include <immintrin.h>
42#elif defined(__aarch64__) || defined(_M_ARM64)
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) {
80 if ((i < 4 || i > 9) && header[i] != expected[i])
return 0;
82 return (uint32_t{header[16]} | uint32_t{header[17]} << 8U) + 1U;
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) {
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;
98 uint32_t
shift = 1U << 31U, power = 1U << 30U;
99 for (uint64_t
bits = length * 8;
bits != 0;
bits >>= 1U) {
101 power = multiply(power, power);
103 return multiply(
shift, a) ^ b;
109template <
typename Block>
110[[nodiscard]] std::optional<std::pair<uint32_t, uint64_t>>
112 uint64_t at = 0,
total = 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);
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] = {};
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);
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));
151 name.find(
".fq") != std::string::npos || name.find(
".fastq") != std::string::npos;
152 if (header[0] ==
'>') {
154 probe.
plain = size < std::numeric_limits<uint32_t>::max();
155 probe.
isize =
static_cast<uint32_t
>(probe.
plain ? size : 0);
159 uint64_t at = 0,
total = 0;
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)) {
169 uint32_t block_crc = 0, inflated = 0;
170 std::memcpy(&block_crc, trailer, 4);
171 std::memcpy(&inflated, trailer + 4, 4);
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);
193 probe.
device = header[0] == 0x1f && header[1] == 0x8b && header[2] == 8 &&
194 (header[3] & 0xe4U) == 0 && probe.
isize != 0 && !
fastq;
202template <
typename Work>
204 std::atomic<size_t> next{0};
206 for (
size_t index = next++; index <
count; index = next++)
work(index);
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);
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;
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) {
237 for (; at + 67 <= size; at += 64) {
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))
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))
249 if ((first & second & third & flags) != 0)
return true;
255__attribute__((target(
"avx2"))) inline
bool has_gzip_signature_avx2(
char const* data,
size_t size) {
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))
265 auto const third = _mm256_cmpeq_epi8(
266 _mm256_loadu_si256(
reinterpret_cast<__m256i const*
>(data + at + 2)),
267 _mm256_set1_epi8(0x08)
269 auto const flags = _mm256_cmpeq_epi8(
271 _mm256_loadu_si256(
reinterpret_cast<__m256i const*
>(data + at + 3)),
272 _mm256_set1_epi8(
static_cast<char>(0xe0))
274 _mm256_setzero_si256()
277 _mm256_and_si256(_mm256_and_si256(first, flags), _mm256_and_si256(second, third));
278 if (!_mm256_testz_si256(
all,
all))
return true;
284#if defined(__aarch64__) || defined(_M_ARM64)
286inline bool has_gzip_signature_neon(
char const* data,
size_t size) {
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));
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) {
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;
311#if defined(__aarch64__) || defined(_M_ARM64)
312 return has_gzip_signature_neon;
319namespace device_gzip {
325 return c ==
'\n' || c ==
'\r' || c ==
' ' || c ==
'\t';
337 uint32_t m = 1U << 31U, p = 0;
341 if ((a & (m - 1)) == 0)
break;
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) {
360static __global__
void slot_init_kernel(
362 uint64_t
const*
slots,
364 unsigned long long* first_header,
365 unsigned long long* header_count
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];
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];
383 auto const t = threadIdx.x;
386 for (
int bit = 0; bit < 8; ++bit) {
387 entry = (entry & 1U) != 0 ? (entry >> 1U) ^
crc_polynomial : entry >> 1U;
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];
397 uint32_t power = 1U << 30U;
399 for (
int n = 1; n < 32; ++n) x2n[n] = power =
crc_multiply(power, power);
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;
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);
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);
418 c = table[3][c & 0xFFU] ^ table[2][(c >> 8U) & 0xFFU] ^ table[1][(c >> 16U) & 0xFFU] ^
422 for (; at < end; ++at)
byte(data[at]);
424 bytes[t] = end - begin;
427 if (t % (2 *
stride) == 0) {
429 bytes[t] += bytes[t +
stride];
433 if (t == 0) crcs[blockIdx.x] = crc[0];
438static __global__
void header_kernel(
443 unsigned long long*
count
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;
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;
467__device__ __forceinline__ uint32_t
469 uint32_t
low = 0,
high = files;
471 auto const middle = (
low +
high) / 2;
472 if (
slots[middle] <= position) {
483static __global__
void blank_header_kernel(
485 uint64_t
const* headers,
486 unsigned long long const*
count,
488 uint64_t
const*
slots,
490 unsigned long long* first_header
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;
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));
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;
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;
516 auto const zero_bytes = [](uint64_t x) {
517 return ~(((x & low7) + low7) | x | low7);
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));
534static __global__
void kept_count_kernel(
536 uint64_t
const*
slots,
538 unsigned long long* kept
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;
554 auto const slot = tile_slot;
556 for (
auto at =
tile * kept_tile + uint64_t{threadIdx.x} * 16; at < (
tile + 1) * kept_tile;
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};
562 for (uint32_t half = 0; half < 2; ++half) {
563 auto const base = at + 8 * half;
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
569 if (first >= last)
continue;
570 if (slot != straddles) {
576 atomicAdd(kept + head,
kept_in_word(halves[half], first, last));
579 for (
auto byte = first;
byte < last; ++byte) {
587 if (slot != straddles) {
588 auto const sum = block_reduce(reduce_storage).Sum(
count);
589 if (threadIdx.x == 0) atomicAdd(kept + slot, sum);
597static __global__
void slot_finish_kernel(
599 uint64_t
const*
slots,
600 unsigned long long const* first_header,
603 auto const begin =
slots[blockIdx.x] + 1;
604 auto const end =
slots[blockIdx.x + 1];
605 if (threadIdx.x == 0) {
607 while (at < end && (buffer[at] ==
'\n' || buffer[at] ==
'\r')) ++at;
608 first_char[blockIdx.x] = at < end ? buffer[at] :
'\0';
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';
619 void* address =
nullptr;
620 cudaDriverEntryPointQueryResult found{};
621 if (cudaGetDriverEntryPointByVersion(
622 "cuDeviceGetAttribute", &address, 12000U, cudaEnableDefault, &found
624 found != cudaDriverEntryPointSuccess) {
628 auto const attribute =
reinterpret_cast<decltype(&cuDeviceGetAttribute)
>(address);
630 attribute(&limit, CU_DEVICE_ATTRIBUTE_MEM_DECOMPRESS_MAXIMUM_LENGTH, device);
631 return status == CUDA_SUCCESS && limit > 0 ?
static_cast<size_t>(limit) : 0;
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;
650 return cuda::device_memory_pool_ref(pool);
655 void* address =
nullptr;
656 cudaDriverEntryPointQueryResult found{};
657 if (cudaGetDriverEntryPointByVersion(
658 "cuMemBatchDecompressAsync", &address, 12060U, cudaEnableDefault, &found
660 found != cudaDriverEntryPointSuccess) {
663 return reinterpret_cast<PFN_cuMemBatchDecompressAsync_v12060
>(address);
669 auto const flags =
static_cast<unsigned char>(bytes[3]);
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;
679 if ((flags & 2) != 0) at += 2;
680 return at + 8 < size ? at : 0;
715 static constexpr size_t lane_count = 6;
716 static constexpr size_t max_files = 4096;
719 static constexpr size_t max_members = 65536;
726 cuda::stream_ref stream,
727 size_t slot_capacity,
728 size_t compressed_capacity,
730 bool hardware_inflate
732 : workers_(std::max<size_t>(1, workers)),
733 compressed_capacity_(compressed_capacity),
734 slot_capacity_(slot_capacity),
736 static_cast<uint32_t>(std::clamp<size_t>(
737 slot_capacity / 4096,
739 std::numeric_limits<uint32_t>::max()
742 auto const options = nvcompBatchedDeflateDecompressDefaultOpts;
743 nvcomp_temp_bytes_ = 0;
744 if (compressed_capacity_ != 0 &&
745 nvcompBatchedDeflateDecompressGetTempSizeAsync(
747 std::min<size_t>(slot_capacity_, std::numeric_limits<uint32_t>::max()),
751 ) != nvcompSuccess) {
752 throw std::bad_alloc();
754 select_temp_bytes_ = 0;
755 auto const selected = compressed_capacity_ == 0 ? cub::DeviceSelect::If(
758 static_cast<char*
>(
nullptr),
759 static_cast<int64_t*
>(
nullptr),
760 static_cast<int64_t
>(slot_capacity_),
764 : cub::DeviceSelect::If(
767 static_cast<char*
>(
nullptr),
768 static_cast<char*
>(
nullptr),
769 static_cast<int64_t*
>(
nullptr),
770 static_cast<int64_t
>(slot_capacity_),
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());
783 compressed_capacity_ == 0
787 device.attribute(cuda::device_attributes::max_access_policy_window_size)
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);
804 return slot_capacity_;
808 return compressed_capacity_;
813 return hardware_limit_;
818 return slot_capacity / 4096 *
sizeof(uint64_t) + (
size_t{64} << 20);
823 std::span<size_t const> ids,
824 std::span<gzip_file_probe const> probes
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) {
833 compressed += probe.host_bytes();
834 slots += probe.isize + 1;
835 members += probe.members;
850 std::span<size_t const> ids,
851 std::span<std::filesystem::path const> paths,
852 std::span<gzip_file_probe const> probes,
855 auto&
lane = lanes_[lane_index];
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);
862 auto table = host_table(
lane);
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();
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;
875 table.compressed_offsets[file] = slot_at + 1;
876 slot_at += probe.host_bytes();
878 table.compressed_offsets[file] = compressed_at;
879 compressed_at += probe.compressed;
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);
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;
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);
899 if (failed)
return Err(Error::invalid_argument(
"cannot read reference inputs"));
901 lane.fallback.clear();
902 lane.member_first.clear();
903 engine_params_.clear();
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]);
912 for (
size_t file = 0; file < files; ++file) {
913 if (probes[
ids[file]].plain) accept(file);
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]);
923 auto const add_member = [&](
unsigned char const* deflate,
924 uint64_t deflate_bytes,
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{};
936 params.srcNumBytes = deflate_bytes;
938 params.dstNumBytes = size;
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);
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;
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]
961 auto* out =
lane.slots->data() + slot_at + 1;
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]);
972 bytes, probe.compressed, [&](uint64_t at, uint64_t length, uint32_t size) {
973 add_member(bytes + at, length, size, out);
979 auto const deflate_at = device_gzip::deflate_offset(
980 reinterpret_cast<char const*
>(bytes), probe.compressed
982 if (deflate_at == 0) {
983 lane.fallback.push_back(
ids[file]);
987 add_member(bytes + deflate_at, probe.compressed - deflate_at - 8, probe.isize, out);
991 auto const engine_members = members;
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;
998 auto const s = cuda::stream_ref{*
lane.stream};
999 auto*
const stream = s.get();
1000 auto const device_table = device_tables(
lane);
1004 if (plain_at != 0) {
1008 cuda::std::span{
lane.pinned->data(), plain_at},
1013 if (compressed_at != plain_at) {
1017 cuda::std::span{
lane.pinned->data() + plain_at, compressed_at - plain_at},
1025 cuda::std::span{
lane.host->data(), inputs_bytes()},
1030 auto const count =
static_cast<uint32_t
>(
lane.files.size());
1031 device_gzip::slot_init_kernel<<<(
count + 255) / 256, 256, 0, stream>>>(
1035 device_table.first_header,
1036 device_table.header_count
1039 if (engine_members != 0) {
1044 static_assert(nvcompSuccess == 0);
1045 static_assert(std::endian::native == std::endian::little);
1047 device_table.member_inflated, 0, engine_members *
sizeof(uint64_t), stream
1050 device_table.member_statuses, 0, engine_members *
sizeof(nvcompStatus_t), stream
1052 size_t error_index = 0;
1053 auto const submitted = batch_decompress_(
1054 engine_params_.data(), engine_params_.size(), 0, &error_index, stream
1056 if (submitted != CUDA_SUCCESS) {
1059 "hardware decompression launch failed: " + std::to_string(submitted)
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,
1076 reinterpret_cast<void* const*
>(device_table.member_outputs + first),
1078 device_table.member_statuses + first,
1081 if (inflated != nvcompSuccess) {
1083 Error::resource(
"nvCOMP DEFLATE launch failed: " + std::to_string(inflated))
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
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
1106 device_gzip::header_kernel<<<grid, 256, 0, stream>>>(
1109 lane.headers->data(),
1111 device_table.header_count
1114 device_gzip::blank_header_kernel<<<grid, 256, 0, stream>>>(
1116 lane.headers->data(),
1117 device_table.header_count,
1121 device_table.first_header
1124 device_gzip::slot_finish_kernel<<<count, device_gzip::block_size, 0, stream>>>(
1127 device_table.first_header,
1128 device_table.first_char
1132 cudaMemsetAsync(device_table.kept, 0, count *
sizeof(
unsigned long long), stream)
1134 device_gzip::kept_count_kernel<<<grid, device_gzip::block_size, 0, stream>>>(
1135 lane.slots->data(), device_table.slots, count, device_table.kept
1138 auto temp_bytes = select_temp_bytes_;
1141 cub::DeviceSelect::If(
1145 device_table.selected,
1146 static_cast<int64_t
>(slot_at),
1147 device_gzip::kept_byte{},
1154 cub::DeviceSelect::If(
1158 lane.payload->data(),
1159 device_table.selected,
1160 static_cast<int64_t
>(slot_at),
1161 device_gzip::kept_byte{},
1169 cuda::std::span{
lane.tables->data() + inputs_bytes(), outputs_bytes()},
1170 device_span<char>{
lane.host->data() + inputs_bytes(), outputs_bytes()}
1180 [[nodiscard]] Result<void>
1182 if (sources.empty() || sources.size() > max_files || compressed_capacity_ != 0) {
1183 return Err(Error::invalid_argument(
"invalid decoded FASTA batch"));
1185 auto&
lane = lanes_[lane_index];
1187 lane.uploaded->sync();
1188 lane.decoded =
true;
1190 lane.fallback.clear();
1191 auto const table = host_table(
lane);
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"));
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);
1205 table.slots[sources.size()] = bytes;
1206 auto const s = cuda::stream_ref{*
lane.stream};
1207 auto const device_table = device_tables(
lane);
1212 cuda::std::span{
lane.host->data(), inputs_bytes()},
1216 auto const count =
static_cast<uint32_t
>(sources.size());
1217 device_gzip::slot_init_kernel<<<(
count + 255) / 256, 256, 0, s.get()>>>(
1221 device_table.first_header,
1222 device_table.header_count
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;
1232 reinterpret_cast<void* const*
>(table.output_ptrs),
1233 reinterpret_cast<void const* const*
>(table.compressed_ptrs),
1234 table.compressed_bytes,
1242 return normalise(
lane, bytes);
1247 for (
auto&
lane : lanes_) {
1261 std::span<gzip_file_probe const> probes,
1262 std::vector<inflated_genome>& genomes,
1263 std::vector<size_t>& fallback
1265 auto&
lane = lanes_[lane_index];
1266 fallback.insert(fallback.end(),
lane.fallback.begin(),
lane.fallback.end());
1267 if (
lane.files.empty())
return Ok();
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;
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];
1285 succeeded && inflated == probes[id].isize && table.crcs[file] == probes[id].crc;
1288 table.first_char[file] !=
'@' && table.first_header[file] < table.slots[file + 1];
1291 auto const has_sequence = size > *table.header_count;
1292 if (!overflow && intact &&
fasta && has_sequence) {
1293 genomes.push_back({id, bases + offset, size});
1295 fallback.push_back(
id);
1299 if (
static_cast<int64_t
>(offset) != *table.selected) {
1300 return Err(Error::resource(
"device FASTA compaction lost track of its bytes"));
1314 uint64_t* compressed_offsets;
1315 char** compressed_ptrs;
1316 uint64_t* compressed_bytes;
1320 char const** member_sources;
1321 uint64_t* member_source_bytes;
1322 uint64_t* member_output_bytes;
1323 char** member_outputs;
1325 uint64_t* member_inflated;
1326 nvcompStatus_t* member_statuses;
1328 unsigned long long* first_header;
1329 unsigned long long* kept;
1330 unsigned long long* header_count;
1335 static constexpr size_t inputs_bytes() noexcept {
1336 return 5 * max_files * 8 + 8 + 4 * max_members * 8;
1339 static constexpr size_t outputs_bytes() noexcept {
1340 return max_members * (8 +
sizeof(nvcompStatus_t)) + max_files * (4 + 8 + 8 + 1) + 16;
1343 static constexpr size_t host_table_bytes() noexcept {
1344 return inputs_bytes() + outputs_bytes();
1347 static table_view view(
char* base) {
1350 auto take = [&]<
typename T>(T*& field,
size_t count) {
1351 field =
reinterpret_cast<T*
>(at);
1352 at +=
count *
sizeof(T);
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);
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>>
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;
1384 std::array<bool, max_files> possible_members;
1385 uint64_t slot_bytes = 0;
1388 static table_view host_table(lane_state& lane) {
1389 return view(
lane.host->data());
1392 static table_view device_tables(lane_state& lane) {
1393 return view(
lane.tables->data());
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();
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);
1407 return cuda::make_buffer<char>(
1408 s, device_gzip::hardware_inflate_pool(device), bytes, cuda::no_init
1411 if (compressed_capacity_ != 0) {
1412 lane.pinned.emplace(
1415 compressed_capacity_,
1418 lane.payload.emplace(inflate_buffer(std::max(compressed_capacity_, slot_capacity_)));
1421 s,
device_pinned_pool(device, pinned_placement::host), host_table_bytes(), cuda::no_init
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)
1429 cuda::make_device_buffer<char>(
1430 s, device, std::max(nvcomp_temp_bytes_, select_temp_bytes_) + 1, cuda::no_init
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;
1444 cudaStreamSetAttribute(s.get(), cudaStreamAttributeAccessPolicyWindow, &policy);
1445 if (status != cudaSuccess)
throw std::runtime_error(cudaGetErrorString(status));
1451 worker_pool readers_{
1452 ablated(
"read-depth") ? workers_ : std::max(workers_, parallel_read_depth)
1454 size_t compressed_capacity_ = 0, slot_capacity_ = 0, streaming_window_ = 0;
1455 size_t hardware_limit_ = 0;
1456 PFN_cuMemBatchDecompressAsync_v12060 batch_decompress_ =
nullptr;
1457 std::vector<CUmemDecompressParams> engine_params_;
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_;
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>.
#define CUDDL_TRY(expr)
Propagates a cuddl::Result failure from the enclosing function (GNU statement expression).
__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
@ pinned
Decompress into page-locked buffers, which the transfer engine reads directly.
@ 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.
__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
@ fasta
FASTA (> headers).
@ fastq
FASTQ (@ headers).
__global__ top_k_slots slots
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.
__global__ uint16_t const uint32_t uint32_t uint32_t unsigned long long * work
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.
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...
for(auto i=static_cast< size_t >(blockIdx.x) *block_size+threadIdx.x;i< total;i+=static_cast< size_t >(gridDim.x) *block_size)
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.
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 {}.
cuda::std::unexpected< Error > Err(Error error)
Failure return; converts to any Result<T> via cuda::std::unexpected.
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.
Fallible API result: cuda::std::expected<T, Error> with cuDDL factories.
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.