/// @file dpf/interleave_leaves.hpp /// @brief Interleave (and deinterleave) per-key leaf vectors into cohort order. /// @details Given `nkeys` vectors of length `nleaves`, write one buffer whose /// logical lane index is `cohort_index(i, k, nkeys) = i * nkeys + k` /// (leaf `i` of key `k`). That matches sequence-cohort layout and the /// key-major axis of interval-cohort leaves (lanes inside a leaf stay /// contiguous in each one-key buffer; this routine interleaves whole /// leaf *values*, not interior walk nodes). /// /// Storage and packing: /// - `uint64_t` / `uint32_t` / `uint16_t` / `uint8_t`: one array element /// per leaf; `out[i * nkeys + k] = keys[k][i]`. /// - `dpf::bit`, `dpf::twobit`, `dpf::nyble`, and the matching /// `dpf::gf2` / `gf22` / `gf24` lanes: same logical order, packed /// tightly into `uint64_t` words like `dynamic_bit_array` / /// `dynamic_packed_array` — lane `j` occupies bits /// `[j * W, (j + 1) * W)` of the flat stream (`W` = 1, 2, or 4), /// low lane in the low bits of each word. So a naive *element* /// index `i * nkeys + k` is wrong for sub-byte widths; use the /// logical index above, then pack with width `W`. /// /// Kernels are word-wise (and 8×8 bit transpose batches for 1-bit), /// not a scalar loop per bit. Deinterleave is the inverse transpose. /// @copyright Copyright (c) 2019-2026 Ryan Henry and [others](@ref authors) /// @license Released under a GNU General Public v2.0 (GPLv2) license; /// see [LICENSE.md](@ref license) for details. #ifndef LIBDPF_INCLUDE_DPF_INTERLEAVE_LEAVES_HPP__ #define LIBDPF_INCLUDE_DPF_INTERLEAVE_LEAVES_HPP__ #include #include #include #include #include "hedley/hedley.h" #include "dpf/bit.hpp" #include "dpf/modint.hpp" #include "dpf/nyble.hpp" #include "dpf/twobit.hpp" #include "dpf/utils.hpp" namespace dpf { /// @brief Underlying word/element type used to store a sequence of `LaneT` leaves. template struct leaf_storage { using type = LaneT; }; template struct leaf_storage>> { using type = std::uint64_t; }; template <> struct leaf_storage { using type = std::uint64_t; }; template <> struct leaf_storage { using type = std::uint64_t; }; template <> struct leaf_storage { using type = std::uint64_t; }; template using leaf_storage_t = typename leaf_storage::type; /// @brief Number of `leaf_storage_t` words needed for `n` logical leaves. template HEDLEY_CONST HEDLEY_NO_THROW constexpr std::size_t leaf_storage_words(std::size_t n) noexcept { if constexpr (utils::is_packed_subbyte_v) { constexpr std::size_t w = utils::packed_lane_bits_v; return (n * w + 63u) / 64u; } else { return n; } } namespace interleave_detail { /// @brief 8×8 bit matrix transpose in a 64-bit word (Hacker's Delight). /// @details Byte `r` holds row `r`; bit `c` of that byte is column `c` /// (LSB = column 0). Transpose swaps rows and columns. HEDLEY_CONST HEDLEY_NO_THROW HEDLEY_ALWAYS_INLINE constexpr std::uint64_t bit_transpose_8x8(std::uint64_t x) noexcept { std::uint64_t t = (x ^ (x >> 7)) & 0x00AA00AA00AA00AAull; x = x ^ t ^ (t << 7); t = (x ^ (x >> 14)) & 0x0000CCCC0000CCCCull; x = x ^ t ^ (t << 14); t = (x ^ (x >> 28)) & 0x00000000F0F0F0F0ull; x = x ^ t ^ (t << 28); return x; } HEDLEY_NO_THROW HEDLEY_ALWAYS_INLINE void write_bits(std::uint64_t * out, std::size_t bit_pos, std::uint64_t bits, unsigned nbits) noexcept { if (nbits == 0) return; const std::size_t word = bit_pos / 64u; const unsigned shift = static_cast(bit_pos % 64u); const std::uint64_t mask = nbits == 64 ? ~std::uint64_t{0} : ((std::uint64_t{1} << nbits) - 1u); const std::uint64_t val = bits & mask; out[word] |= val << shift; if (shift + nbits > 64u) out[word + 1u] |= val >> (64u - shift); } HEDLEY_NO_THROW HEDLEY_ALWAYS_INLINE std::uint64_t read_bits(const std::uint64_t * in, std::size_t bit_pos, unsigned nbits) noexcept { if (nbits == 0) return 0; const std::size_t word = bit_pos / 64u; const unsigned shift = static_cast(bit_pos % 64u); const std::uint64_t mask = nbits == 64 ? ~std::uint64_t{0} : ((std::uint64_t{1} << nbits) - 1u); std::uint64_t val = in[word] >> shift; if (shift + nbits > 64u) val |= in[word + 1u] << (64u - shift); return val & mask; } HEDLEY_NO_THROW HEDLEY_ALWAYS_INLINE std::uint8_t load_key_byte_bits(const std::uint64_t * key, std::size_t nleaves, std::size_t i0) noexcept { if (i0 >= nleaves) return 0; const unsigned take = static_cast( nleaves - i0 < 8u ? nleaves - i0 : 8u); return static_cast(read_bits(key, i0, take)); } /// @brief Interleave 1-bit lanes with 8×8 transpose batches plus a bit writer. HEDLEY_NO_THROW inline void interleave_bits(std::uint64_t * HEDLEY_RESTRICT out, const std::uint64_t * const * HEDLEY_RESTRICT keys, std::size_t nkeys, std::size_t nleaves) noexcept { const std::size_t out_words = leaf_storage_words(nkeys * nleaves); if (out_words != 0) std::memset(out, 0, out_words * sizeof(std::uint64_t)); if (nkeys == 0 || nleaves == 0) return; for (std::size_t i0 = 0; i0 < nleaves; i0 += 8u) { const unsigned nrows = static_cast( nleaves - i0 < 8u ? nleaves - i0 : 8u); for (std::size_t k0 = 0; k0 < nkeys; k0 += 8u) { const unsigned ncols = static_cast( nkeys - k0 < 8u ? nkeys - k0 : 8u); std::uint64_t packed = 0; DPF_UNROLL_LOOP for (unsigned c = 0; c < 8u; ++c) { std::uint8_t b = 0; if (c < ncols) b = load_key_byte_bits(keys[k0 + c], nleaves, i0); packed |= static_cast(b) << (8u * c); } const std::uint64_t t = bit_transpose_8x8(packed); DPF_UNROLL_LOOP for (unsigned r = 0; r < nrows; ++r) { const std::uint64_t row = (t >> (8u * r)) & ((std::uint64_t{1} << ncols) - 1u); write_bits(out, (i0 + r) * nkeys + k0, row, ncols); } } } } HEDLEY_NO_THROW inline void deinterleave_bits(std::uint64_t * const * HEDLEY_RESTRICT keys, const std::uint64_t * HEDLEY_RESTRICT in, std::size_t nkeys, std::size_t nleaves) noexcept { if (nkeys == 0 || nleaves == 0) return; for (std::size_t k = 0; k < nkeys; ++k) { const std::size_t words = leaf_storage_words(nleaves); if (words != 0) std::memset(keys[k], 0, words * sizeof(std::uint64_t)); } for (std::size_t i0 = 0; i0 < nleaves; i0 += 8u) { const unsigned nrows = static_cast( nleaves - i0 < 8u ? nleaves - i0 : 8u); for (std::size_t k0 = 0; k0 < nkeys; k0 += 8u) { const unsigned ncols = static_cast( nkeys - k0 < 8u ? nkeys - k0 : 8u); std::uint64_t packed = 0; DPF_UNROLL_LOOP for (unsigned r = 0; r < nrows; ++r) { const std::uint64_t row = read_bits(in, (i0 + r) * nkeys + k0, ncols); packed |= row << (8u * r); } // Inverse of the interleave transpose: same 8×8 transpose. const std::uint64_t t = bit_transpose_8x8(packed); DPF_UNROLL_LOOP for (unsigned c = 0; c < ncols; ++c) { const std::uint64_t col = (t >> (8u * c)) & ((std::uint64_t{1} << nrows) - 1u); write_bits(keys[k0 + c], i0, col, nrows); } } } } /// @brief Interleave `W`-bit lanes (`W` = 2 or 4) by filling output words. template HEDLEY_NO_THROW void interleave_packed_w(std::uint64_t * HEDLEY_RESTRICT out, const std::uint64_t * const * HEDLEY_RESTRICT keys, std::size_t nkeys, std::size_t nleaves) noexcept { static_assert(W == 2 || W == 4, "packed width must be 2 or 4"); constexpr std::uint64_t lane_mask = (std::uint64_t{1} << W) - 1u; constexpr unsigned lanes_per_word = 64u / W; const std::size_t total = nkeys * nleaves; const std::size_t out_words = (total * W + 63u) / 64u; if (out_words != 0) std::memset(out, 0, out_words * sizeof(std::uint64_t)); if (nkeys == 0 || nleaves == 0) return; for (std::size_t ow = 0; ow < out_words; ++ow) { std::uint64_t word = 0; const std::size_t base = ow * lanes_per_word; const unsigned nlanes = static_cast( total - base < lanes_per_word ? total - base : lanes_per_word); DPF_UNROLL_LOOP for (unsigned t = 0; t < nlanes; ++t) { const std::size_t g = base + t; const std::size_t i = g / nkeys; const std::size_t k = g - i * nkeys; const std::size_t src_bit = i * W; const std::uint64_t lane = (keys[k][src_bit / 64u] >> (src_bit % 64u)) & lane_mask; word |= lane << (t * W); } out[ow] = word; } } template HEDLEY_NO_THROW void deinterleave_packed_w(std::uint64_t * const * HEDLEY_RESTRICT keys, const std::uint64_t * HEDLEY_RESTRICT in, std::size_t nkeys, std::size_t nleaves) noexcept { static_assert(W == 2 || W == 4, "packed width must be 2 or 4"); constexpr std::uint64_t lane_mask = (std::uint64_t{1} << W) - 1u; if (nkeys == 0 || nleaves == 0) return; for (std::size_t k = 0; k < nkeys; ++k) { const std::size_t words = (nleaves * W + 63u) / 64u; if (words != 0) std::memset(keys[k], 0, words * sizeof(std::uint64_t)); } const std::size_t total = nkeys * nleaves; for (std::size_t g = 0; g < total; ++g) { const std::size_t i = g / nkeys; const std::size_t k = g - i * nkeys; const std::size_t src_bit = g * W; const std::uint64_t lane = (in[src_bit / 64u] >> (src_bit % 64u)) & lane_mask; const std::size_t dst_bit = i * W; keys[k][dst_bit / 64u] |= lane << (dst_bit % 64u); } } template HEDLEY_NO_THROW void interleave_wide(T * HEDLEY_RESTRICT out, const T * const * HEDLEY_RESTRICT keys, std::size_t nkeys, std::size_t nleaves) noexcept { if (nkeys == 0 || nleaves == 0) return; if (nkeys == 1) { std::memcpy(out, keys[0], nleaves * sizeof(T)); return; } for (std::size_t i = 0; i < nleaves; ++i) { T * dst = out + i * nkeys; DPF_UNROLL_LOOP for (std::size_t k = 0; k < nkeys; ++k) dst[k] = keys[k][i]; } } template HEDLEY_NO_THROW void deinterleave_wide(T * const * HEDLEY_RESTRICT keys, const T * HEDLEY_RESTRICT in, std::size_t nkeys, std::size_t nleaves) noexcept { if (nkeys == 0 || nleaves == 0) return; if (nkeys == 1) { std::memcpy(keys[0], in, nleaves * sizeof(T)); return; } for (std::size_t i = 0; i < nleaves; ++i) { const T * src = in + i * nkeys; DPF_UNROLL_LOOP for (std::size_t k = 0; k < nkeys; ++k) keys[k][i] = src[k]; } } } // namespace interleave_detail /// @brief Interleave `nkeys` leaf vectors of length `nleaves` into `out`. /// @tparam LaneT an integer, a packed lane (`bit`, `twobit`, `nyble`, /// `gf2`, `gf22`, `gf24`), or a trivially copyable element of at /// most 8 bytes (`gf28`, `gf216`, `gf232`, `gf264`) /// @param out destination storage (`leaf_storage_t`; see file comment) /// @param keys array of `nkeys` pointers to per-key leaf storage /// @param nkeys number of keys (`m`) /// @param nleaves number of leaves per key (`L`) /// \complexity Θ(`nkeys * nleaves`) lane moves. Wide types stream by leaf; /// 1-bit uses 8×8 transpose tiles; 2/4-bit fill output words. template HEDLEY_NO_THROW void interleave_leaves(leaf_storage_t * out, const leaf_storage_t * const * keys, std::size_t nkeys, std::size_t nleaves) noexcept { if constexpr (utils::is_packed_subbyte_v) { constexpr unsigned w = utils::packed_lane_bits_v; if constexpr (w == 1) interleave_detail::interleave_bits(out, keys, nkeys, nleaves); else if constexpr (w == 2) interleave_detail::interleave_packed_w<2>(out, keys, nkeys, nleaves); else { static_assert(w == 4, "packed lane width must be 1, 2, or 4"); interleave_detail::interleave_packed_w<4>(out, keys, nkeys, nleaves); } } else if constexpr ((std::is_integral_v && !std::is_same_v) || (std::is_trivially_copyable_v && std::is_standard_layout_v && sizeof(LaneT) > 0 && sizeof(LaneT) <= 8)) { interleave_detail::interleave_wide(out, keys, nkeys, nleaves); } else { static_assert(std::is_integral_v, "interleave_leaves LaneT must be an integer, a packed lane, or a field element of at most 8 bytes"); } } /// @brief Inverse of `interleave_leaves`: split one interleaved buffer into /// `nkeys` per-key leaf vectors of length `nleaves`. /// @tparam LaneT same set as `interleave_leaves` /// @param keys destination per-key storage pointers /// @param in interleaved source /// @param nkeys number of keys /// @param nleaves number of leaves per key /// \complexity Same order as `interleave_leaves`. template HEDLEY_NO_THROW void deinterleave_leaves(leaf_storage_t * const * keys, const leaf_storage_t * in, std::size_t nkeys, std::size_t nleaves) noexcept { if constexpr (utils::is_packed_subbyte_v) { constexpr unsigned w = utils::packed_lane_bits_v; if constexpr (w == 1) interleave_detail::deinterleave_bits(keys, in, nkeys, nleaves); else if constexpr (w == 2) interleave_detail::deinterleave_packed_w<2>(keys, in, nkeys, nleaves); else { static_assert(w == 4, "packed lane width must be 1, 2, or 4"); interleave_detail::deinterleave_packed_w<4>(keys, in, nkeys, nleaves); } } else if constexpr ((std::is_integral_v && !std::is_same_v) || (std::is_trivially_copyable_v && std::is_standard_layout_v && sizeof(LaneT) > 0 && sizeof(LaneT) <= 8)) { interleave_detail::deinterleave_wide(keys, in, nkeys, nleaves); } else { static_assert(std::is_integral_v, "deinterleave_leaves LaneT must be an integer, a packed lane, or a field element of at most 8 bytes"); } } /// @brief Dot an interleaved 1-bit cohort with `weights`, as `modint`. /// @details `bits` is the buffer from `interleave_leaves`: leaf `i` /// is the integer whose bit `k` is key `k`'s leaf `i`. That integer, /// reduced modulo `2^Nbits` (the low `Nbits` bits), is one `modint`. /// The result is `sum_i modint(leaf i) * weights[i]`. `weights[i]` /// may be a `modint` or an integer. /// When `nkeys == Nbits` and that width is 8, 16, 32, 64, or 128, /// the leaves are contiguous native words and the product is a /// straight multiply-accumulate. /// @tparam Nbits modulus width, `1` through `256` /// @param bits interleaved 1-bit stream /// @param nleaves number of leaves (domain points) /// @param nkeys number of keys that were interleaved /// @param weights one weight per leaf /// @return the inner product in `modint` /// \complexity Θ(`nleaves`) multiplications in the `modint` word. Aligned /// widths do one native multiply per leaf; other widths extract /// the `Nbits` bits of each leaf first. template HEDLEY_NO_THROW HEDLEY_WARN_UNUSED_RESULT modint interleaved_bits_inner_product(const std::uint64_t * bits, std::size_t nleaves, std::size_t nkeys, Weights && weights) { using mod = modint; using limb = typename mod::integral_type; mod acc{}; if (nleaves == 0 || nkeys == 0 || bits == nullptr) return acc; auto limb_of = [](auto v) -> limb { using T = std::decay_t; if constexpr (std::is_same_v) return static_cast(v); else return static_cast(mod(static_cast(v))); }; auto mac = [&](limb v, std::size_t i) { acc += mod(v) * mod(limb_of(weights[i])); }; if constexpr (Nbits == 128) { if (nkeys == 128) { limb sum{}; for (std::size_t i = 0; i < nleaves; ++i) { const std::uint64_t * p = bits + i * 2u; const limb v = static_cast(p[0]) | (static_cast(p[1]) << 64); sum += v * limb_of(weights[i]); } return mod(sum); } } if constexpr (Nbits == 8 || Nbits == 16 || Nbits == 32 || Nbits == 64) { if (nkeys == Nbits) { limb sum{}; for (std::size_t i = 0; i < nleaves; ++i) { limb v{}; if constexpr (Nbits == 64) v = static_cast(bits[i]); else if constexpr (Nbits == 32) { const std::uint64_t word = bits[i / 2u]; v = static_cast((i & 1u) ? (word >> 32) : (word & 0xffffffffu)); } else if constexpr (Nbits == 16) { const std::uint64_t word = bits[i / 4u]; v = static_cast((word >> ((i % 4u) * 16u)) & 0xffffu); } else { const std::uint64_t word = bits[i / 8u]; v = static_cast((word >> ((i % 8u) * 8u)) & 0xffu); } sum += v * limb_of(weights[i]); } return mod(sum); } } for (std::size_t i = 0; i < nleaves; ++i) { const std::size_t bit_pos = i * nkeys; limb v{}; if constexpr (Nbits <= 64) { const unsigned take = static_cast( nkeys < Nbits ? nkeys : Nbits); v = static_cast(interleave_detail::read_bits(bits, bit_pos, take)); } else { unsigned left = static_cast(Nbits); std::size_t pos = bit_pos; unsigned shift = 0; while (left != 0 && shift < nkeys) { const unsigned room = static_cast(nkeys - shift); const unsigned n = left < 64u ? left : 64u; const unsigned take = n < room ? n : room; const auto chunk = interleave_detail::read_bits(bits, pos, take); v |= static_cast(chunk) << shift; shift += take; pos += take; left -= take; if (take == 0) break; } } mac(v, i); } return acc; } } // namespace dpf #endif // LIBDPF_INCLUDE_DPF_INTERLEAVE_LEAVES_HPP__