cuDDL
Loading...
Searching...
No Matches
fastx_sequence_file.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <libdeflate.h>
4
5#include <algorithm>
6#include <array>
7#include <bit>
8#include <condition_variable>
9#include <cstdint>
10#include <cstdlib>
11#include <cstring>
12#include <deque>
13#include <filesystem>
14#include <format>
15#include <fstream>
16#include <memory>
17#include <mutex>
18#include <optional>
19#include <span>
20#include <string>
21#include <string_view>
22#include <thread>
23#include <utility>
24#include <vector>
25
27#include <cuddl/detail/dna.hpp>
28#include <cuddl/error.hpp>
29
30#if defined(__aarch64__) || defined(_M_ARM64)
31 #include <arm_neon.h>
32#endif
33#if defined(__ARM_FEATURE_SVE)
34 #include <arm_sve.h>
35#elif defined(__x86_64__) || defined(_M_X64)
36 #include <immintrin.h>
37#endif
38
39#if defined(__linux__)
40 #include <fcntl.h>
41 #include <sys/mman.h>
42 #include <sys/stat.h>
43 #include <unistd.h>
44#endif
45
46namespace cuddl::detail {
47
50 char const* begin;
51 char const* end;
52};
53
56 uint64_t word;
57 uint64_t bases;
58};
59
61[[nodiscard]] inline constexpr bool fastx_is_sequence_whitespace(char ch) noexcept {
62 return ch == '\n' || ch == '\r' || ch == ' ' || ch == '\t';
63}
64
65// SIMD line scanners: https://github.com/tdortman/cuSBF
66// Source revision: ec678b0750b0067d4fc66e066c8cd7a67cc5e792
67/*
68Boost Software License - Version 1.0 - August 17th, 2003
69
70Permission is hereby granted, free of charge, to any person or organization
71obtaining a copy of the software and accompanying documentation covered by
72this license (the "Software") to use, reproduce, display, distribute,
73execute, and transmit the Software, and to prepare derivative works of the
74Software, and to permit third-parties to whom the Software is furnished to
75do so, all subject to the following:
76
77The copyright notices in the Software and this entire statement, including
78the above license grant, this restriction and the following disclaimer,
79must be included in all copies of the Software, in whole or in part, and
80all derivative works of the Software, unless such copies or derivative
81works are solely in the form of machine-executable object code generated by
82a source language processor.
83
84THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
85IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
86FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
87SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
88FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
89ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
90DEALINGS IN THE SOFTWARE.
91*/
92[[nodiscard]] inline size_t fastx_line_end_scalar(std::string_view data, size_t position) {
93 while (position < data.size() && data[position] != '\n' && data[position] != '\r') {
94 ++position;
95 }
96 return position;
97}
98
99#if (defined(__GNUC__) || defined(__clang__)) && defined(__x86_64__)
100[[nodiscard]] __attribute__((target("avx2"))) inline size_t
101fastx_line_end_avx2(std::string_view data, size_t position) {
102 const __m256i newline = _mm256_set1_epi8('\n');
103 const __m256i carriage_return = _mm256_set1_epi8('\r');
104 while (data.size() - position >= 32) {
105 const __m256i bytes =
106 _mm256_loadu_si256(reinterpret_cast<const __m256i*>(data.data() + position));
107 const auto mask = static_cast<uint32_t>(_mm256_movemask_epi8(_mm256_or_si256(
108 _mm256_cmpeq_epi8(bytes, newline), _mm256_cmpeq_epi8(bytes, carriage_return)
109 )));
110 if (mask != 0) {
111 return position + static_cast<size_t>(__builtin_ctz(mask));
112 }
113 position += 32;
114 }
115 return fastx_line_end_scalar(data, position);
116}
117#endif
118
119#if (defined(__GNUC__) || defined(__clang__)) && defined(__x86_64__)
120[[nodiscard]] __attribute__((target("avx512f,avx512bw"))) inline size_t
121fastx_line_end_avx512(std::string_view data, size_t position) {
122 while (data.size() - position >= 64) {
123 auto const bytes = _mm512_loadu_si512(data.data() + position);
124 auto const mask = _mm512_cmpeq_epi8_mask(bytes, _mm512_set1_epi8('\n')) |
125 _mm512_cmpeq_epi8_mask(bytes, _mm512_set1_epi8('\r'));
126 if (mask != 0) return position + std::countr_zero(mask);
127 position += 64;
128 }
129 return fastx_line_end_scalar(data, position);
130}
131#endif
132
133#if defined(__ARM_FEATURE_SVE)
135[[nodiscard]] inline size_t fastx_line_end_sve(std::string_view data, size_t position) {
136 while (position < data.size()) {
137 const svbool_t active = svwhilelt_b8(position, data.size());
138 const svuint8_t bytes =
139 svld1_u8(active, reinterpret_cast<const uint8_t*>(data.data() + position));
140 const svbool_t matches =
141 svorr_b_z(active, svcmpeq_n_u8(active, bytes, '\n'), svcmpeq_n_u8(active, bytes, '\r'));
142 if (svptest_any(active, matches)) {
143 return position + svcntp_b8(active, svbrkb_z(active, matches));
144 }
145 position += svcntb();
146 }
147 return position;
148}
149#endif
150
151#if defined(__aarch64__) || defined(_M_ARM64)
153[[nodiscard]] inline size_t fastx_line_end_neon(std::string_view data, size_t position) {
154 const uint8x16_t newline = vdupq_n_u8('\n');
155 const uint8x16_t carriage_return = vdupq_n_u8('\r');
156 while (data.size() - position >= 16) {
157 const uint8x16_t bytes = vld1q_u8(reinterpret_cast<const uint8_t*>(data.data() + position));
158 const uint8x16_t matches =
159 vorrq_u8(vceqq_u8(bytes, newline), vceqq_u8(bytes, carriage_return));
160 const uint64x2_t words = vreinterpretq_u64_u8(matches);
161 const uint64_t low = vgetq_lane_u64(words, 0);
162 if (low != 0) {
163 return position + (std::countr_zero(low) >> 3);
164 }
165 const uint64_t high = vgetq_lane_u64(words, 1);
166 if (high != 0) {
167 return position + 8 + (std::countr_zero(high) >> 3);
168 }
169 position += 16;
170 }
171 return fastx_line_end_scalar(data, position);
172}
173#endif
174
176
179 if (ablated("simd-parsing")) return fastx_line_end_scalar;
180#if defined(__ARM_FEATURE_SVE)
181 return fastx_line_end_sve;
182#elif defined(__aarch64__) || defined(_M_ARM64)
183 return fastx_line_end_neon;
184#elif (defined(__GNUC__) || defined(__clang__)) && defined(__x86_64__)
185 if (__builtin_cpu_supports("avx512bw")) return fastx_line_end_avx512;
186 return __builtin_cpu_supports("avx2") ? fastx_line_end_avx2 : fastx_line_end_scalar;
187#else
189#endif
190}
191
192static const fastx_line_end_fn line_end = resolve_fastx_line_end();
193
195[[nodiscard]] inline size_t fastx_line_end(std::string_view data, size_t position) {
196 return line_end(data, position);
197}
198
199#if defined(__linux__)
209[[nodiscard]] inline bool
210read_whole_file(int fd, char* out, size_t size, bool direct = false, size_t capacity = 0) noexcept {
211 constexpr size_t page = 4096;
212 size_t done = 0;
213 int const flags = direct && size > 64 * page ? ::fcntl(fd, F_GETFL) : -1;
214 if (flags != -1 && ::fcntl(fd, F_SETFL, flags | O_DIRECT) == 0) {
215 auto const whole = (size + page - 1) / page * page;
216 if (reinterpret_cast<uintptr_t>(out) % page == 0) {
217 // Whole pages straight into out; the last partial page too if out has room for it.
218 auto const direct_end = capacity >= whole ? whole : size / page * page;
219 while (done < direct_end && done < size) {
220 auto const got =
221 ::pread(fd, out + done, direct_end - done, static_cast<off_t>(done));
222 if (got <= 0) break;
223 done = std::min(size, done + static_cast<size_t>(got));
224 }
225 }
226 constexpr size_t bounce_bytes = size_t{8} << 20;
227 static thread_local std::unique_ptr<char, decltype(&std::free)> bounce{
228 static_cast<char*>(std::aligned_alloc(page, bounce_bytes)), &std::free
229 };
230 while (bounce && done < size && done % page == 0) {
231 auto const wanted = std::min(bounce_bytes, (size - done + page - 1) / page * page);
232 auto const got = ::pread(fd, bounce.get(), wanted, static_cast<off_t>(done));
233 if (got <= 0) break;
234 auto const used = std::min(size - done, static_cast<size_t>(got));
235 std::memcpy(out + done, bounce.get(), used);
236 done += used;
237 }
238 if (done < size) ::fcntl(fd, F_SETFL, flags);
239 }
240 while (done < size) {
241 auto const got = ::pread(fd, out + done, size - done, static_cast<off_t>(done));
242 if (got <= 0) break;
243 done += static_cast<size_t>(got);
244 }
245 ::close(fd);
246 return done == size;
247}
248#endif
249
255 public:
256 fastx_mapped_file() = default;
260 *this = std::move(other);
261 }
262
264 if (this != &other) {
265 release();
266 owned_storage_ = std::move(other.owned_storage_);
267 data_ = other.data_;
268 size_ = other.size_;
269 other.data_ = nullptr;
270 other.size_ = 0;
271#if defined(__linux__)
272 mapped_ = other.mapped_;
273 mapped_size_ = other.mapped_size_;
274 other.mapped_ = nullptr;
275 other.mapped_size_ = 0;
276#endif
277 }
278 return *this;
279 }
280
282 release();
283 }
284
297 load(std::string const& path, bool read = false, bool direct = false) {
298 auto buffer = std::make_unique<fastx_mapped_file>();
299 auto loaded = buffer->load_from_path(path, read, direct);
300 if (!loaded) return Err(loaded.error());
301 return buffer;
302 }
303
304#if defined(__linux__)
307 adopt(int fd, size_t size, std::string const& path, bool read, bool direct = false) {
308 auto buffer = std::make_unique<fastx_mapped_file>();
309 auto loaded = buffer->load_from_descriptor(fd, size, path, read, direct);
310 if (!loaded) return Err(loaded.error());
311 return buffer;
312 }
313#endif
314
316 [[nodiscard]] std::string_view data() const noexcept {
317 return std::string_view{data_, size_};
318 }
319
321 [[nodiscard]] bool empty() const noexcept {
322 return size_ == 0;
323 }
324
325 private:
326 struct free_deleter {
327 void operator()(char* data) const noexcept {
328 std::free(data);
329 }
330 };
332 std::unique_ptr<char, free_deleter> owned_storage_;
333
334 [[nodiscard]] static size_t page_rounded(size_t size) noexcept {
335 return (std::max<size_t>(size, 1) + 4095) / 4096 * 4096;
336 }
337
338 void allocate(size_t size) {
339 owned_storage_.reset(static_cast<char*>(std::aligned_alloc(4096, page_rounded(size))));
340 if (!owned_storage_) throw std::bad_alloc();
341 }
342 char const* data_{nullptr};
343 size_t size_{0};
344
345#if defined(__linux__)
346 void* mapped_{nullptr};
347 size_t mapped_size_{0};
348#endif
349
350 void release() {
351#if defined(__linux__)
352 if (mapped_ != nullptr) {
353 munmap(mapped_, mapped_size_);
354 mapped_ = nullptr;
355 mapped_size_ = 0;
356 }
357#endif
358 owned_storage_.reset();
359 data_ = nullptr;
360 size_ = 0;
361 }
362
363 [[nodiscard]] Result<void> load_from_path(
364 std::string const& path,
365 [[maybe_unused]] bool read,
366 [[maybe_unused]] bool direct
367 ) {
368#if defined(__linux__)
369 int const fd = ::open(path.c_str(), O_RDONLY);
370 if (fd == -1) {
371 return Err(Error::invalid_argument("Failed to open FASTA/FASTQ file: " + path));
372 }
373
374 struct stat file_status{};
375 if (::fstat(fd, &file_status) != 0 || file_status.st_size < 0) {
376 ::close(fd);
377 return Err(Error::invalid_argument("Failed to stat FASTA/FASTQ file: " + path));
378 }
379 return load_from_descriptor(
380 fd, static_cast<size_t>(file_status.st_size), path, read, direct
381 );
382#else
383 std::ifstream input(path, std::ios::binary);
384 if (!input.is_open()) {
385 return Err(Error::invalid_argument("Failed to open FASTA/FASTQ file: " + path));
386 }
387 input.seekg(0, std::ios::end);
388 auto const file_size = input.tellg();
389 if (file_size < 0) {
390 return Err(Error::invalid_argument("Failed to size FASTA/FASTQ file: " + path));
391 }
392 allocate(static_cast<size_t>(file_size));
393 input.seekg(0, std::ios::beg);
394 if (file_size > 0) {
395 input.read(owned_storage_.get(), static_cast<std::streamsize>(file_size));
396 if (!input) {
397 return Err(Error::invalid_argument("Failed to read FASTA/FASTQ file: " + path));
398 }
399 }
400 data_ = owned_storage_.get();
401 size_ = static_cast<size_t>(file_size);
402 return Ok();
403#endif
404 }
405
406#if defined(__linux__)
407 [[nodiscard]] Result<void>
408 load_from_descriptor(int fd, size_t size, std::string const& path, bool read, bool direct) {
409 if (size == 0) {
410 ::close(fd);
411 return Ok();
412 }
413 if (!read) {
414 mapped_ = ::mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0);
415 ::close(fd);
416 if (mapped_ == MAP_FAILED) {
417 mapped_ = nullptr;
418 return Err(Error::invalid_argument("Failed to mmap FASTA/FASTQ file: " + path));
419 }
420 mapped_size_ = size;
421 data_ = static_cast<char const*>(mapped_);
422 size_ = size;
423 return Ok();
424 }
425 allocate(size);
426 if (!read_whole_file(fd, owned_storage_.get(), size, direct, page_rounded(size))) {
427 owned_storage_.reset();
428 return Err(Error::invalid_argument("Failed to read FASTA/FASTQ file: " + path));
429 }
430 data_ = owned_storage_.get();
431 size_ = size;
432 return Ok();
433 }
434#endif
435};
436
438enum class fastx_format : uint8_t {
440 unknown,
442 fasta,
444 fastq,
445};
446
456 public:
463 explicit fastx_fastq_reader(std::string_view data, std::string source_name = "<buffer>")
464 : data_(data), source_name_(std::move(source_name)) {}
465
471 [[nodiscard]] Result<bool> append_next_record(std::string& sequence) {
472 auto header = CUDDL_TRY(read_header_line());
473 if (header.empty()) {
474 return false;
475 }
476
477 char const tag = header.front();
478 if (format_ == fastx_format::unknown) {
479 if (tag == '>') {
480 format_ = fastx_format::fasta;
481 } else if (tag == '@') {
482 format_ = fastx_format::fastq;
483 } else {
484 return Err(parse_error("expected FASTA or FASTQ header"));
485 }
486 }
487
488 char const expected = format_ == fastx_format::fasta ? '>' : '@';
489 if (tag != expected) {
490 return Err(parse_error("mixed FASTA and FASTQ records are not supported"));
491 }
492 if (format_ != fastx_format::fastq) {
493 return Err(parse_error("mixed FASTA and FASTQ records are not supported"));
494 }
495 auto const base = sequence.size();
496 CUDDL_TRY(read_fastq_sequence(sequence, base));
497 return true;
498 }
499
500 private:
501 std::string_view data_;
502 std::string source_name_;
503 size_t position_{0};
505 uint64_t line_number_{};
506
507 [[nodiscard]] Error parse_error(std::string_view message) const {
509 std::format("{}:{}: {}", source_name_, line_number_, message)
510 );
511 }
512
513 [[nodiscard]] std::string_view read_line() {
514 if (position_ >= data_.size()) {
515 return {};
516 }
517
518 size_t const end = fastx_line_end(data_, position_);
519
520 std::string_view const line = data_.substr(position_, end - position_);
521 position_ = end;
522 if (position_ < data_.size() && data_[position_] == '\r') {
523 ++position_;
524 }
525 if (position_ < data_.size() && data_[position_] == '\n') {
526 ++position_;
527 }
528 ++line_number_;
529 return line;
530 }
531
532 [[nodiscard]] Result<std::string_view> read_header_line() {
533 while (position_ < data_.size()) {
534 std::string_view const line = read_line();
535 if (!line.empty()) {
536 return line;
537 }
538 }
539 return std::string_view{};
540 }
541
542 [[nodiscard]] Result<void> read_fastq_sequence(std::string& sequence, size_t base) {
543 while (position_ < data_.size()) {
544 std::string_view const line = read_line();
545 if (!line.empty() && line.front() == '+') {
546 return read_fastq_qualities(sequence.size() - base);
547 }
548 sequence.append(line.data(), line.size());
549 }
550 return Err(parse_error("unterminated FASTQ record: missing '+' separator"));
551 }
552
553 [[nodiscard]] Result<void> read_fastq_qualities(uint64_t expected_length) {
554 uint64_t quality_length = 0;
555 while (quality_length < expected_length && position_ < data_.size()) {
556 std::string_view const line = read_line();
557 quality_length += line.size();
558 if (quality_length > expected_length) {
559 return Err(parse_error("FASTQ quality length exceeds sequence length"));
560 }
561 }
562 if (quality_length != expected_length) {
563 return Err(parse_error("FASTQ quality length does not match sequence length"));
564 }
565 return Ok();
566 }
567};
568
569// Heap ownership keeps extent addresses stable, including short decompressed strings.
571 std::unique_ptr<fastx_mapped_file> file;
572 std::string_view input;
573 std::string decompressed;
574 std::string sequence;
575 std::vector<fastx_sequence_extent> extents;
576 // Set when the caller supplied memory for the decompressed bytes, which then live outside
577 // this object and stay valid as long as the caller keeps them. `decompressed` stays empty.
578 char* decompressed_target = nullptr;
580 std::shared_ptr<void> storage_owner;
581 // Set by pack_fastx_sequence_file, which consumes `extents`: runs of at least k bases as
582 // two-bit words, sixteen bases each, every run starting on a word of its own.
583 uint32_t const* words = nullptr;
584 std::vector<fastx_packed_record> packed;
585};
586
591[[nodiscard]] inline uint32_t pack_bases16_portable(char const* bases, size_t count, uint32_t& word) {
592 if (count < 16) {
593 uint32_t bad = 0;
594 word = 0;
595 for (uint32_t j = 0; j < 16; ++j) {
596 auto const symbol = j < count ? encode_base(bases[j]) : uint8_t{0xFF};
597 word |= static_cast<uint32_t>(symbol & 3U) << (2 * j);
598 bad |= static_cast<uint32_t>(symbol == 0xFFU) << j;
599 }
600 return bad;
601 }
602 uint32_t bad = 0;
603 word = 0;
604 for (uint32_t half = 0; half < 2; ++half) {
605 uint64_t x;
606 std::memcpy(&x, bases + 8 * half, sizeof(x));
607 constexpr uint64_t low7 = 0x7F7F7F7F7F7F7F7FULL;
608 // High bit of each byte of v that is zero, without carries between bytes.
609 auto const zero = [](uint64_t v) { return ~(((v & low7) + low7) | v | low7); };
610 auto const upper = x & 0xDFDFDFDFDFDFDFDFULL;
611 auto const valid = zero(upper ^ 0x4141414141414141ULL) |
612 zero(upper ^ 0x4343434343434343ULL) |
613 zero(upper ^ 0x4747474747474747ULL) | zero(upper ^ 0x5454545454545454ULL);
614 auto codes = (x >> 1) & 0x0303030303030303ULL;
615 codes = (codes | (codes >> 6)) & 0x000F000F000F000FULL;
616 codes = (codes | (codes >> 12)) & 0x000000FF000000FFULL;
617 codes = (codes | (codes >> 24)) & 0xFFFFULL;
618 word |= static_cast<uint32_t>(codes) << (16 * half);
619 // Byte j's flag lands on bit j.
620 bad |= static_cast<uint32_t>(
621 ((~valid & 0x8080808080808080ULL) >> 7) * 0x0102040810204080ULL >> 56
622 )
623 << (8 * half);
624 }
625 return bad;
626}
627
629inline uint64_t pack_bases_pieces(char const* bases, size_t count, uint32_t* words, size_t width) {
630 uint64_t bad = 0;
631 for (size_t piece = 0; piece < width / 16; ++piece) {
632 auto const left = count > 16 * piece ? count - 16 * piece : 0;
633 bad |= uint64_t{pack_bases16_portable(bases + 16 * piece, left, words[piece])} << (16 * piece);
634 }
635 return bad;
636}
637
638inline uint64_t pack_bases_portable(char const* bases, size_t count, uint32_t* words) {
639 return pack_bases16_portable(bases, count, words[0]);
640}
641
642#if (defined(__x86_64__) || defined(__i386__)) && (defined(__GNUC__) || defined(__clang__))
644__attribute__((target("avx2"))) inline uint64_t
645pack_bases_avx2(char const* bases, size_t count, uint32_t* words) {
646 if (count < 32) return pack_bases_pieces(bases, count, words, 32);
647 auto const x = _mm256_loadu_si256(reinterpret_cast<__m256i const*>(bases));
648 auto const upper = _mm256_and_si256(x, _mm256_set1_epi8(static_cast<char>(0xDF)));
649 auto const valid = _mm256_or_si256(
650 _mm256_or_si256(
651 _mm256_cmpeq_epi8(upper, _mm256_set1_epi8('A')),
652 _mm256_cmpeq_epi8(upper, _mm256_set1_epi8('C'))
653 ),
654 _mm256_or_si256(
655 _mm256_cmpeq_epi8(upper, _mm256_set1_epi8('G')),
656 _mm256_cmpeq_epi8(upper, _mm256_set1_epi8('T'))
657 )
658 );
659 // Codes weighted 1, 4, 16, 64 and summed: every four bases make one byte.
660 auto const codes = _mm256_and_si256(_mm256_srli_epi16(x, 1), _mm256_set1_epi8(3));
661 auto const pairs = _mm256_maddubs_epi16(codes, _mm256_set1_epi16(0x0401));
662 auto const quads = _mm256_madd_epi16(pairs, _mm256_set1_epi32(0x00100001));
663 // Each lane's four bytes, then both lanes into one quadword.
664 auto const gathered = _mm256_shuffle_epi8(
665 quads, _mm256_setr_epi8(0, 4, 8, 12, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
666 0, 4, 8, 12, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1)
667 );
668 auto const both = _mm256_permutevar8x32_epi32(gathered, _mm256_setr_epi32(0, 4, 0, 0, 0, 0, 0, 0));
669 _mm_storel_epi64(reinterpret_cast<__m128i*>(words), _mm256_castsi256_si128(both));
670 return ~static_cast<uint64_t>(static_cast<uint32_t>(_mm256_movemask_epi8(valid))) &
671 0xFFFFFFFFULL;
672}
673
675__attribute__((target("avx2,avx512f,avx512bw"))) inline uint64_t
676pack_bases_avx512(char const* bases, size_t count, uint32_t* words) {
677 if (count < 64) {
678 // A short tail packs as two AVX2 halves.
679 auto const low = pack_bases_avx2(bases, count, words);
680 auto const high = pack_bases_avx2(bases + 32, count > 32 ? count - 32 : 0, words + 2);
681 return low | (high << 32);
682 }
683 auto const x = _mm512_loadu_si512(bases);
684 auto const upper = _mm512_and_si512(x, _mm512_set1_epi8(static_cast<char>(0xDF)));
685 auto const valid = _mm512_cmpeq_epi8_mask(upper, _mm512_set1_epi8('A')) |
686 _mm512_cmpeq_epi8_mask(upper, _mm512_set1_epi8('C')) |
687 _mm512_cmpeq_epi8_mask(upper, _mm512_set1_epi8('G')) |
688 _mm512_cmpeq_epi8_mask(upper, _mm512_set1_epi8('T'));
689 // Codes weighted 1, 4, 16, 64 and summed: every four bases make one byte.
690 auto const codes = _mm512_and_si512(_mm512_srli_epi16(x, 1), _mm512_set1_epi8(3));
691 auto const pairs = _mm512_maddubs_epi16(codes, _mm512_set1_epi16(0x0401));
692 auto const quads = _mm512_madd_epi16(pairs, _mm512_set1_epi32(0x00100001));
693 _mm_storeu_si128(reinterpret_cast<__m128i*>(words), _mm512_cvtepi32_epi8(quads));
694 return ~static_cast<uint64_t>(valid);
695}
696#endif
697
698#if defined(__aarch64__) || defined(_M_ARM64)
702inline uint64_t pack_bases_neon(char const* bases, size_t count, uint32_t* words) {
703 if (count < 64) return pack_bases_pieces(bases, count, words, 64);
704 static constexpr int8_t shift_table[16] = {0, 2, 4, 6, 0, 2, 4, 6, 0, 2, 4, 6, 0, 2, 4, 6};
705 auto const shifts = vld1q_s8(shift_table);
706 for (size_t piece = 0; piece < 4; ++piece) {
707 auto const x = vld1q_u8(reinterpret_cast<uint8_t const*>(bases) + 16 * piece);
708 auto const upper = vandq_u8(x, vdupq_n_u8(0xDF));
709 auto const valid = vorrq_u8(
710 vorrq_u8(vceqq_u8(upper, vdupq_n_u8('A')), vceqq_u8(upper, vdupq_n_u8('C'))),
711 vorrq_u8(vceqq_u8(upper, vdupq_n_u8('G')), vceqq_u8(upper, vdupq_n_u8('T')))
712 );
713 // One nibble per byte: set when the byte is not a base.
714 auto const invalid = vget_lane_u64(
715 vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(vmvnq_u8(valid)), 4)), 0
716 );
717 // Shift each code into its slot, then add four neighbours: their bits are disjoint.
718 auto const codes = vshlq_u8(vandq_u8(vshrq_n_u8(x, 1), vdupq_n_u8(3)), shifts);
719 auto const pairs = vpaddq_u8(codes, codes);
720 words[piece] = vgetq_lane_u32(vreinterpretq_u32_u8(vpaddq_u8(pairs, pairs)), 0);
721 if (invalid != 0) {
722 return ~uint64_t{0} << (16 * piece + static_cast<size_t>(std::countr_zero(invalid)) / 4);
723 }
724 }
725 return 0;
726}
727#endif
728
734 uint64_t (*pack)(char const* bases, size_t count, uint32_t* words);
735 size_t width;
736};
737
739[[nodiscard]] inline bases_packer resolve_bases_packer() {
740 if (ablated("simd-parsing")) return {pack_bases_portable, 16};
741#if (defined(__x86_64__) || defined(__i386__)) && (defined(__GNUC__) || defined(__clang__))
742 if (__builtin_cpu_supports("avx512bw")) return {pack_bases_avx512, 64};
743 if (__builtin_cpu_supports("avx2")) return {pack_bases_avx2, 32};
744#endif
745#if defined(__aarch64__) || defined(_M_ARM64)
746 return {pack_bases_neon, 64};
747#else
748 return {pack_bases_portable, 16};
749#endif
750}
751
752static const bases_packer packer = resolve_bases_packer();
753
761inline void pack_fastx_sequence_file(fastx_sequence_file& result, uint32_t k) {
762 if (result.extents.empty() || k < 16) return;
763 auto* const out = const_cast<char*>(result.extents.front().begin);
764 uint64_t written = 0;
765 for (auto const& extent : result.extents) {
766 auto const* next = extent.begin;
767 auto const* const end = extent.end;
768 while (next < end) {
769 while (next < end && encode_base(*next) == 0xFF) ++next;
770 auto const* const start = next;
771 auto const first = written;
772 while (next < end) {
773 uint32_t words[4];
774 auto const bad = packer.pack(next, static_cast<size_t>(end - next), words);
775 auto const bases =
776 bad == 0 ? packer.width : static_cast<size_t>(std::countr_zero(bad));
777 // A partial step ends the run; a short run's partial words are never written.
778 if (bases == packer.width || static_cast<size_t>(next + bases - start) >= k) {
779 auto const count = (bases + 15) / 16;
780 std::memcpy(out + written * sizeof(uint32_t), words, count * sizeof(uint32_t));
781 written += count;
782 }
783 next += bases;
784 if (bases != packer.width) break;
785 }
786 auto const bases = static_cast<uint64_t>(next - start);
787 if (bases >= k) {
788 result.packed.push_back({first, bases});
789 } else {
790 written = first;
791 }
792 }
793 }
794 result.words = reinterpret_cast<uint32_t const*>(out);
795 result.extents.clear();
796}
797
803 char* data = nullptr;
804 size_t capacity = 0;
805 std::shared_ptr<void> owner;
806};
807
813 decompression_target (*acquire)(void* context, size_t bytes) = nullptr;
814 void* context = nullptr;
816 bool direct_reads = false;
817
818 [[nodiscard]] decompression_target request(size_t bytes) const {
819 return acquire != nullptr ? acquire(context, bytes) : decompression_target{};
820 }
821};
822
825 libdeflate_decompressor* handle{libdeflate_alloc_decompressor()};
830 libdeflate_free_decompressor(handle);
831 }
832};
833
836[[nodiscard]] inline libdeflate_result decompress_gzip_member(
837 libdeflate_decompressor* decoder,
838 std::string_view input,
839 char* output,
840 size_t capacity,
841 size_t& consumed,
842 size_t& produced
843) {
844 auto const byte = [&](size_t i) {
845 return static_cast<uint32_t>(static_cast<unsigned char>(input[i]));
846 };
847 if (input.size() < 18 || byte(0) != 0x1f || byte(1) != 0x8b || byte(2) != 8 ||
848 (byte(3) & 0xe0) != 0) {
849 return LIBDEFLATE_BAD_DATA;
850 }
851 auto const flags = byte(3);
852 auto const end = input.size() - 8;
853 size_t at = 10;
854 if ((flags & 4) != 0) {
855 if (end - at < 2) return LIBDEFLATE_BAD_DATA;
856 auto const length = byte(at) | (byte(at + 1) << 8);
857 at += 2;
858 if (length > end - at) return LIBDEFLATE_BAD_DATA;
859 at += length;
860 }
861 for (auto const flag : {8U, 16U}) {
862 if ((flags & flag) != 0) {
863 auto const terminator = input.find('\0', at);
864 if (terminator == std::string_view::npos || terminator >= end) {
865 return LIBDEFLATE_BAD_DATA;
866 }
867 at = terminator + 1;
868 }
869 }
870 if ((flags & 2) != 0) {
871 if (end - at < 2) return LIBDEFLATE_BAD_DATA;
872 at += 2;
873 }
874 size_t deflate_bytes = 0;
875 auto const status = libdeflate_deflate_decompress_ex(
876 decoder, input.data() + at, end - at, output, capacity, &deflate_bytes, &produced
877 );
878 if (status != LIBDEFLATE_SUCCESS) return status;
879 auto const trailer = at + deflate_bytes;
880 auto const size = byte(trailer + 4) | (byte(trailer + 5) << 8) | (byte(trailer + 6) << 16) |
881 (byte(trailer + 7) << 24);
882 if (static_cast<uint32_t>(produced) != size) return LIBDEFLATE_BAD_DATA;
883 consumed = trailer + 8;
884 return LIBDEFLATE_SUCCESS;
885}
886
892[[nodiscard]] inline Result<void>
893gunzip_members_into(std::string_view input, std::string& output, std::string const& path) {
894 static thread_local shared_gzip_decompressor shared;
895 if (shared.handle == nullptr) {
896 return Err(Error::resource("cannot allocate gzip decompressor"));
897 }
898 // Genomes compress around 3x; guessing low costs a realloc per member, not correctness.
899 if (output.empty()) output.resize(std::max<size_t>(input.size() * 4, 1U << 16));
900 size_t consumed = 0;
901 size_t produced = 0;
902 while (consumed < input.size()) {
903 size_t member_in = 0;
904 size_t member_out = 0;
905 auto const status = decompress_gzip_member(
906 shared.handle,
907 input.substr(consumed),
908 output.data() + produced,
909 output.size() - produced,
910 member_in,
911 member_out
912 );
913 if (status == LIBDEFLATE_INSUFFICIENT_SPACE) {
914 output.resize(output.size() * 2 + 1);
915 continue;
916 }
917 if (status != LIBDEFLATE_SUCCESS) {
918 return Err(Error::invalid_argument("gzip FASTX error: invalid stream: " + path));
919 }
920 consumed += member_in;
921 produced += member_out;
922 }
923 output.resize(produced);
924 return Ok();
925}
926
932 std::string_view input,
934 bool& insufficient,
935 std::string const& path
936) {
937 insufficient = false;
938 static thread_local shared_gzip_decompressor shared;
939 if (shared.handle == nullptr) {
940 return Err(Error::resource("cannot allocate gzip decompressor"));
941 }
942 size_t consumed = 0;
943 size_t produced = 0;
944 while (consumed < input.size()) {
945 size_t member_in = 0;
946 size_t member_out = 0;
947 auto const status = decompress_gzip_member(
948 shared.handle,
949 input.substr(consumed),
950 target.data + produced,
951 target.capacity - produced,
952 member_in,
953 member_out
954 );
955 if (status == LIBDEFLATE_INSUFFICIENT_SPACE) {
956 // A caller-supplied buffer cannot grow, so the loader falls back instead.
957 insufficient = true;
958 return Result<size_t>::ok(0);
959 }
960 if (status != LIBDEFLATE_SUCCESS) {
961 return Err(Error::invalid_argument("gzip FASTX error: invalid stream: " + path));
962 }
963 consumed += member_in;
964 produced += member_out;
965 }
966 return produced;
967}
968
977[[nodiscard]] inline uint64_t gzip_decompressed_size(std::string const& path) {
978 auto file = std::fopen(path.c_str(), "rb");
979 if (file == nullptr) return 0;
980 uint64_t size = 0;
981 if (std::fseek(file, -4, SEEK_END) == 0) {
982 unsigned char trailer[4] = {};
983 if (std::fread(trailer, 1, sizeof(trailer), file) == sizeof(trailer)) {
984 size = static_cast<uint64_t>(trailer[0]) | (static_cast<uint64_t>(trailer[1]) << 8) |
985 (static_cast<uint64_t>(trailer[2]) << 16) |
986 (static_cast<uint64_t>(trailer[3]) << 24);
987 }
988 }
989 std::fclose(file);
990 return size;
991}
992
993[[nodiscard]] inline size_t gzip_size_hint(std::string_view input) {
994 if (input.size() < 8) return 0;
995 auto const byte = [&](size_t index) {
996 return static_cast<uint32_t>(static_cast<unsigned char>(input[index]));
997 };
998 auto const tail = input.size() - 4;
999 return byte(tail) | (byte(tail + 1) << 8) | (byte(tail + 2) << 16) | (byte(tail + 3) << 24);
1000}
1001
1002#if defined(__x86_64__) || defined(__i386__)
1003 #include <immintrin.h>
1004 #include <tmmintrin.h>
1005#elif defined(__ARM_NEON)
1006 #include <arm_neon.h>
1007#endif
1008
1009namespace compact_impl {
1010
1017 alignas(64) uint8_t control[256][16];
1018 uint8_t kept[256];
1020 for (int mask = 0; mask < 256; ++mask) {
1021 int at = 0;
1022 for (int bit = 0; bit < 8; ++bit) {
1023 if ((mask & (1 << bit)) == 0) control[mask][at++] = static_cast<uint8_t>(bit);
1024 }
1025 for (; at < 16; ++at) control[mask][at] = 0x80U;
1026 kept[mask] = static_cast<uint8_t>(8 - __builtin_popcount(static_cast<unsigned>(mask)));
1027 }
1028 }
1029};
1030
1031[[nodiscard]] inline compact_shuffle_table const& compact_shuffle() noexcept {
1032 static compact_shuffle_table const table;
1033 return table;
1034}
1035
1036#if defined(__ARM_NEON)
1038[[nodiscard]] inline unsigned compact_mask(uint8x16_t flags) noexcept {
1039 constexpr std::uint8_t weights[16] = {1, 2, 4, 8, 16, 32, 64, 128, 1, 2, 4, 8, 16, 32, 64, 128};
1040 auto const weighted = vandq_u8(flags, vld1q_u8(weights));
1041 return static_cast<unsigned>(vaddv_u8(vget_low_u8(weighted))) |
1042 (static_cast<unsigned>(vaddv_u8(vget_high_u8(weighted))) << 8U);
1043}
1044#endif
1045
1046} // namespace compact_impl
1047
1048#if defined(__ARM_NEON)
1049inline char* compact_block_neon(uint8x16_t block, uint8x16_t whitespace, char* out) {
1050 if (vmaxvq_u8(whitespace) == 0) {
1051 vst1q_u8(reinterpret_cast<uint8_t*>(out), block);
1052 return out + 16;
1053 }
1054 auto const& table = compact_impl::compact_shuffle();
1055 auto const flags = compact_impl::compact_mask(whitespace);
1056 auto const low = flags & 0xFFU;
1057 auto const high = (flags >> 8U) & 0xFFU;
1058 auto const packed_low = vqtbl1q_u8(block, vld1q_u8(table.control[low]));
1059 auto const packed_high = vqtbl1q_u8(vextq_u8(block, block, 8), vld1q_u8(table.control[high]));
1060 // Half-width stores cannot overwrite input beyond the loaded block when compacting in place.
1061 vst1_u8(reinterpret_cast<uint8_t*>(out), vget_low_u8(packed_low));
1062 out += table.kept[low];
1063 vst1_u8(reinterpret_cast<uint8_t*>(out), vget_low_u8(packed_high));
1064 return out + table.kept[high];
1065}
1066
1067inline char* compact_sequence_whitespace_neon(char const* first, char const* last, char* out) {
1068 auto const newline = vdupq_n_u8('\n');
1069 auto const carriage = vdupq_n_u8('\r');
1070 auto const space = vdupq_n_u8(' ');
1071 auto const tab = vdupq_n_u8('\t');
1072 auto const* at = first;
1073 while (static_cast<size_t>(last - at) >= 16) {
1074 auto const block = vld1q_u8(reinterpret_cast<std::uint8_t const*>(at));
1075 auto const whitespace = vorrq_u8(
1076 vorrq_u8(vceqq_u8(block, newline), vceqq_u8(block, carriage)),
1077 vorrq_u8(vceqq_u8(block, space), vceqq_u8(block, tab))
1078 );
1079 out = compact_block_neon(block, whitespace, out);
1080 at += 16;
1081 }
1082 for (; at != last; ++at) {
1083 if (!fastx_is_sequence_whitespace(*at)) *out++ = *at;
1084 }
1085 return out;
1086}
1087#endif
1088
1089#if defined(__x86_64__) || defined(__i386__)
1094__attribute__((target("avx2"))) inline char*
1095compact_block_avx2(__m256i block, unsigned flags, char* out) {
1096 auto const& table = compact_impl::compact_shuffle();
1097 auto const low_first = flags & 0xFFU;
1098 auto const low_second = (flags >> 8U) & 0xFFU;
1099 auto const high_first = (flags >> 16U) & 0xFFU;
1100 auto const high_second = (flags >> 24U) & 0xFFU;
1101 auto const control_first = _mm256_set_m128i(
1102 _mm_load_si128(reinterpret_cast<__m128i const*>(table.control[high_first])),
1103 _mm_load_si128(reinterpret_cast<__m128i const*>(table.control[low_first]))
1104 );
1105 auto const control_second = _mm256_set_m128i(
1106 _mm_load_si128(reinterpret_cast<__m128i const*>(table.control[high_second])),
1107 _mm_load_si128(reinterpret_cast<__m128i const*>(table.control[low_second]))
1108 );
1109 auto const packed_first = _mm256_shuffle_epi8(block, control_first);
1110 auto const packed_second = _mm256_shuffle_epi8(_mm256_bsrli_epi128(block, 8), control_second);
1111 // A lane holds the kept bytes of two halves, which are eight bytes apart in the vector and
1112 // adjacent in the output. The halves leave the vector in the order 0-7, 16-23, 8-15, 24-31,
1113 // and the output wants them in byte order, so each half is stored where its own count puts it.
1114 _mm_storel_epi64(reinterpret_cast<__m128i*>(out), _mm256_castsi256_si128(packed_first));
1115 out += table.kept[low_first];
1116 _mm_storel_epi64(reinterpret_cast<__m128i*>(out), _mm256_castsi256_si128(packed_second));
1117 out += table.kept[low_second];
1118 _mm_storel_epi64(reinterpret_cast<__m128i*>(out), _mm256_extracti128_si256(packed_first, 1));
1119 out += table.kept[high_first];
1120 _mm_storel_epi64(reinterpret_cast<__m128i*>(out), _mm256_extracti128_si256(packed_second, 1));
1121 out += table.kept[high_second];
1122 return out;
1123}
1124
1126__attribute__((target("avx2"))) inline char*
1127compact_sequence_whitespace_avx2(char const* first, char const* last, char* out) {
1128 auto const newline = _mm256_set1_epi8('\n');
1129 auto const carriage = _mm256_set1_epi8('\r');
1130 auto const space = _mm256_set1_epi8(' ');
1131 auto const tab = _mm256_set1_epi8('\t');
1132 auto const* at = first;
1133 while (static_cast<size_t>(last - at) >= 32) {
1134 auto const block = _mm256_loadu_si256(reinterpret_cast<__m256i const*>(at));
1135 auto const whitespace = _mm256_or_si256(
1136 _mm256_or_si256(_mm256_cmpeq_epi8(block, newline), _mm256_cmpeq_epi8(block, carriage)),
1137 _mm256_or_si256(_mm256_cmpeq_epi8(block, space), _mm256_cmpeq_epi8(block, tab))
1138 );
1139 out =
1140 compact_block_avx2(block, static_cast<unsigned>(_mm256_movemask_epi8(whitespace)), out);
1141 at += 32;
1142 }
1143 for (; at != last; ++at) {
1144 if (!fastx_is_sequence_whitespace(*at)) *out++ = *at;
1145 }
1146 return out;
1147}
1148#endif
1149
1150static bool const simd_compaction = !ablated("simd-parsing");
1151
1152#if (defined(__x86_64__) || defined(__i386__)) && (defined(__GNUC__) || defined(__clang__))
1153static bool const compact_avx512 = simd_compaction && __builtin_cpu_supports("avx512bw") &&
1154 __builtin_cpu_supports("avx512vbmi2");
1155static bool const compact_avx2 = simd_compaction && __builtin_cpu_supports("avx2");
1156
1157__attribute__((target("avx512f,avx512bw,avx512vbmi2,popcnt"))) inline char*
1158compact_sequence_whitespace_avx512(char const* first, char const* last, char* out) {
1159 auto const* at = first;
1160 while (static_cast<size_t>(last - at) >= 64) {
1161 auto const block = _mm512_loadu_si512(at);
1162 auto const keep =
1163 ~(_mm512_cmpeq_epi8_mask(block, _mm512_set1_epi8('\n')) |
1164 _mm512_cmpeq_epi8_mask(block, _mm512_set1_epi8('\r')) |
1165 _mm512_cmpeq_epi8_mask(block, _mm512_set1_epi8(' ')) |
1166 _mm512_cmpeq_epi8_mask(block, _mm512_set1_epi8('\t')));
1167 _mm512_storeu_si512(out, _mm512_maskz_compress_epi8(keep, block));
1168 out += std::popcount(keep);
1169 at += 64;
1170 }
1171 for (; at != last; ++at) {
1172 if (!fastx_is_sequence_whitespace(*at)) *out++ = *at;
1173 }
1174 return out;
1175}
1176#endif
1177
1186inline char* compact_sequence_whitespace(char const* first, char const* last, char* out) {
1187 if (static_cast<size_t>(last - first) >= 64 && simd_compaction) {
1188#if defined(__ARM_NEON)
1189 return compact_sequence_whitespace_neon(first, last, out);
1190#elif defined(__x86_64__) || defined(__i386__)
1191 if (compact_avx512) return compact_sequence_whitespace_avx512(first, last, out);
1192 if (compact_avx2) {
1193 return compact_sequence_whitespace_avx2(first, last, out);
1194 }
1195#endif
1196 }
1197 constexpr uint64_t ones = 0x0101010101010101ULL;
1198 auto const has_zero_byte = [](uint64_t word) {
1199 return (word - ones) & ~word & 0x8080808080808080ULL;
1200 };
1201 auto const has_byte = [&](uint64_t word, uint64_t byte) {
1202 return has_zero_byte(word ^ (ones * byte));
1203 };
1204 auto const* word = first;
1205 while (static_cast<size_t>(last - word) >= sizeof(uint64_t)) {
1206 uint64_t value = 0;
1207 std::memcpy(&value, word, sizeof(value));
1208 auto const whitespace = has_byte(value, ' ') | has_byte(value, '\t') |
1209 has_byte(value, '\n') | has_byte(value, '\r');
1210 if (whitespace == 0) {
1211 std::memcpy(out, word, sizeof(value));
1212 out += sizeof(value);
1213 } else {
1214 for (size_t index = 0; index < sizeof(value); ++index) {
1215 if (!fastx_is_sequence_whitespace(word[index])) *out++ = word[index];
1216 }
1217 }
1218 word += sizeof(value);
1219 }
1220 for (auto const* at = word; at != last; ++at) {
1221 if (!fastx_is_sequence_whitespace(*at)) *out++ = *at;
1222 }
1223 return out;
1224}
1225
1229[[nodiscard]] inline char const* fasta_next_header(char const* first, char const* last) {
1230 for (auto const* at = first; at != last; ++at) {
1231 at = static_cast<char const*>(std::memchr(at, '>', static_cast<size_t>(last - at)));
1232 if (at == nullptr) return last;
1233 if (at == first || at[-1] == '\n' || at[-1] == '\r') return at;
1234 }
1235 return last;
1236}
1237
1238#if defined(__x86_64__) || defined(__i386__)
1244__attribute__((target("avx512f,avx512bw,avx512vbmi2,popcnt"))) inline char*
1245fasta_sequence_compact_avx512(char const* first, char const* last, char* out, char const*& stop) {
1246 auto const newline = _mm512_set1_epi8('\n');
1247 auto const carriage = _mm512_set1_epi8('\r');
1248 auto const space = _mm512_set1_epi8(' ');
1249 auto const tab = _mm512_set1_epi8('\t');
1250 auto const marker = _mm512_set1_epi8('>');
1251 // Bit set when the byte before the block ends a line; @p first follows a line ending.
1252 uint64_t carry = 1;
1253 auto const* at = first;
1254 while (static_cast<size_t>(last - at) >= 64) {
1255 auto const block = _mm512_loadu_si512(at);
1256 auto const ends =
1257 _mm512_cmpeq_epi8_mask(block, newline) | _mm512_cmpeq_epi8_mask(block, carriage);
1258 auto const headers = _mm512_cmpeq_epi8_mask(block, marker) & ((ends << 1U) | carry);
1259 if (headers != 0) {
1260 stop = at + std::countr_zero(headers);
1261 return compact_sequence_whitespace(at, stop, out);
1262 }
1263 carry = ends >> 63U;
1264 auto const keep =
1265 ~(ends | _mm512_cmpeq_epi8_mask(block, space) | _mm512_cmpeq_epi8_mask(block, tab));
1266 _mm512_storeu_si512(out, _mm512_maskz_compress_epi8(keep, block));
1267 out += std::popcount(keep);
1268 at += 64;
1269 }
1270 bool line_start = carry != 0;
1271 for (; at != last; ++at) {
1272 char const ch = *at;
1273 if (ch == '>' && line_start) break;
1274 line_start = ch == '\n' || ch == '\r';
1275 if (!fastx_is_sequence_whitespace(ch)) *out++ = ch;
1276 }
1277 stop = at;
1278 return out;
1279}
1280
1281__attribute__((target("avx2"))) inline char*
1282fasta_sequence_compact_avx2(char const* first, char const* last, char* out, char const*& stop) {
1283 auto const newline = _mm256_set1_epi8('\n');
1284 auto const carriage = _mm256_set1_epi8('\r');
1285 auto const space = _mm256_set1_epi8(' ');
1286 auto const tab = _mm256_set1_epi8('\t');
1287 auto const marker = _mm256_set1_epi8('>');
1288 // Bit set when the byte before the block ends a line; @p first follows a line ending.
1289 unsigned carry = 1;
1290 auto const* at = first;
1291 while (static_cast<size_t>(last - at) >= 32) {
1292 auto const block = _mm256_loadu_si256(reinterpret_cast<__m256i const*>(at));
1293 auto const line_end =
1294 _mm256_or_si256(_mm256_cmpeq_epi8(block, newline), _mm256_cmpeq_epi8(block, carriage));
1295 auto const ends = static_cast<unsigned>(_mm256_movemask_epi8(line_end));
1296 auto const markers =
1297 static_cast<unsigned>(_mm256_movemask_epi8(_mm256_cmpeq_epi8(block, marker)));
1298 auto const headers = markers & ((ends << 1U) | carry);
1299 if (headers != 0) {
1300 stop = at + std::countr_zero(headers);
1301 return compact_sequence_whitespace(at, stop, out);
1302 }
1303 carry = ends >> 31U;
1304 auto const whitespace = _mm256_or_si256(
1305 line_end,
1306 _mm256_or_si256(_mm256_cmpeq_epi8(block, space), _mm256_cmpeq_epi8(block, tab))
1307 );
1308 out =
1309 compact_block_avx2(block, static_cast<unsigned>(_mm256_movemask_epi8(whitespace)), out);
1310 at += 32;
1311 }
1312 bool line_start = carry != 0;
1313 for (; at != last; ++at) {
1314 char const ch = *at;
1315 if (ch == '>' && line_start) break;
1316 line_start = ch == '\n' || ch == '\r';
1317 if (!fastx_is_sequence_whitespace(ch)) *out++ = ch;
1318 }
1319 stop = at;
1320 return out;
1321}
1322#endif
1323
1324#if defined(__ARM_NEON)
1325inline char*
1326fasta_sequence_compact_neon(char const* first, char const* last, char* out, char const*& stop) {
1327 unsigned carry = 1;
1328 auto const* at = first;
1329 while (static_cast<size_t>(last - at) >= 16) {
1330 auto const block = vld1q_u8(reinterpret_cast<uint8_t const*>(at));
1331 auto const line_end =
1332 vorrq_u8(vceqq_u8(block, vdupq_n_u8('\n')), vceqq_u8(block, vdupq_n_u8('\r')));
1333 auto const ends = compact_impl::compact_mask(line_end);
1334 auto const markers = compact_impl::compact_mask(vceqq_u8(block, vdupq_n_u8('>')));
1335 auto const headers = markers & ((ends << 1U) | carry);
1336 if (headers != 0) {
1337 stop = at + std::countr_zero(headers);
1338 return compact_sequence_whitespace(at, stop, out);
1339 }
1340 carry = ends >> 15U;
1341 auto const whitespace = vorrq_u8(
1342 line_end, vorrq_u8(vceqq_u8(block, vdupq_n_u8(' ')), vceqq_u8(block, vdupq_n_u8('\t')))
1343 );
1344 out = compact_block_neon(block, whitespace, out);
1345 at += 16;
1346 }
1347 bool line_start = carry != 0;
1348 for (; at != last; ++at) {
1349 char const ch = *at;
1350 if (ch == '>' && line_start) break;
1351 line_start = ch == '\n' || ch == '\r';
1352 if (!fastx_is_sequence_whitespace(ch)) *out++ = ch;
1353 }
1354 stop = at;
1355 return out;
1356}
1357#endif
1358
1365inline char*
1366fasta_sequence_compact(char const* first, char const* last, char* out, char const*& stop) {
1367#if (defined(__x86_64__) || defined(__i386__)) && (defined(__GNUC__) || defined(__clang__))
1368 if (compact_avx512) return fasta_sequence_compact_avx512(first, last, out, stop);
1369 if (compact_avx2) return fasta_sequence_compact_avx2(first, last, out, stop);
1370#endif
1371#if defined(__ARM_NEON)
1372 if (simd_compaction) return fasta_sequence_compact_neon(first, last, out, stop);
1373#endif
1374 stop = fasta_next_header(first, last);
1375 return compact_sequence_whitespace(first, stop, out);
1376}
1377
1383[[nodiscard]] inline std::vector<fastx_sequence_extent>
1384fastx_fasta_compact_extents(std::string_view data, char* out) {
1385 std::vector<fastx_sequence_extent> extents;
1386 size_t header = data.find('>');
1387 while (header != std::string_view::npos && header != 0 && data[header - 1] != '\n' &&
1388 data[header - 1] != '\r') {
1389 header = data.find('>', header + 1);
1390 }
1391 if (header == std::string_view::npos) return extents;
1392 auto const* const last = data.data() + data.size();
1393 while (true) {
1394 auto const end = fastx_line_end(data, header);
1395 if (end + 1 >= data.size()) break;
1396 auto const* const sequence = data.data() + end + 1;
1397 char const* stop = nullptr;
1398 auto* const begin = out;
1399 out = fasta_sequence_compact(sequence, last, out, stop);
1400 if (stop != sequence) extents.push_back({begin, out});
1401 if (stop == last) break;
1402 header = static_cast<size_t>(stop - data.data());
1403 }
1404 return extents;
1405}
1406
1414 if (result.extents.empty()) return;
1415 // Extents point into whichever buffer holds the bytes: a page-locked target, the heap copy of
1416 // a compressed file, or the FASTQ sequence buffer. Those are writable; a memory-mapped plain
1417 // file is not, so its bases are compacted into `decompressed` instead.
1418 struct writable_buffer {
1419 char* data;
1420 size_t size;
1421 };
1422 auto const buffers = std::array{
1423 writable_buffer{result.decompressed_target, result.decompressed_size},
1424 writable_buffer{result.decompressed.data(), result.decompressed.size()},
1425 writable_buffer{result.sequence.data(), result.sequence.size()},
1426 };
1427 auto const writable = [&](char const* first, size_t size) {
1428 return std::any_of(buffers.begin(), buffers.end(), [&](writable_buffer const& buffer) {
1429 return buffer.data != nullptr && buffer.size >= size && first >= buffer.data &&
1430 first + size <= buffer.data + buffer.size;
1431 });
1432 };
1433 size_t raw_total = 0;
1434 for (auto const& extent : result.extents)
1435 raw_total += static_cast<size_t>(extent.end - extent.begin);
1436 // Reserved up front so appending cannot move the buffer the extents are about to point at.
1437 std::string copied;
1438 copied.reserve(raw_total);
1439 std::vector<std::pair<size_t, std::pair<size_t, size_t>>> offsets;
1440 for (size_t index = 0; index < result.extents.size(); ++index) {
1441 auto const& extent = result.extents[index];
1442 auto const size = static_cast<size_t>(extent.end - extent.begin);
1443 if (writable(extent.begin, size)) {
1444 // Compaction only ever moves bytes towards the buffer's start.
1445 auto* const write = compact_sequence_whitespace(
1446 extent.begin, extent.end, const_cast<char*>(extent.begin)
1447 );
1448 result.extents[index] = {extent.begin, write};
1449 } else {
1450 auto const begin = copied.size();
1451 // Sized before the copy so the write needs no bounds check per byte.
1452 copied.resize(begin + size);
1453 auto* const write =
1454 compact_sequence_whitespace(extent.begin, extent.end, copied.data() + begin);
1455 copied.resize(static_cast<size_t>(write - copied.data()));
1456 offsets.emplace_back(index, std::pair{begin, copied.size()});
1457 }
1458 }
1459 if (!copied.empty()) {
1460 result.decompressed = std::move(copied);
1461 for (auto const& [index, span] : offsets) {
1462 result.extents[index] = {
1463 result.decompressed.data() + span.first, result.decompressed.data() + span.second
1464 };
1465 }
1466 }
1467}
1468
1469inline Result<void>
1470parse_fastx_sequence_file(fastx_sequence_file& result, std::string const& path) {
1471 auto const data = std::exchange(result.input, {});
1472 auto& sequence = result.sequence;
1473 auto& extents = result.extents;
1474 auto const first = data.find_first_not_of("\r\n");
1475 if (first != std::string_view::npos && data[first] == '@') {
1476 fastx_fastq_reader reader{data, path};
1477 std::vector<std::pair<size_t, size_t>> records;
1478 while (true) {
1479 auto const begin = sequence.size();
1480 auto next = reader.append_next_record(sequence);
1481 if (!next) return Err(Error::invalid_argument(next.error().message()));
1482 if (!*next) break;
1483 if (sequence.size() > begin) records.emplace_back(begin, sequence.size());
1484 }
1485 if (sequence.empty()) return Ok();
1486 for (auto const& [begin, end] : records) {
1487 extents.push_back({sequence.data() + begin, sequence.data() + end});
1488 }
1490 return Ok();
1491 }
1492 // Mapped plain bytes are read-only; decompressed bytes can be compacted in place.
1493 char* out = result.decompressed_target;
1494 if (out == nullptr) {
1495 if (data.data() != result.decompressed.data()) result.decompressed.resize(data.size());
1496 out = result.decompressed.data();
1497 }
1498 extents = fastx_fasta_compact_extents(data, out);
1499 if (extents.empty() && !data.empty()) {
1500 return Err(Error::invalid_argument("FASTX parse error near: " + path));
1501 }
1502 return Ok();
1503}
1504
1509 fastx_sequence_file& result,
1510 std::string_view data,
1511 char* out,
1512 uint32_t k,
1513 std::string const& path
1514) {
1515 size_t header = data.find('>');
1516 while (header != std::string_view::npos && header != 0 && data[header - 1] != '\n' &&
1517 data[header - 1] != '\r') {
1518 header = data.find('>', header + 1);
1519 }
1520 if (header == std::string_view::npos) {
1521 if (!data.empty()) return Err(Error::invalid_argument("FASTX parse error near: " + path));
1522 return Ok();
1523 }
1524 std::string scratch, carry;
1525 fastx_sequence_file chunk;
1526 uint64_t written = 0;
1527 bool has_sequence = false;
1528 auto const* const last = data.data() + data.size();
1529 while (true) {
1530 auto const end = fastx_line_end(data, header);
1531 if (end + 1 >= data.size()) break;
1532 auto const* at = data.data() + end + 1;
1533 carry.clear();
1534 while (at != last) {
1535 auto const offset = static_cast<size_t>(at - data.data());
1536 auto const boundary = offset + std::min<size_t>(16384, data.size() - offset);
1537 auto const limit = boundary == data.size()
1538 ? boundary
1539 : std::min(data.size(), fastx_line_end(data, boundary) + 1);
1540 auto const* const chunk_end = data.data() + limit;
1541 scratch.resize(carry.size() + static_cast<size_t>(chunk_end - at));
1542 std::memcpy(scratch.data(), carry.data(), carry.size());
1543 char const* stop = nullptr;
1544 auto* compact_end =
1545 fasta_sequence_compact(at, chunk_end, scratch.data() + carry.size(), stop);
1546 has_sequence |= stop != at;
1547 auto const bases = static_cast<size_t>(compact_end - scratch.data());
1548 carry.assign(compact_end - std::min<size_t>(k - 1, bases), compact_end);
1549 chunk.extents.push_back({scratch.data(), compact_end});
1550 pack_fastx_sequence_file(chunk, k);
1551 uint64_t words = 0;
1552 for (auto record : chunk.packed) {
1553 words = record.word + (record.bases + 15) / 16;
1554 record.word += written;
1555 result.packed.push_back(record);
1556 }
1557 std::memcpy(out + written * sizeof(uint32_t), scratch.data(), words * sizeof(uint32_t));
1558 written += words;
1559 chunk.packed.clear();
1560 at = stop;
1561 if (stop != chunk_end || stop == last) break;
1562 }
1563 if (at == last) break;
1564 header = static_cast<size_t>(at - data.data());
1565 }
1566 if (!has_sequence && !data.empty()) {
1567 return Err(Error::invalid_argument("FASTX parse error near: " + path));
1568 }
1569 result.words = reinterpret_cast<uint32_t const*>(out);
1570 result.input = {};
1571 return Ok();
1572}
1573
1576 std::unique_ptr<fastx_sequence_file> result,
1577 std::string_view data,
1578 std::string const& path,
1579 bool defer_fasta,
1580 uint32_t pack_k
1581) {
1582 result->input = data;
1583 if (defer_fasta) {
1584 auto const first = data.find_first_not_of("\r\n");
1585 if (first != std::string_view::npos && data[first] == '>') {
1586 auto const end = fastx_line_end(data, first);
1587 // Empty first records and malformed inputs stay with the CPU parser.
1588 if (end + 1 < data.size() && data[end + 1] != '>') return result;
1589 }
1590 }
1591 if (pack_k >= 16) {
1592 auto const first = data.find_first_not_of("\r\n");
1593 if (first == std::string_view::npos || data[first] != '@') {
1594 char* out = result->decompressed_target;
1595 if (out == nullptr) {
1596 if (data.data() != result->decompressed.data())
1597 result->decompressed.resize(data.size());
1598 out = result->decompressed.data();
1599 }
1600 CUDDL_TRY(pack_fasta_chunks(*result, data, out, pack_k, path));
1601 return result;
1602 }
1603 }
1604 CUDDL_TRY(parse_fastx_sequence_file(*result, path));
1605 return result;
1606}
1607
1616 fastx_sequence_file& result,
1617 std::string_view input,
1618 std::string const& path,
1619 decompression_source source,
1620 uint32_t k
1621) {
1622 thread_local std::unique_ptr<char[]> scratch;
1623 thread_local size_t capacity = 0;
1624 // A single-member file's ISIZE is its inflated length. ISIZE sizes nothing for a BGZF stream,
1625 // which ends in an empty EOF block that reads back as zero, so a hint below the compressed
1626 // size falls back to an over-estimate; a short buffer doubles and inflates again.
1627 auto const hint = gzip_size_hint(input);
1628 size_t wanted = hint >= input.size() ? hint : input.size() * 8;
1629 size_t produced = 0;
1630 while (true) {
1631 if (capacity < wanted) {
1632 capacity = std::bit_ceil(wanted);
1633 scratch = std::make_unique_for_overwrite<char[]>(capacity);
1634 }
1635 bool insufficient = false;
1636 produced = CUDDL_TRY(
1637 gunzip_members_direct(input, {scratch.get(), capacity, {}}, insufficient, path)
1638 );
1639 if (!insufficient) break;
1640 wanted = capacity * 2;
1641 }
1642 std::string_view const data{scratch.get(), produced};
1643 auto const first = data.find_first_not_of("\r\n");
1644 bool const fastq = first != std::string_view::npos && data[first] == '@';
1645 if (!fastq) CUDDL_TRY(pack_fasta_chunks(result, data, scratch.get(), k, path));
1646 uint64_t bytes = produced;
1647 if (!fastq) {
1648 uint64_t words = 0;
1649 for (auto const& record : result.packed) {
1650 words = std::max(words, record.word + (record.bases + 15) / 16);
1651 }
1652 bytes = words * sizeof(uint32_t);
1653 }
1654 auto target = source.request(bytes);
1655 char* out = target.data;
1656 if (out == nullptr) {
1657 result.decompressed.resize(bytes);
1658 out = result.decompressed.data();
1659 } else {
1660 result.decompressed_target = out;
1661 result.decompressed_size = bytes;
1662 result.storage_owner = std::move(target.owner);
1663 }
1664 std::memcpy(out, scratch.get(), bytes);
1665 if (fastq) return std::optional{std::string_view{out, bytes}};
1666 result.words = reinterpret_cast<uint32_t const*>(out);
1667 return std::optional<std::string_view>{};
1668}
1669
1671 std::unique_ptr<fastx_mapped_file> file,
1672 std::string const& path,
1673 decompression_source source,
1674 bool defer_fasta,
1675 uint32_t pack_k = 0
1676) {
1677 auto result = std::make_unique<fastx_sequence_file>();
1678 result->file = std::move(file);
1679 auto data = result->file->data();
1680 auto& decompressed = result->decompressed;
1681 if (data.size() >= 2 && static_cast<unsigned char>(data[0]) == 0x1f &&
1682 static_cast<unsigned char>(data[1]) == 0x8b) {
1683 if (pack_k >= 16 && !defer_fasta && source.acquire != nullptr) {
1684 auto const fastq = CUDDL_TRY(inflate_and_pack(*result, data, path, source, pack_k));
1685 if (!fastq) return result;
1686 return finish_fastx_sequence_file(std::move(result), *fastq, path, false, pack_k);
1687 }
1688 // A single-member file's ISIZE is its inflated length. Asking for that rather than an
1689 // over-estimate keeps loader buffers small, which matters most when they are
1690 // page-locked, since the driver pins one allocation at a time. ISIZE sizes
1691 // nothing for a BGZF stream, which ends in an empty EOF block that reads back as zero, so
1692 // a hint below the compressed size falls back to the over-estimate the heap path assumes.
1693 // A concatenated stream advertises only its last member; a short target grows the
1694 // request, which keeps a multi-member file off the heap path.
1695 auto const hint = gzip_size_hint(data);
1696 auto request = hint >= data.size() ? hint : data.size() * 8;
1697 auto target = source.request(request);
1698 bool insufficient = false;
1699 bool inflated = false;
1700 size_t produced = 0;
1701 while (target.data != nullptr && target.capacity > 0) {
1702 auto const written = gunzip_members_direct(data, target, insufficient, path);
1703 if (!written) return Err(written.error());
1704 produced = *written;
1705 if (!insufficient) {
1706 inflated = true;
1707 break;
1708 }
1709 request = std::max(request * 2, data.size());
1710 target = source.request(request);
1711 }
1712 if (inflated) {
1713 result->decompressed_target = target.data;
1714 result->decompressed_size = produced;
1715 result->storage_owner = std::move(target.owner);
1716 data = std::string_view{target.data, produced};
1717 }
1718 if (result->decompressed_target == nullptr) {
1719 auto inflated = gunzip_members_into(data, decompressed, path);
1720 if (!inflated) return Err(inflated.error());
1721 data = decompressed;
1722 }
1723 } else if (source.acquire != nullptr && !defer_fasta) {
1724 // Plain bytes go to the caller's buffer, as inflated bytes do: the parser compacts them
1725 // in place and the stager reads them without another copy.
1726 auto target = source.request(data.size());
1727 if (target.data != nullptr) {
1728 std::memcpy(target.data, data.data(), data.size());
1729 result->decompressed_target = target.data;
1730 result->decompressed_size = data.size();
1731 result->storage_owner = std::move(target.owner);
1732 data = std::string_view{target.data, data.size()};
1733 }
1734 }
1735 return finish_fastx_sequence_file(std::move(result), data, path, defer_fasta, pack_k);
1736}
1737
1739 std::string const& path,
1740 decompression_source source = {},
1741 bool defer_fasta = false,
1742 bool read = false,
1743 uint32_t pack_k = 0
1744) {
1745#if defined(__linux__)
1746 if (source.acquire != nullptr) {
1747 int const fd = ::open(path.c_str(), O_RDONLY);
1748 struct stat status{};
1749 if (fd == -1 || ::fstat(fd, &status) != 0 || status.st_size < 0) {
1750 if (fd != -1) ::close(fd);
1751 return Err(Error::invalid_argument("cannot open FASTX file: " + path));
1752 }
1753 auto const size = static_cast<size_t>(status.st_size);
1754 unsigned char magic[2] = {};
1755 bool const gzip = ::pread(fd, magic, sizeof(magic), 0) == sizeof(magic) &&
1756 magic[0] == 0x1f && magic[1] == 0x8b;
1757 // Plain bytes go straight into the caller's buffer, as inflated bytes do: the parser
1758 // compacts them in place and the stager reads them without another copy.
1759 auto target = gzip ? decompression_target{} : source.request(size);
1760 if (target.data == nullptr) {
1761 auto file = fastx_mapped_file::adopt(fd, size, path, read, source.direct_reads);
1762 if (!file) return Err(Error::invalid_argument("cannot open FASTX file: " + path));
1763 return decode_fastx_sequence_file(std::move(*file), path, source, defer_fasta, pack_k);
1764 }
1765 if (!read_whole_file(fd, target.data, size, source.direct_reads, target.capacity)) {
1766 return Err(Error::invalid_argument("cannot open FASTX file: " + path));
1767 }
1768 auto result = std::make_unique<fastx_sequence_file>();
1769 result->decompressed_target = target.data;
1770 result->decompressed_size = size;
1771 result->storage_owner = std::move(target.owner);
1773 std::move(result), {target.data, size}, path, defer_fasta, pack_k
1774 );
1775 }
1776#endif
1777 auto file = fastx_mapped_file::load(path, read, source.direct_reads);
1778 if (!file) return Err(Error::invalid_argument("cannot open FASTX file: " + path));
1779 return decode_fastx_sequence_file(std::move(*file), path, source, defer_fasta, pack_k);
1780}
1781
1784inline constexpr size_t parallel_read_depth = 64;
1785
1786// Fixed worker pool loading FASTX files ahead of the GPU loop. A pthread spawn and join
1787// per genome costs tens of microseconds and dominates at high file counts. Readers pull
1788// ids in order with a bounded number of outstanding files. Consumers may collect by ID
1789// or by completion, retaining each input's ID. Loader exceptions are
1790// rethrown by take, matching std::async propagation into the build error handlers.
1791//
1792// Readers read whole files ahead of the loaders, which only decode. A parallel file system
1793// gives its bandwidth only to many reads in flight, and a loader that read its own files left
1794// storage idle while it inflated: on MOGON's Lustre, 16 loaders sketched 4,000 genomes no node
1795// had cached in 10.3-11.6 s reading their own files, and in 4.0-4.5 s with readers.
1797 public:
1809 std::span<std::filesystem::path const> paths,
1810 size_t workers,
1811 decompression_source source = {},
1812 size_t window = 0,
1813 bool defer_fasta = false,
1814 bool completion_order = false,
1815 uint32_t pack_k = 0
1816 )
1817 : paths_(paths),
1818 workers_(std::max(size_t{1}, workers)),
1819 window_(std::max({size_t{1}, window, workers_})),
1820 source_(source),
1821 defer_fasta_(defer_fasta),
1822 pack_k_(pack_k),
1823 results_(paths.size()),
1824 errors_(paths.size()),
1825 ready_(completion_order ? window_ : 0),
1826 retired_(defer_fasta ? window_ : 0) {
1827 threads_.reserve(workers_);
1828 if (defer_fasta_) retirer_ = std::thread([this] { retire_files(); });
1829 // A reader spends most of its time waiting on storage, so there are as many as the reads
1830 // a parallel file system needs in flight, however few the loaders.
1831 auto const readers =
1832 ablated("read-depth") ? workers_
1833 : std::min(window_, std::max(workers_, parallel_read_depth));
1834 readers_.reserve(readers);
1835 for (size_t i = 0; i < readers; ++i) {
1836 readers_.emplace_back([this] { read_files(); });
1837 }
1838 for (size_t i = 0; i < workers_; ++i) threads_.emplace_back([this] { work(); });
1839 }
1841 {
1842 std::lock_guard lock(mutex_);
1843 stop_ = true;
1844 }
1845 assign_.notify_all();
1846 mapped_ready_.notify_all();
1847 for (auto& thread : readers_) thread.join();
1848 for (auto& thread : threads_) thread.join();
1849 // Loaders may be waiting for retirement space, so stop the reaper only after joining them.
1850 if (retirer_.joinable()) {
1851 {
1852 std::lock_guard lock(retired_mutex_);
1853 retire_stop_ = true;
1854 }
1855 retired_ready_.notify_one();
1856 retirer_.join();
1857 }
1858 }
1861
1863 std::unique_lock lock(mutex_);
1864 filled_.wait(lock, [&] { return results_[id].has_value() || errors_[id] != nullptr; });
1865 ++taken_;
1866 // One freed window slot needs one loader, not all of them: waking the rest is a syscall
1867 // and a context switch each, once per genome, at 100,000 genomes.
1868 assign_.notify_one();
1869 lock.unlock();
1870 if (errors_[id] != nullptr) std::rethrow_exception(errors_[id]);
1871 return std::move(*results_[id]);
1872 }
1873
1875 if (ready_.empty())
1876 return Err(Error::invalid_argument("completion-order loading is disabled"));
1877 std::unique_lock lock(mutex_);
1878 filled_.wait(lock, [&] { return ready_count_ != 0; });
1879 auto const id = ready_[ready_head_];
1880 ready_head_ = (ready_head_ + 1) % ready_.size();
1881 --ready_count_;
1882 ++taken_;
1883 assign_.notify_one();
1884 lock.unlock();
1885 if (errors_[id] != nullptr) std::rethrow_exception(errors_[id]);
1886 return std::pair{id, CUDDL_TRY(std::move(*results_[id]))};
1887 }
1888
1892 {
1893 std::unique_lock lock(mutex_);
1894 filled_.wait(lock, [&] { return ready_count_ != 0 || taken_ >= limit_; });
1895 if (ready_count_ == 0) return std::nullopt;
1896 }
1897 return CUDDL_TRY(take_ready());
1898 }
1899
1901 [[nodiscard]] size_t ready() {
1902 std::lock_guard lock(mutex_);
1903 return ready_count_;
1904 }
1905
1910 size_t cap(size_t limit) {
1911 {
1912 std::lock_guard lock(mutex_);
1913 limit_ = std::clamp(limit, claimed_end_, limit_);
1914 limit = limit_;
1915 }
1916 assign_.notify_all();
1917 filled_.notify_all();
1918 mapped_ready_.notify_all();
1919 return limit;
1920 }
1921
1922 private:
1924 struct mapped_input {
1925 size_t id = 0;
1926 std::optional<Result<std::unique_ptr<fastx_mapped_file>>> file;
1927 std::exception_ptr error;
1928 };
1929
1930 void read_files() {
1931 while (true) {
1932 size_t id;
1933 {
1934 std::unique_lock lock(mutex_);
1935 assign_.wait(lock, [&] {
1936 return stop_ || mapped_next_ >= limit_ || mapped_next_ - taken_ < window_;
1937 });
1938 if (stop_ || mapped_next_ >= limit_) return;
1939 id = mapped_next_++;
1940 }
1941 mapped_input input;
1942 input.id = id;
1943 try {
1944 input.file =
1945 fastx_mapped_file::load(paths_[id].string(), true, source_.direct_reads);
1946 } catch (...) {
1947 input.error = std::current_exception();
1948 }
1949 {
1950 std::lock_guard lock(mutex_);
1951 mapped_.push_back(std::move(input));
1952 }
1953 mapped_ready_.notify_one();
1954 }
1955 }
1956
1957 void retire(std::unique_ptr<fastx_mapped_file> file) {
1958 std::unique_lock lock(retired_mutex_);
1959 retired_space_.wait(lock, [&] { return retired_count_ < retired_.size(); });
1960 retired_[(retired_head_ + retired_count_) % retired_.size()] = std::move(file);
1961 ++retired_count_;
1962 lock.unlock();
1963 retired_ready_.notify_one();
1964 }
1965
1966 void retire_files() {
1967 while (true) {
1968 std::unique_ptr<fastx_mapped_file> file;
1969 {
1970 std::unique_lock lock(retired_mutex_);
1971 retired_ready_.wait(lock, [&] { return retire_stop_ || retired_count_ != 0; });
1972 if (retired_count_ == 0) return;
1973 file = std::move(retired_[retired_head_]);
1974 retired_head_ = (retired_head_ + 1) % retired_.size();
1975 --retired_count_;
1976 }
1977 retired_space_.notify_one();
1978 }
1979 }
1980
1981 void work() {
1982 while (true) {
1983 size_t id;
1984 mapped_input input;
1985 {
1986 std::unique_lock lock(mutex_);
1987 // Readers own the window; loaders take files as they are read, so one slow read
1988 // holds up only itself. Files read past a cap belong to another consumer.
1989 mapped_ready_.wait(lock, [&] {
1990 return stop_ || claimed_ >= limit_ || !mapped_.empty();
1991 });
1992 if (stop_ || claimed_ >= limit_) return;
1993 input = std::move(mapped_.front());
1994 mapped_.pop_front();
1995 id = input.id;
1996 if (id >= limit_) continue;
1997 ++claimed_;
1998 claimed_end_ = std::max(claimed_end_, id + 1);
1999 // Another file may have been read with no loader woken for it.
2000 if (!mapped_.empty()) mapped_ready_.notify_one();
2001 }
2002 try {
2003 auto const path = paths_[id].string();
2004 auto loaded = [&]() -> Result<std::unique_ptr<fastx_sequence_file>> {
2005 if (input.error) std::rethrow_exception(input.error);
2006 if (!*input.file)
2007 return Err(Error::invalid_argument("cannot open FASTX file: " + path));
2009 std::move(**input.file), path, source_, defer_fasta_, pack_k_
2010 );
2011 }();
2012 if (pack_k_ != 0 && loaded) pack_fastx_sequence_file(**loaded, pack_k_);
2013 // Inflated or copied bytes no longer reference the file's bytes.
2014 if (defer_fasta_ && loaded && (*loaded)->file &&
2015 ((*loaded)->decompressed_target != nullptr ||
2016 !(*loaded)->decompressed.empty())) {
2017 retire(std::move((*loaded)->file));
2018 }
2019 std::lock_guard lock(mutex_);
2020 results_[id] = std::move(loaded);
2021 if (!ready_.empty()) {
2022 ready_[(ready_head_ + ready_count_) % ready_.size()] = id;
2023 ++ready_count_;
2024 }
2025 } catch (...) {
2026 std::lock_guard lock(mutex_);
2027 errors_[id] = std::current_exception();
2028 if (!ready_.empty()) {
2029 ready_[(ready_head_ + ready_count_) % ready_.size()] = id;
2030 ++ready_count_;
2031 }
2032 }
2033 filled_.notify_all();
2034 }
2035 }
2036 std::span<std::filesystem::path const> paths_;
2037 size_t workers_;
2038 size_t window_;
2039 decompression_source source_;
2040 bool defer_fasta_;
2041 uint32_t pack_k_;
2042 std::vector<std::optional<Result<std::unique_ptr<fastx_sequence_file>>>> results_;
2043 std::vector<std::exception_ptr> errors_;
2044 std::vector<size_t> ready_;
2045 std::vector<std::unique_ptr<fastx_mapped_file>> retired_;
2046 std::deque<mapped_input> mapped_;
2047 std::vector<std::thread> threads_;
2048 std::thread retirer_;
2049 std::vector<std::thread> readers_;
2050 std::mutex mutex_;
2051 std::mutex retired_mutex_;
2052 std::condition_variable assign_, filled_;
2053 std::condition_variable retired_ready_, retired_space_;
2054 std::condition_variable mapped_ready_;
2055 // Files loaders took, and one past the highest id among them.
2056 size_t claimed_ = 0, claimed_end_ = 0, taken_ = 0, limit_ = paths_.size();
2057 size_t ready_head_ = 0, ready_count_ = 0;
2058 size_t retired_head_ = 0, retired_count_ = 0;
2059 size_t mapped_next_ = 0;
2060 bool stop_ = false;
2061 bool retire_stop_ = false;
2062};
2063
2064} // namespace cuddl::detail
2065
2066namespace cuddl {
2067
2074 size_t direct_bytes = 0;
2075 size_t staged_bytes = 0;
2076 size_t direct_chunks = 0;
2077 size_t staged_chunks = 0;
2078 size_t pinned_buffers = 0;
2079 unsigned workers = 0; // loaders the build ran, after its own defaulting
2080 bool in_place = false; // the kernels read the loader's own buffers, so nothing was copied
2081 size_t staging_bytes = 0;
2082 size_t batches = 0;
2083 size_t transfers = 0; // runs copied in one piece, plus records split across pieces
2084 // Files the gpu backend took without a host loader: inflated by nvCOMP or the
2085 // decompression engine, or plain FASTA read straight into the GPU's buffers.
2086 size_t device_files = 0;
2088 bool measure_resident = false;
2090};
2091} // namespace cuddl
FASTQ parser over a contiguous in-memory buffer.
fastx_fastq_reader(std::string_view data, std::string source_name="<buffer>")
Constructs a reader over a contiguous in-memory FASTQ buffer.
Result< bool > append_next_record(std::string &sequence)
Appends the next record's sequence bytes to sequence.
size_t ready()
Completion-order results waiting to be taken.
fastx_load_pool & operator=(fastx_load_pool const &)=delete
fastx_load_pool(fastx_load_pool const &)=delete
fastx_load_pool(std::span< std::filesystem::path const > paths, size_t workers, decompression_source source={}, size_t window=0, bool defer_fasta=false, bool completion_order=false, uint32_t pack_k=0)
Result< std::pair< size_t, std::unique_ptr< fastx_sequence_file > > > take_ready()
size_t cap(size_t limit)
Stops the loaders before id limit, or after the ids they already claimed.
Result< std::optional< std::pair< size_t, std::unique_ptr< fastx_sequence_file > > > > take_ready_until_limit()
take_ready, or nothing once every id below the cap limit is taken.
Result< std::unique_ptr< fastx_sequence_file > > take(size_t id)
Read-only contiguous file payload for in-memory FASTX parsing.
std::string_view data() const noexcept
Contiguous read-only file bytes.
fastx_mapped_file & operator=(fastx_mapped_file const &)=delete
fastx_mapped_file & operator=(fastx_mapped_file &&other) noexcept
static Result< std::unique_ptr< fastx_mapped_file > > load(std::string const &path, bool read=false, bool direct=false)
Maps or reads a file.
bool empty() const noexcept
True when the file is empty or load produced no bytes.
fastx_mapped_file(fastx_mapped_file &&other) noexcept
fastx_mapped_file(fastx_mapped_file const &)=delete
#define CUDDL_TRY(expr)
Propagates a cuddl::Result failure from the enclosing function (GNU statement expression).
Definition error.hpp:225
compact_shuffle_table const & compact_shuffle() noexcept
Result< void > pack_fasta_chunks(fastx_sequence_file &result, std::string_view data, char *out, uint32_t k, std::string const &path)
Compact and pack one cache-sized block at a time, without a genome-sized ASCII write.
std::vector< fastx_sequence_extent > fastx_fasta_compact_extents(std::string_view data, char *out)
Collects FASTA sequence extents with whitespace compacted out, in one pass over data.
size_t fastx_line_end(std::string_view data, size_t position)
First line-end position at or after position using the resolved scanner.
auto const high
Definition kernels.cuh:940
Result< std::unique_ptr< fastx_sequence_file > > finish_fastx_sequence_file(std::unique_ptr< fastx_sequence_file > result, std::string_view data, std::string const &path, bool defer_fasta, uint32_t pack_k)
Parses, packs or defers the bytes data of a loaded file, which result holds.
auto const block
Definition top_k.cuh:236
uint32_t pack_bases16_portable(char const *bases, size_t count, uint32_t &word)
Packs up to sixteen bases into a two-bit word, base j in bits 2j and 2j+1.
void compact_fastx_sequence_extents(fastx_sequence_file &result)
Compacts sequence whitespace in place, leaving every extent a run of bases.
Result< void > gunzip_members_into(std::string_view input, std::string &output, std::string const &path)
Decompresses gzip members from a mapped buffer into output, appending.
auto const pairs
Definition kernels.cuh:1592
auto const count
Definition kernels.cuh:1031
size_t fastx_line_end_scalar(std::string_view data, size_t position)
constexpr size_t parallel_read_depth
Reads a parallel file system needs in flight to give one node its bandwidth: on MOGON's Lustre,...
Result< std::unique_ptr< fastx_sequence_file > > decode_fastx_sequence_file(std::unique_ptr< fastx_mapped_file > file, std::string const &path, decompression_source source, bool defer_fasta, uint32_t pack_k=0)
void pack_fastx_sequence_file(fastx_sequence_file &result, uint32_t k)
Packs a parsed file's records in place into two-bit words, run by run.
char * compact_sequence_whitespace(char const *first, char const *last, char *out)
Copies bases from [first, last) to out, dropping sequence whitespace.
uint64_t pack_bases_pieces(char const *bases, size_t count, uint32_t *words, size_t width)
Packs width bases from sixteen-base pieces, for a tail shorter than a full step.
decltype(&fastx_line_end_scalar) fastx_line_end_fn
fastx_format
Detected record format for a FASTX buffer.
@ fasta
FASTA (> headers).
@ fastq
FASTQ (@ headers).
@ unknown
Format not yet determined from the first header.
Result< void > parse_fastx_sequence_file(fastx_sequence_file &result, std::string const &path)
constexpr bool fastx_is_sequence_whitespace(char ch) noexcept
True for bytes that FASTA sequence consumers conventionally skip between bases.
Result< std::unique_ptr< fastx_sequence_file > > load_fastx_sequence_file(std::string const &path, decompression_source source={}, bool defer_fasta=false, bool read=false, uint32_t pack_k=0)
char const * fasta_next_header(char const *first, char const *last)
First '>' in [first, last) that starts a line, or last.
Result< std::optional< std::string_view > > inflate_and_pack(fastx_sequence_file &result, std::string_view input, std::string const &path, decompression_source source, uint32_t k)
Inflates gzip input in this thread's reusable heap buffer and, for FASTA, packs it there and moves on...
auto const words
Definition top_k.cuh:204
size_t gzip_size_hint(std::string_view input)
__global__ uint16_t const uint32_t uint32_t uint32_t unsigned long long * work
Definition kernels.cuh:474
constexpr uint8_t encode_base(char base) noexcept
Maps one DNA byte to its 2-bit symbol (A=0, C=1, T=2, G=3) or 0xff when invalid.
Definition dna.hpp:16
constexpr bool ablated(std::string_view) noexcept
Always false: this build compiles the ablation switches out.
Definition ablation.hpp:82
uint64_t gzip_decompressed_size(std::string const &path)
Uncompressed size the gzip trailer advertises, or 0 when it cannot be read.
static __global__ uint32_t const * offsets
Definition kernels.cuh:536
libdeflate_result decompress_gzip_member(libdeflate_decompressor *decoder, std::string_view input, char *output, size_t capacity, size_t &consumed, size_t &produced)
Decodes one gzip member, checking structure and ISIZE but not checksums.
Result< size_t > gunzip_members_direct(std::string_view input, decompression_target target, bool &insufficient, std::string const &path)
Decompresses gzip members straight into a caller's buffer.
uint64_t pack_bases_portable(char const *bases, size_t count, uint32_t *words)
uint32_t carry
Definition kernels.cuh:547
char * fasta_sequence_compact(char const *first, char const *last, char *out, char const *&stop)
Compacts one FASTA record's sequence from first to out, stopping at the next header.
auto const width
Definition kernels.cuh:963
bases_packer resolve_bases_packer()
Selects the widest base packer the host supports.
auto const word
Definition top_k.cuh:241
fastx_line_end_fn resolve_fastx_line_end()
Selects the fastest line-end scanner the host supports.
Batch operations over contiguous device rows.
Definition a48.hpp:14
Result< void > Ok() noexcept
Success return for Result<void>; same as return {}.
Definition error.hpp:181
cuda::std::unexpected< Error > Err(Error error)
Failure return; converts to any Result<T> via cuda::std::unexpected.
Definition error.hpp:176
static Error invalid_argument(std::string message)
Definition error.hpp:99
static Error resource(std::string message)
Definition error.hpp:103
Fallible API result: cuda::std::expected<T, Error> with cuDDL factories.
Definition error.hpp:118
static Result ok(T value)
Definition error.hpp:121
A base packer and the bases it takes per step, a multiple of sixteen up to 64.
uint64_t(* pack)(char const *bases, size_t count, uint32_t *words)
Shuffle controls per whitespace mask, for the vector paths.
Supplies storage for one file, or a null target when none is available.
bool direct_reads
Read files with O_DIRECT, as read_whole_file does.
decompression_target request(size_t bytes) const
decompression_target(* acquire)(void *context, size_t bytes)
Host-visible buffer a caller offers for decompressed bytes.
One run of bases packed two bits each: its first word and its length in bases.
A contiguous run of sequence bases (record headers and line breaks excluded).
std::unique_ptr< fastx_mapped_file > file
std::vector< fastx_sequence_extent > extents
std::vector< fastx_packed_record > packed
libdeflate handle, one per thread because the object is single-threaded.
shared_gzip_decompressor & operator=(shared_gzip_decompressor const &)=delete
shared_gzip_decompressor(shared_gzip_decompressor const &)=delete
How a build moved sequence bytes to the device.
bool measure_resident
Measure GPU row reset and sketch construction, excluding input transfers and readback.