/// @file dpf/eval_inner_product.hpp /// @brief Full / interval / sequence DPF evaluation that reduces against a /// public vector instead of materializing the output. /// @details Three local forms share the same interval / sequence domains: /// - **Batched leaf walk** (no tag): one output, weights in /// `eval_interval` layout, exterior AES batched like that walk, /// O(1) accumulator. Cost shape matches `eval_interval` on the /// same range (Θ(L) nodes) plus a multiply-add per packed slot. /// - **`dpf::paired`** (row-wise): one row of the weight vector per /// input. A row is a scalar (one output) or a `tuple` / `array` /// zipped with several outputs — leaf slots or an ancestor prefix /// plus the leaf — read off one path. Products use `operator*`; /// they are summed with `operator+`. Same walk cost as the point /// list, plus O(1) arithmetic per selected output per input. /// - **`dpf::columns`** (transposed / column-wise): one output, /// several weight streams, one accumulator per stream, one walk. /// Products are *not* summed across streams. A stream is anything /// with `w[i]` or `w(i)`. `dpf::project` maps the share before the /// multiply; `dpf::also` sees the unmapped share. Cost is one path /// walk plus O(stream count) arithmetic per input. /// /// A single-stream `columns` result matches `paired` on that stream; /// a single-output batched leaf walk matches `paired` when the /// interval is leaf-aligned (covering-leaf weights equal the clipped /// domain points). Unaligned intervals still weight every lane of the /// covering leaves — same layout as one-key `eval_interval` buffers / /// cohort interval inner products, not the clipped iterable. A sized /// weight container shorter than that covering span throws /// `std::invalid_argument`. `paired` and `columns` throw the same way /// when a sized stream is shorter than the point list or the clipped /// interval. #ifndef LIBDPF_INCLUDE_DPF_EVAL_INNER_PRODUCT_HPP__ #define LIBDPF_INCLUDE_DPF_EVAL_INNER_PRODUCT_HPP__ #include #include #include #include #include #include #include #include #include #include #include #include "hedley/hedley.h" #include #include #include "dpf/dpf_key.hpp" #include "dpf/eval_interval.hpp" #include "dpf/eval_sequence.hpp" #include "dpf/eval_target.hpp" #include "dpf/incremental.hpp" #include "dpf/leaf_node.hpp" #include "dpf/path_memoizer.hpp" #include "dpf/twiddle.hpp" #include "dpf/utils.hpp" #include "dpf/xor_wrapper.hpp" namespace dpf { namespace detail_ip_check { template struct has_container_size : std::false_type { }; template struct has_container_size()))>> : std::true_type { }; /// @brief Throw when a sized weight container is shorter than the walk. /// Unsizable weights (`w(i)` callables, raw pointers) are left to /// the caller. template void require_weight_count(const W & w, std::size_t need, const char * what) { if constexpr (has_container_size>::value) { if (static_cast(std::size(w)) < need) throw std::invalid_argument(what); } } } // namespace detail_ip_check namespace internal { template struct is_xor_wrapper : std::false_type {}; template struct is_xor_wrapper> : std::true_type {}; template inline constexpr bool is_xor_wrapper_v = is_xor_wrapper::value; template HEDLEY_ALWAYS_INLINE auto weight_as_u64(W && w, std::size_t i) { return static_cast(w[i]); } HEDLEY_ALWAYS_INLINE simde__m128i load_weight_pair_u64(psnip_uint64_t lo, psnip_uint64_t hi) { return simde_mm_set_epi64x(static_cast(hi), static_cast(lo)); } HEDLEY_ALWAYS_INLINE simde__m128i mullo_epi64x2(simde__m128i a, simde__m128i b) { #if defined(__AVX512DQ__) && defined(__AVX512VL__) return _mm_mullo_epi64(a, b); #else psnip_uint64_t av[2], bv[2]; std::memcpy(av, &a, sizeof(av)); std::memcpy(bv, &b, sizeof(bv)); av[0] *= bv[0]; av[1] *= bv[1]; simde__m128i r; std::memcpy(&r, av, sizeof(r)); return r; #endif } template struct ip_prg_range { static constexpr std::size_t pos_min = const_min_size...>::value; static constexpr std::size_t pos_end = const_max_size<(block_offset_of_leaf_v + block_length_of_leaf_v, NodeT>)...>::value; static constexpr std::size_t count = pos_end - pos_min; }; template struct ip_accum { using output_type = OutputT; static constexpr bool xor_mode = is_xor_wrapper_v; static constexpr bool simd64 = (sizeof(OutputT) == 8); simde__m128i vacc = simde_mm_setzero_si128(); output_type scalar{}; template HEDLEY_ALWAYS_INLINE void mac(const LeafT & leaf, std::size_t base, std::size_t opl, W && w) { HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") if constexpr (simd64 && std::is_same_v) { if (HEDLEY_LIKELY(opl == 2)) { simde__m128i ww = load_weight_pair_u64( weight_as_u64(w, base), weight_as_u64(w, base + 1)); if constexpr (xor_mode) { vacc = simde_mm_xor_si128(vacc, simde_mm_and_si128(leaf, ww)); } else { vacc = simde_mm_add_epi64(vacc, mullo_epi64x2(leaf, ww)); } return; } } HEDLEY_PRAGMA(GCC diagnostic pop) for (std::size_t p = 0; p < opl; ++p) { output_type val; if constexpr (utils::is_packed_subbyte_v) { val = extract_leaf, output_type>(leaf, p); } else { std::memcpy(&val, reinterpret_cast(std::addressof(leaf)) + p * sizeof(output_type), sizeof(val)); } const auto wt = weight_as_u64(w, base + p); if constexpr (xor_mode) { scalar = output_type{static_cast( static_cast(scalar) ^ (static_cast(val) & wt))}; } else if constexpr (utils::is_packed_subbyte_v && !std::is_same_v) { constexpr unsigned mask = (1u << utils::packed_lane_bits_v) - 1u; const auto wlane = static_cast( static_cast(wt) & mask); scalar = scalar + val * wlane; } else { scalar = static_cast( static_cast(scalar) + static_cast(val) * wt); } } } HEDLEY_ALWAYS_INLINE output_type finish() const { if constexpr (simd64) { psnip_uint64_t lanes[2]; std::memcpy(lanes, &vacc, sizeof(lanes)); if constexpr (xor_mode) { return output_type{static_cast( (lanes[0] ^ lanes[1]) ^ static_cast(scalar))}; } else { return output_type{ lanes[0] + lanes[1] + static_cast(scalar)}; } } return scalar; } }; template void eval_inner_product_exterior(const DpfKey & dpf, IntegralT from_node, IntegralT to_node, Weights && weights, IntervalMemoizer && memoizer, std::index_sequence, std::tuple>...> & accs, std::size_t start = 0) { assert_not_wildcard_output(dpf); if (HEDLEY_UNLIKELY(to_node < from_node && to_node != IntegralT{0})) throw std::runtime_error("to_node; HEDLEY_PRAGMA(GCC diagnostic pop) constexpr std::size_t opl = DpfKey::outputs_per_leaf; std::size_t nodes_in_interval = static_cast(to_node - from_node); // `to_node == 0` is the saturated exclusive end; the subtraction is the // leaf count. A real inverted range is rejected above. auto *nodes = memoizer[DpfKey::depth]; auto cws = std::make_tuple(std::get(dpf.leaf_nodes).get()...); if constexpr (DpfKey::is_extractable) { std::size_t j = 0, k = start; for (; j < nodes_in_interval; ++j, ++k) { auto apply_output = [&](auto out_index, auto buf_index) { constexpr std::size_t out_i = decltype(out_index)::value; constexpr std::size_t buf_i = decltype(buf_index)::value; auto leaf = dpf.template traverse_exterior(nodes[j]); std::get(accs).mac(leaf, k * opl, opl, utils::get(weights)); }; (apply_output(std::integral_constant{}, std::integral_constant{}), ...); } return; } HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") auto apply_masks = [&](std::size_t k, const node_type & node, const node_type * HEDLEY_RESTRICT masks) { auto apply_output = [&](auto out_index, auto buf_index) { constexpr std::size_t out_i = decltype(out_index)::value; constexpr std::size_t buf_i = decltype(buf_index)::value; using output_type = typename DpfKey::concrete_output_type; using leaf_type = dpf::leaf_node_t; constexpr auto pos = block_offset_of_leaf_v; leaf_type mask; std::memcpy(&mask, masks + (pos - range::pos_min), sizeof(leaf_type)); // Subtractive share: CW_if_t − mask so reconstruct(y0, y1) = y0 − y1 = β. auto leaf = dpf::subtract_leaf( get_if_lo_bit(std::get(cws), node), mask); std::get(accs).mac(leaf, k * opl, opl, utils::get(weights)); }; (apply_output(std::integral_constant{}, std::integral_constant{}), ...); }; std::size_t j = 0, k = start; if constexpr (range::count == 2 && range::pos_min == 0) { for (; j + 4 <= nodes_in_interval; j += 4, k += 4) { alignas(node_type) node_type seeds[4]; alignas(node_type) node_type left[4]; alignas(node_type) node_type right[4]; DPF_UNROLL_LOOP for (std::size_t t = 0; t < 4; ++t) { seeds[t] = utils::to_exterior_node( unset_lo_2bits(nodes[j + t])); } DpfKey::exterior_prg::eval01_x4(seeds, left, right); DPF_UNROLL_LOOP for (std::size_t t = 0; t < 4; ++t) { node_type masks[2] = {left[t], right[t]}; apply_masks(k + t, nodes[j + t], masks); } } } else if constexpr (range::count == 1) { const auto pos = static_cast(range::pos_min); for (; j + 8 <= nodes_in_interval; j += 8, k += 8) { alignas(node_type) node_type seeds[8]; alignas(node_type) node_type masks[8]; HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Warray-bounds") DPF_UNROLL_LOOP for (std::size_t t = 0; t < 8; ++t) { seeds[t] = utils::to_exterior_node( unset_lo_2bits(nodes[j + t])); } HEDLEY_PRAGMA(GCC diagnostic pop) DpfKey::exterior_prg::eval_x8(seeds, masks, pos); DPF_UNROLL_LOOP for (std::size_t t = 0; t < 8; ++t) { apply_masks(k + t, nodes[j + t], &masks[t]); } } for (; j + 4 <= nodes_in_interval; j += 4, k += 4) { alignas(node_type) node_type seeds[4]; alignas(node_type) node_type masks[4]; DPF_UNROLL_LOOP for (std::size_t t = 0; t < 4; ++t) { seeds[t] = utils::to_exterior_node( unset_lo_2bits(nodes[j + t])); } DpfKey::exterior_prg::eval_x4(seeds, masks, pos); DPF_UNROLL_LOOP for (std::size_t t = 0; t < 4; ++t) { apply_masks(k + t, nodes[j + t], &masks[t]); } } } DPF_UNROLL_LOOP for (; j < nodes_in_interval; ++j, ++k) { const auto & node = nodes[j]; auto seed = utils::to_exterior_node(unset_lo_2bits(node)); std::array masks; DpfKey::exterior_prg::eval(seed, masks.data(), static_cast(range::count), static_cast(range::pos_min)); apply_masks(k, node, masks.data()); } HEDLEY_PRAGMA(GCC diagnostic pop) } template void eval_prepare_nodes(const DpfKey & dpf, InputT from, InputT to, IntervalMemoizer && memoizer) { using dpf_type = DpfKey; using integral_type = typename DpfKey::integral_type; utils::flip_msb_if_signed_integral(from); utils::flip_msb_if_signed_integral(to); integral_type from_node = utils::get_from_node(from); integral_type to_node = utils::get_to_node(to); constexpr auto to_int = utils::to_integral_type{}; const bool wraps = utils::interval_wraps( static_cast(to_int(from)), static_cast(to_int(to)), utils::bitlength_of_v); auto segs = utils::split_leaf_nodes(from_node, to_node, dpf.depth, wraps); // The memoizer keeps one interval. A wrap is two intervals, and walking // the first clobbers the second, so only a single segment can be cached. if (segs.n == 1) { eval_interval_interior(dpf, segs.seg[0].from_node, segs.seg[0].to_node, memoizer); } } template auto eval_inner_product_impl(const DpfKey & dpf, InputT from, InputT to, Weights && weights, IntervalMemoizer && memoizer, std::index_sequence) { using dpf_type = DpfKey; using integral_type = typename DpfKey::integral_type; utils::flip_msb_if_signed_integral(from); utils::flip_msb_if_signed_integral(to); integral_type from_node = utils::get_from_node(from); integral_type to_node = utils::get_to_node(to); constexpr auto to_int = utils::to_integral_type{}; const bool wraps = utils::interval_wraps( static_cast(to_int(from)), static_cast(to_int(to)), utils::bitlength_of_v); auto segs = utils::split_leaf_nodes(from_node, to_node, dpf.depth, wraps); constexpr std::size_t opl = DpfKey::outputs_per_leaf; const std::size_t lanes = segs.total * opl; auto check_one = [&](auto which) { constexpr std::size_t wi = decltype(which)::value; detail_ip_check::require_weight_count( utils::get(weights), lanes, "inner product weights are shorter than the covering leaves"); }; (check_one(std::integral_constant{}), ...); auto accs = std::make_tuple( ip_accum>{}...); auto idxs = std::index_sequence{}; std::size_t start = 0; for (std::size_t s = 0; s < segs.n; ++s) { const auto & seg = segs.seg[s]; eval_interval_interior(dpf, seg.from_node, seg.to_node, memoizer); eval_inner_product_exterior(dpf, seg.from_node, seg.to_node, weights, memoizer, idxs, accs, start); start += seg.count; } if constexpr (sizeof...(Is) == 1) { return std::get<0>(accs).finish(); } else { return std::make_tuple(std::get(accs).finish()...); } } } // namespace internal /// @brief Expand the interior tree for `[from, to]`. A wrapping interval is left /// cold: the memoizer holds one half, and walking the first half of the later /// inner product would clobber a cached second half. Safe to call before the /// weight vector exists; a subsequent inner-product on the same memoizer /// skips the interior AES when the interval did not wrap. /// @tparam DpfKey DPF key type /// @tparam InputT input domain type /// @tparam IntervalMemoizer interval memoizer type /// @param dpf the DPF key /// @param from the inclusive start of the range /// @param to the `to` /// @param memoizer the memoizer built for this key template HEDLEY_ALWAYS_INLINE void eval_prepare_interval(const DpfKey & dpf, InputT from, InputT to, IntervalMemoizer && memoizer) { internal::eval_prepare_nodes(dpf, dpf.offset_x(from), dpf.offset_x(to), memoizer); } template HEDLEY_ALWAYS_INLINE void eval_prepare_full(const DpfKey & dpf, IntervalMemoizer && memoizer) { using input_type = typename DpfKey::input_type; eval_prepare_interval(dpf, std::numeric_limits::min(), std::numeric_limits::max(), memoizer); } /// @brief `sum_x DPF_I(x) * w[x]` (additive) or `xor_x DPF_I(x) & w[x]` (XOR). /// @details `w[j]` is the weight for the `j`-th lane of the *covering* leaves /// of `[from, to]`, matching `eval_interval`'s destination buffer (not the /// clipped iterable). Multiple `Is` take a tuple of weight ranges and return /// a tuple of accumulators; a single `I` takes one range and returns one /// accumulator. /// @tparam I output index /// @tparam Is is /// @tparam DpfKey DPF key type /// @tparam InputT input domain type /// @tparam Weights weights /// @tparam IntervalMemoizer interval memoizer type /// @tparam DpfKey DPF key type /// @param dpf the DPF key /// @param from the inclusive start of the range /// @param to the `to` /// @param weights the weights /// @param memoizer the memoizer built for this key /// @return `sum_x DPF_I(x) * w[x]` (additive) or `xor_x DPF_I(x) & w[x]` (XOR) /// \complexity Same interior expansion as `eval_interval` on `[from, to]` (Θ(L) nodes, L = leaf nodes in the interval) plus a multiply-add per output slot into an O(1) accumulator. The basic memoizer still holds O(L) nodes. template && !is_multilevel_key_v, bool> = true> HEDLEY_ALWAYS_INLINE HEDLEY_WARN_UNUSED_RESULT auto eval_inner_product(const DpfKey & dpf, InputT from, InputT to, Weights && weights, IntervalMemoizer && memoizer) { assert_not_wildcard_output(dpf); return internal::eval_inner_product_impl( dpf, dpf.offset_x(from), dpf.offset_x(to), weights, memoizer, std::make_index_sequence<1 + sizeof...(Is)>{}); } /// @brief Inner product with a VDPF path proof over the same interval nodes. /// @details Folds once per BFS node (same transcript as `prove_interval`), then /// evaluates. Weights are not mixed into the token. /// \complexity Same interior expansion as `eval_interval` on `[from, to]` (Θ(L) nodes, L = leaf nodes in the interval) plus a multiply-add per output slot into an O(1) accumulator. The basic memoizer still holds O(L) nodes. template && !is_multilevel_key_v, bool> = true> HEDLEY_ALWAYS_INLINE HEDLEY_WARN_UNUSED_RESULT auto eval_inner_product(const DpfKey & dpf, InputT from, InputT to, Weights && weights, IntervalMemoizer && memoizer, prove_ref pr) { static_assert(DpfKey::is_verifiable, "eval_inner_product(..., prove(π)): key must carry dpf::verifiable"); detail::vdpf::init_proof(pr.token, dpf); prove_fold_interval(dpf, from, to, pr.token); detail::vdpf::fold_output_binding(pr.token, dpf); return eval_inner_product(dpf, from, to, std::forward(weights), std::forward(memoizer)); } /// \complexity Same interior expansion as `eval_interval` on `[from, to]` (Θ(L) nodes, L = leaf nodes in the interval) plus a multiply-add per output slot into an O(1) accumulator. The basic memoizer still holds O(L) nodes. template && !is_multilevel_key_v, bool> = true> HEDLEY_ALWAYS_INLINE HEDLEY_WARN_UNUSED_RESULT auto eval_inner_product(const DpfKey & dpf, InputT from, InputT to, Weights && weights, prove_ref pr) { return eval_inner_product(dpf, from, to, std::forward(weights), dpf::make_basic_interval_memoizer(dpf, from, to), pr); } template && !is_multilevel_key_v, bool> = true> HEDLEY_ALWAYS_INLINE HEDLEY_WARN_UNUSED_RESULT auto eval_full_inner_product(const DpfKey & dpf, Weights && weights, IntervalMemoizer && memoizer) { using input_type = typename DpfKey::input_type; return eval_inner_product(dpf, std::numeric_limits::min(), std::numeric_limits::max(), weights, memoizer); } /// @brief Tag: row-wise zip of several DPF outputs with each weight element. struct paired_t { }; inline constexpr paired_t paired{}; /// @brief Tag: transposed walk — one output, several weight streams kept apart. /// @details Unlike `paired`, products are not summed across streams. struct columns_t { }; inline constexpr columns_t columns{}; /// @brief Map a leaf share before it is multiplied by column weights. template struct project_fn { F fn; }; /// @brief Wrap `fn` as the column projector. `fn` is called as `fn(share)`. template HEDLEY_ALWAYS_INLINE HEDLEY_WARN_UNUSED_RESULT project_fn> project(F && fn) { return project_fn>{std::forward(fn)}; } /// @brief Observe each unmapped share during a `columns` walk. template struct also_fn { F fn; }; /// @brief Wrap `fn` as a `columns` side visit. /// @details `fn` is called as `fn(i, x, share)`: list index, domain point, /// then the share `project` has not seen. template HEDLEY_ALWAYS_INLINE HEDLEY_WARN_UNUSED_RESULT also_fn> also(F && fn) { return also_fn>{std::forward(fn)}; } /// @brief Column projector that returns the share unchanged. struct identity_project { template HEDLEY_ALWAYS_INLINE constexpr T operator()(T value) const { return value; } }; /// @brief Column side visit that ignores its arguments. struct noop_also { template HEDLEY_ALWAYS_INLINE void operator()(A && ...) const noexcept { } }; namespace detail_ip { template struct has_public_addends : std::false_type { }; template struct has_public_addends().public_addends)>> : std::true_type { }; template struct is_std_array : std::false_type { }; template struct is_std_array> : std::true_type { }; /// @brief Component `K` of a row. A scalar row pairs with output 0 only. template HEDLEY_ALWAYS_INLINE decltype(auto) component(Row && row) { using R = std::decay_t; if constexpr (utils::is_tuple_v || is_std_array::value) return std::get(std::forward(row)); else { static_assert(K == 0, "a scalar weight pairs with one DPF output; use a tuple or " "std::array row for several outputs"); return std::forward(row); } } template HEDLEY_ALWAYS_INLINE auto share_at(const Key & key, const typename Key::input_type & tx, const typename Key::interior_node & node) { constexpr auto bits = utils::bitlength_of_v; constexpr auto prefix = Key::meta[I].prefix == 0 ? bits : Key::meta[I].prefix; auto lane = detail::incr::lane_input(tx, prefix, bits); auto leaf = key.template traverse_exterior(node); if constexpr (has_public_addends::value) detail::incr::absorb_public_addend_lane(key, leaf, lane); using output_type = typename Key::template concrete_output_type; return *make_eval_dpf_output(leaf, lane); } template HEDLEY_ALWAYS_INLINE void mac_row(const Key & key, const Tx & tx, Path & path, Row && row, Acc & acc, std::index_sequence) { // One exterior expansion per output per leaf/ancestor bucket. Sequential // and sorted queries reuse the node already on the path. ((acc = acc + (share_at(key, tx, path[Key::meta[Outs].tree_level]) * component(row))), ...); } template void accumulate_points(const Key & key, Point first, Point last, Rows && rows, Acc & acc) { constexpr std::size_t nout = sizeof...(Outs); constexpr std::size_t deepest = std::max({std::size_t{0}, Key::meta[Outs].tree_level...}); auto path = make_basic_path_memoizer(); std::size_t i = 0; for (auto it = first; it != last; ++it, ++i) { auto tx = key.offset_x(*it); utils::flip_msb_if_signed_integral(tx); detail::ensure_level(key, tx, path, deepest); mac_row(key, tx, path, rows[i], acc, std::make_index_sequence{}); } } template struct pack_first { static constexpr std::size_t value = I0; }; template auto accum_type_from(const Key &, Rows && rows) { using row_type = std::decay_t; using y0 = decltype(share_at::value>( std::declval(), std::declval(), std::declval())); using w0 = std::decay_t(std::declval()))>; using acc_type = decltype(std::declval() * std::declval()); return acc_type{}; } /// @brief `w[i]` when `w` is a range, otherwise `w(i)`. template HEDLEY_ALWAYS_INLINE decltype(auto) weight_at(W && w, std::size_t i) { if constexpr (std::is_invocable_v) return w(i); else return w[i]; } template struct is_column_pack : std::false_type { }; template struct is_column_pack> : std::true_type { }; template struct is_column_pack> : std::true_type { }; template auto column_accum_type() { using share_type = decltype(share_at( std::declval(), std::declval(), std::declval())); using mapped = decltype(std::declval()(std::declval())); using weight = decltype(weight_at( std::get(std::declval()), std::size_t{0})); using acc_type = decltype(std::declval() * std::declval()); return acc_type{}; } template auto accumulate_columns(const Key & key, Point first, Point last, Weights && weights, Proj && proj, Sink && sink, std::index_sequence) { static_assert(sizeof...(Ks) > 0, "columns needs at least one weight stream"); using acc_tuple = std::tuple, std::decay_t>())...>; acc_tuple acc{}; if constexpr (std::is_base_of_v::iterator_category>) { const auto n = static_cast(std::distance(first, last)); auto guard = [&](auto which) { constexpr std::size_t k = decltype(which)::value; detail_ip_check::require_weight_count(std::get(weights), n, "column weights are shorter than the point list"); }; (guard(std::integral_constant{}), ...); } constexpr std::size_t deepest = Key::meta[Out].tree_level; auto path = make_basic_path_memoizer(); std::size_t i = 0; for (auto it = first; it != last; ++it, ++i) { auto tx = key.offset_x(*it); utils::flip_msb_if_signed_integral(tx); detail::ensure_level(key, tx, path, deepest); auto share = share_at(key, tx, path[Key::meta[Out].tree_level]); sink(i, *it, share); auto y = proj(share); ((std::get(acc) = std::get(acc) + (y * weight_at(std::get(weights), i))), ...); } return acc; } } // namespace detail_ip namespace internal_paired { template void for_inclusive(Input from, Input to, Fn && fn) { constexpr auto to_int = utils::to_integral_type{}; using integral = decltype(to_int(from)); const bool wraps = utils::interval_wraps( static_cast(to_int(from)), static_cast(to_int(to)), utils::bitlength_of_v); auto step = [&](Input x) { fn(x); }; if (!wraps) { for (auto x = from;; ++x) { step(x); if (x == to) break; } return; } const auto hi = std::numeric_limits::max(); const auto lo = std::numeric_limits::min(); for (auto x = from;; ++x) { step(x); if (x == hi) break; } for (auto x = lo;; ++x) { step(x); if (x == to) break; } } template auto run_points(const Key & key, const std::vector & xs, Rows && rows) { assert_not_wildcard_output(key); if (xs.size() == 0) { using acc_type = decltype(detail_ip::accum_type_from(key, rows)); return acc_type{}; } detail_ip_check::require_weight_count(rows, xs.size(), "paired weights are shorter than the point list"); using acc_type = decltype(detail_ip::accum_type_from(key, rows)); acc_type acc{}; detail_ip::accumulate_points(key, xs.begin(), xs.end(), rows, acc); return acc; } /// @brief Clipped interval dot via the interval tree, not one path per point. /// Wrapping intervals stay on the path walk. template auto interval_scalar(const DpfKey & dpf, InputT from, InputT to, Rows && rows) { constexpr auto to_int = utils::to_integral_type{}; using integral = decltype(to_int(from)); const bool wraps = utils::interval_wraps( static_cast(to_int(from)), static_cast(to_int(to)), utils::bitlength_of_v); if (wraps) { std::vector xs; for_inclusive(from, to, [&](InputT x) { xs.push_back(x); }); return run_points(dpf, xs, std::forward(rows)); } const auto npoints = static_cast( static_cast(to_int(to)) - static_cast(to_int(from))) + std::size_t{1}; detail_ip_check::require_weight_count(rows, npoints, "paired weights are shorter than the interval"); auto buf = make_output_buffer_for_interval(dpf, from, to); auto iter = eval_interval(dpf, from, to, buf); using share_t = std::decay_t; using weight_t = std::decay_t; using acc_t = decltype(std::declval() * std::declval()); acc_t acc{}; std::size_t i = 0; for (auto it = iter.begin(); it != iter.end(); ++it, ++i) acc = acc + (*it * rows[i]); return acc; } } // namespace internal_paired /// @brief `sum_i Σ_k DPF_{out_k}(x_i) * row_i[k]` over `[from, to]`. /// @details One row of `rows` per input, in interval order (wrapping the same /// way as `eval_interval`). A row is a scalar when one output is /// selected, or a `std::tuple` / `std::array` with one component per /// output. Ancestor slots and leaf slots are read off the same path. /// Differs from the batched leaf walk: one path step per domain point /// (reuse via the path memoizer), not batched exterior AES over leaf /// nodes. For a single output the opened result matches the batched /// form when `rows` is the interval weight vector. /// \complexity One path ensure per input up to the deepest selected output, /// plus one exterior expand and multiply-add per selected output per /// input. Accumulator is O(1); no output buffer. template HEDLEY_WARN_UNUSED_RESULT auto eval_inner_product(paired_t, const DpfKey & dpf, InputT from, InputT to, Rows && rows) { using row_t = std::decay_t()[std::size_t{0}])>; constexpr bool scalar = !utils::is_tuple_v && !detail_ip::is_std_array::value; if constexpr (scalar && sizeof...(Is) == 0 && !is_multilevel_key_v) { return internal_paired::interval_scalar(dpf, from, to, std::forward(rows)); } else { std::vector xs; internal_paired::for_inclusive(from, to, [&](InputT x) { xs.push_back(x); }); return internal_paired::run_points(dpf, xs, std::forward(rows)); } } /// @brief Paired inner product over the whole input domain. template HEDLEY_WARN_UNUSED_RESULT auto eval_full_inner_product(paired_t, const DpfKey & dpf, Rows && rows) { using input_type = typename DpfKey::input_type; return eval_inner_product(paired, dpf, std::numeric_limits::min(), std::numeric_limits::max(), std::forward(rows)); } /// @brief Paired inner product over a sorted point list. /// @details Same order and sortedness rule as `eval_sequence`. Each point /// pairs with `rows[i]`. template HEDLEY_WARN_UNUSED_RESULT auto eval_sequence_inner_product(const DpfKey & dpf, ForwardIterator begin, ForwardIterator end, Rows && rows) { if (!std::is_sorted(begin, end)) throw std::runtime_error("list must be sorted"); std::vector xs(begin, end); return internal_paired::run_points(dpf, xs, std::forward(rows)); } /// @brief Paired inner product over a `sequence_recipe`'s points. /// @details `points` is the same sorted list the recipe was built from. The /// recipe drives nothing the path walk does not already share; it /// checks that the list still matches the recipe's output count. template HEDLEY_WARN_UNUSED_RESULT auto eval_sequence_inner_product(const DpfKey & dpf, const sequence_recipe & recipe, ForwardIterator begin, ForwardIterator end, Rows && rows) { const auto n = static_cast(std::distance(begin, end)); if (n != recipe.output_indices().size()) throw std::invalid_argument( "eval_sequence_inner_product: recipe and point list differ"); return eval_sequence_inner_product(dpf, begin, end, std::forward(rows)); } namespace detail_columns { template HEDLEY_WARN_UNUSED_RESULT auto run(const DpfKey & dpf, ForwardIterator begin, ForwardIterator end, Weights && weights, Proj && proj, Sink && sink, bool require_sorted) { using pack = std::decay_t; static_assert(detail_ip::is_column_pack::value, "columns weights are a std::tuple or std::array of streams; " "each stream is w[i] or w(i)"); if (require_sorted && !std::is_sorted(begin, end)) throw std::runtime_error("list must be sorted"); std::vector xs(begin, end); constexpr auto n = std::tuple_size::value; return detail_ip::accumulate_columns(dpf, xs.begin(), xs.end(), std::forward(weights), std::forward(proj), std::forward(sink), std::make_index_sequence{}); } template auto interval_dots(const DpfKey & dpf, InputT from, InputT to, Weights && weights, std::index_sequence) { constexpr auto to_int = utils::to_integral_type{}; using integral = decltype(to_int(from)); const bool wraps = utils::interval_wraps( static_cast(to_int(from)), static_cast(to_int(to)), utils::bitlength_of_v); if (wraps) { std::vector xs; internal_paired::for_inclusive(from, to, [&](InputT x) { xs.push_back(x); }); return detail_ip::accumulate_columns(dpf, xs.begin(), xs.end(), std::forward(weights), identity_project{}, noop_also{}, std::index_sequence{}); } const auto npoints = static_cast( static_cast(to_int(to)) - static_cast(to_int(from))) + std::size_t{1}; auto guard = [&](auto which) { constexpr std::size_t k = decltype(which)::value; detail_ip_check::require_weight_count(std::get(weights), npoints, "column weights are shorter than the interval"); }; (guard(std::integral_constant{}), ...); auto buf = make_output_buffer_for_interval(dpf, from, to); auto iter = eval_interval(dpf, from, to, buf); using acc_tuple = std::tuple, identity_project>())...>; acc_tuple acc{}; std::size_t i = 0; for (auto it = iter.begin(); it != iter.end(); ++it, ++i) { auto y = *it; ((std::get(acc) = std::get(acc) + (y * detail_ip::weight_at(std::get(weights), i))), ...); } return acc; } } // namespace detail_columns /// @brief Several independent dots of one output, one walk (transposed form). /// @details `weights` is a `std::tuple` or `std::array` of streams. Stream /// `k` is either `w[i]` or `w(i)`, `i` the position in the point /// list (or in the interval, wrapping the same way as /// `eval_interval`). The result is a tuple of accumulators, /// `acc_k = sum_i project(DPF(x_i)) * stream_k(i)`. /// `dpf::project(fn)` maps the share first. `dpf::also(fn)` is /// called as `fn(i, x, share)` on the unmapped share. Pass either /// tag, both, or neither. `also` then `project` is accepted too. /// Sequence points are sorted nondecreasing, same as `eval_sequence`. /// Relative to `paired`: same path walk for one output, but streams /// stay separate (no cross-stream sum). One stream equals `paired` /// on that stream. Relative to the batched leaf walk: path-per-point /// instead of batched exterior AES; same opened scalar when the /// single stream matches the interval weight layout. /// \complexity One path ensure and one exterior expand per input, plus /// O(stream count) multiply-adds per input. Accumulators are O(stream count). /// @{ template HEDLEY_WARN_UNUSED_RESULT auto eval_sequence_inner_product(columns_t, const DpfKey & dpf, ForwardIterator begin, ForwardIterator end, Weights && weights) { assert_not_wildcard_output(dpf); return detail_columns::run(dpf, begin, end, std::forward(weights), identity_project{}, noop_also{}, true); } template HEDLEY_WARN_UNUSED_RESULT auto eval_sequence_inner_product(columns_t, const DpfKey & dpf, ForwardIterator begin, ForwardIterator end, Weights && weights, project_fn proj) { assert_not_wildcard_output(dpf); return detail_columns::run(dpf, begin, end, std::forward(weights), proj.fn, noop_also{}, true); } template HEDLEY_WARN_UNUSED_RESULT auto eval_sequence_inner_product(columns_t, const DpfKey & dpf, ForwardIterator begin, ForwardIterator end, Weights && weights, also_fn sink) { assert_not_wildcard_output(dpf); return detail_columns::run(dpf, begin, end, std::forward(weights), identity_project{}, sink.fn, true); } template HEDLEY_WARN_UNUSED_RESULT auto eval_sequence_inner_product(columns_t, const DpfKey & dpf, ForwardIterator begin, ForwardIterator end, Weights && weights, project_fn proj, also_fn sink) { assert_not_wildcard_output(dpf); return detail_columns::run(dpf, begin, end, std::forward(weights), proj.fn, sink.fn, true); } template HEDLEY_WARN_UNUSED_RESULT auto eval_sequence_inner_product(columns_t, const DpfKey & dpf, ForwardIterator begin, ForwardIterator end, Weights && weights, also_fn sink, project_fn proj) { return eval_sequence_inner_product(columns, dpf, begin, end, std::forward(weights), std::move(proj), std::move(sink)); } template HEDLEY_WARN_UNUSED_RESULT auto eval_sequence_inner_product(columns_t, const DpfKey & dpf, const sequence_recipe & recipe, ForwardIterator begin, ForwardIterator end, Weights && weights, Extra && ... extra) { const auto n = static_cast(std::distance(begin, end)); if (n != recipe.output_indices().size()) throw std::invalid_argument( "eval_sequence_inner_product: recipe and point list differ"); return eval_sequence_inner_product(columns, dpf, begin, end, std::forward(weights), std::forward(extra)...); } /// \complexity Same interior expansion as `eval_interval` on `[from, to]` (Θ(L) nodes, L = leaf nodes in the interval) plus a multiply-add per output slot into an O(1) accumulator. The basic memoizer still holds O(L) nodes. template HEDLEY_WARN_UNUSED_RESULT auto eval_inner_product(columns_t, const DpfKey & dpf, InputT from, InputT to, Weights && weights, Proj && proj, Sink && sink) { assert_not_wildcard_output(dpf); if constexpr (std::is_same_v, identity_project> && std::is_same_v, noop_also> && !is_multilevel_key_v) { using pack = std::decay_t; static_assert(detail_ip::is_column_pack::value, "columns weights are a std::tuple or std::array of streams; " "each stream is w[i] or w(i)"); constexpr auto nstreams = std::tuple_size::value; return detail_columns::interval_dots(dpf, from, to, std::forward(weights), std::make_index_sequence{}); } else { std::vector xs; internal_paired::for_inclusive(from, to, [&](InputT x) { xs.push_back(x); }); return detail_columns::run(dpf, xs.begin(), xs.end(), std::forward(weights), std::forward(proj), std::forward(sink), false); } } /// \complexity Same interior expansion as `eval_interval` on `[from, to]` (Θ(L) nodes, L = leaf nodes in the interval) plus a multiply-add per output slot into an O(1) accumulator. The basic memoizer still holds O(L) nodes. template HEDLEY_WARN_UNUSED_RESULT auto eval_inner_product(columns_t, const DpfKey & dpf, InputT from, InputT to, Weights && weights) { return eval_inner_product(columns, dpf, from, to, std::forward(weights), identity_project{}, noop_also{}); } /// \complexity Same interior expansion as `eval_interval` on `[from, to]` (Θ(L) nodes, L = leaf nodes in the interval) plus a multiply-add per output slot into an O(1) accumulator. The basic memoizer still holds O(L) nodes. template HEDLEY_WARN_UNUSED_RESULT auto eval_inner_product(columns_t, const DpfKey & dpf, InputT from, InputT to, Weights && weights, project_fn proj) { return eval_inner_product(columns, dpf, from, to, std::forward(weights), proj.fn, noop_also{}); } /// \complexity Same interior expansion as `eval_interval` on `[from, to]` (Θ(L) nodes, L = leaf nodes in the interval) plus a multiply-add per output slot into an O(1) accumulator. The basic memoizer still holds O(L) nodes. template HEDLEY_WARN_UNUSED_RESULT auto eval_inner_product(columns_t, const DpfKey & dpf, InputT from, InputT to, Weights && weights, also_fn sink) { return eval_inner_product(columns, dpf, from, to, std::forward(weights), identity_project{}, sink.fn); } /// \complexity Same interior expansion as `eval_interval` on `[from, to]` (Θ(L) nodes, L = leaf nodes in the interval) plus a multiply-add per output slot into an O(1) accumulator. The basic memoizer still holds O(L) nodes. template HEDLEY_WARN_UNUSED_RESULT auto eval_inner_product(columns_t, const DpfKey & dpf, InputT from, InputT to, Weights && weights, project_fn proj, also_fn sink) { return eval_inner_product(columns, dpf, from, to, std::forward(weights), proj.fn, sink.fn); } /// \complexity Same interior expansion as `eval_interval` on `[from, to]` (Θ(L) nodes, L = leaf nodes in the interval) plus a multiply-add per output slot into an O(1) accumulator. The basic memoizer still holds O(L) nodes. template HEDLEY_WARN_UNUSED_RESULT auto eval_inner_product(columns_t, const DpfKey & dpf, InputT from, InputT to, Weights && weights, also_fn sink, project_fn proj) { return eval_inner_product(columns, dpf, from, to, std::forward(weights), proj.fn, sink.fn); } /// @brief `columns` over the whole input domain. Same streams as the interval form. template HEDLEY_WARN_UNUSED_RESULT auto eval_full_inner_product(columns_t, const DpfKey & dpf, Weights && weights, Extra && ... extra) { using input_type = typename DpfKey::input_type; return eval_inner_product(columns, dpf, std::numeric_limits::min(), std::numeric_limits::max(), std::forward(weights), std::forward(extra)...); } /// @} } // namespace dpf #endif // LIBDPF_INCLUDE_DPF_EVAL_INNER_PRODUCT_HPP__