1366 size_t max_threads)
noexcept
1373 const size_t n_input = scalars_span.size();
1375 return Curve::Group::point_at_infinity;
1384 const size_t num_threads_dispatch =
std::max<size_t>(1, std::min(n_input, hw_threads));
1385 const size_t pts_per_thread = (n_input + num_threads_dispatch - 1) / num_threads_dispatch;
1387 return trivial_msm_threaded<Curve>(scalars_span, all_points, hw_threads);
1391 BB_ASSERT_GTE(all_points.size(), scalars_span.start_index + n_input);
1392 std::span<const AffineElement> input_points(&all_points[scalars_span.start_index], n_input);
1394 constexpr size_t FULL_NUM_BITS = ScalarField::modulus.get_msb() + 1;
1397 ScalarField* scalar_ptr =
const_cast<ScalarField*
>(&scalars_span[scalars_span.start_index]);
1408 const bool external_glv_provided = !external_glv_doubled.empty();
1417 const size_t n = use_glv ? (2 * n_input) : n_input;
1418 const size_t NUM_BITS = use_glv ?
size_t{ 128 } : FULL_NUM_BITS;
1421 "working scalar indices must fit in the 29-bit schedule payload");
1423 std::span<const AffineElement> points;
1424 const bool inline_glv_double = use_glv && !external_glv_provided;
1431 const bool dedup_active = dedup_info != 0;
1434 const size_t dedup_count_estimate = dedup_info >= 2 ? dedup_info : 0;
1448 const size_t arena_total_bytes =
1449 compute_arena_bytes_for_msm<Curve>(n_input, external_glv_provided, dedup_active, hw_threads);
1450 round_parallel_detail::MsmArena arena(arena_total_bytes, external_arena);
1464 using round_parallel_detail::MSB_ZERO_SENTINEL;
1466 auto msb_per_scalar = arena.template alloc<uint8_t>(n);
1467 auto per_thread_msb_hist = arena.template alloc<std::array<uint32_t, 256>>(profile_threads);
1478 glv_scalars_storage = arena.template alloc<ScalarField>(n);
1479 if (inline_glv_double) {
1480 glv_points_storage = arena.template alloc<AffineElement>(n);
1494 const BaseField beta = inline_glv_double ? BaseField::cube_root_of_unity() : BaseField{};
1496 auto& th_hist = per_thread_msb_hist[chunk.
thread_index];
1497 for (
size_t i : chunk.
range(n_input)) {
1498 const ScalarField canonical = input_scalars[i].from_montgomery_form_reduced();
1499 const auto split = ScalarField::split_into_endomorphism_scalars(canonical);
1500 const auto& k1 = split.first;
1501 const auto& k2 = split.second;
1502 glv_scalars_storage[2 * i].data[0] = k1[0];
1503 glv_scalars_storage[2 * i].data[1] = k1[1];
1504 glv_scalars_storage[2 * i].data[2] = 0;
1505 glv_scalars_storage[2 * i].data[3] = 0;
1506 glv_scalars_storage[(2 * i) + 1].
data[0] = k2[0];
1507 glv_scalars_storage[(2 * i) + 1].
data[1] = k2[1];
1508 glv_scalars_storage[(2 * i) + 1].
data[2] = 0;
1509 glv_scalars_storage[(2 * i) + 1].
data[3] = 0;
1510 if (inline_glv_double) {
1511 glv_points_storage[2 * i] = input_points[i];
1512 glv_points_storage[(2 * i) + 1].x = input_points[i].x * beta;
1513 glv_points_storage[(2 * i) + 1].y = -input_points[i].y;
1515 round_parallel_detail::record_msb(
1516 round_parallel_detail::msb_of_2limb(k1[0], k1[1]), msb_per_scalar[2 * i], th_hist);
1517 round_parallel_detail::record_msb(
1518 round_parallel_detail::msb_of_2limb(k2[0], k2[1]), msb_per_scalar[(2 * i) + 1], th_hist);
1522 inline_glv_double ? std::span<const AffineElement>(glv_points_storage.data(), n) : external_glv_doubled;
1523 scalars = glv_scalars_storage;
1527 auto& th_hist = per_thread_msb_hist[chunk.
thread_index];
1528 for (
size_t i : chunk.
range(n_input)) {
1529 input_scalars[i].self_from_montgomery_form_reduced();
1530 round_parallel_detail::record_msb(
1531 round_parallel_detail::msb_of_4limb(input_scalars[i].
data), msb_per_scalar[i], th_hist);
1534 scalars = input_scalars;
1535 points = input_points;
1540 for (
size_t t = 0; t < profile_threads; ++t) {
1541 for (
size_t b = 0;
b < 256; ++
b) {
1542 msb_hist[
b] += per_thread_msb_hist[t][
b];
1545 const size_t n_active_early = n -
static_cast<size_t>(msb_hist[0]);
1553 const size_t threads_for_dispatch =
std::max<size_t>(1, std::min(n_active_early, hw_threads));
1554 const size_t pts_per_thread = (n_active_early + threads_for_dispatch - 1) / threads_for_dispatch;
1557 for (
size_t i : chunk.
range(n)) {
1558 scalars[i].self_to_montgomery_form();
1561 std::span<const ScalarField> scalars_const(scalars.data(), n);
1563 return trivial_msm_threaded<Curve>(ps, points, hw_threads);
1574 size_t effective_num_bits = 0;
1575 for (
size_t bin = 256; bin > 1;) {
1577 if (msb_hist[bin] != 0) {
1578 effective_num_bits = bin;
1582 if (effective_num_bits == 0 || effective_num_bits > NUM_BITS) {
1583 effective_num_bits = NUM_BITS;
1599 size_t n_for_window = n_active_early;
1604 const size_t zeros = n - n_active_early;
1605 size_t dup_est =
std::clamp(zeros, (n_active_early * 3) / 20, n_active_early / 2);
1606 if (dedup_count_estimate != 0) {
1610 dup_est = std::min(dedup_count_estimate, (n_active_early * 9) / 10);
1614 const size_t window_bits =
1616 const size_t num_buckets = (
size_t{ 1 } << (window_bits - 1)) + 1;
1632 "window schedule exceeds compile-time max window count");
1644 const size_t max_threads_for_min_batch =
std::max<size_t>(1, n / MIN_BATCH_CAPACITY);
1645 const size_t num_threads = std::min(desired_threads, max_threads_for_min_batch);
1661 const size_t B_eff = num_buckets;
1663 const size_t worker_total_for_budget = num_threads;
1665 const size_t bucket_partials_per_window_max =
1667 const size_t per_window_bytes_lo = round_parallel_detail::compute_per_window_bytes<Curve>(
1668 num_threads, B_eff, n, dense_stride_est, worker_total_for_budget);
1670 const size_t global_max_overflow_per_window_for_budget =
1673 const size_t phase_one_prologue_bytes =
1677 const size_t phase_a_cluster_members_cap = phase_a_caps.members_cap;
1678 const size_t phase_a_cluster_offsets_cap = phase_a_caps.offsets_cap;
1684 SUBCHUNK_ENTRIES_CAP,
1685 global_max_overflow_per_window_for_budget,
1687 phase_a_cluster_members_cap,
1688 phase_a_cluster_offsets_cap,
1693 const size_t fixed_overhead = (worker_union_bytes_for_budget * worker_total_for_budget) +
1694 round_parallel_detail::window_sums_storage_bytes<Curve>() +
1695 (
size_t{ 8 } * (num_threads + 1))
1696 + phase_one_prologue_bytes;
1699 const size_t available_budget =
1700 (BATCH_MEM_BUDGET > fixed_overhead) ? (BATCH_MEM_BUDGET - fixed_overhead) :
size_t{ 0 };
1701 const size_t windows_per_batch =
1712 const size_t global_max_chunk_len = (n + num_threads - 1) / num_threads;
1713 const size_t global_max_overflow_per_window =
1714 (global_max_chunk_len + SUBCHUNK_ENTRIES_CAP - 1) / SUBCHUNK_ENTRIES_CAP;
1715 const size_t chunk_capacity =
std::max(SUBCHUNK_ENTRIES_CAP, 2 * global_max_overflow_per_window);
1722 const size_t worker_total = num_threads;
1726 phase_a_scratch.resize(worker_total);
1769 const size_t bytes_P_prefix = arena.cursor;
1774 auto align_up = [](
size_t off,
size_t align) ->
size_t {
return (off + align - 1) & ~(align - 1); };
1775 auto layout_add = [&](
size_t& off,
size_t bytes,
size_t align) { off = align_up(off, align) + bytes; };
1781 global_max_overflow_per_window,
1783 phase_a_cluster_members_cap,
1784 phase_a_cluster_offsets_cap,
1794 size_t bytes_P_extra_layout = 0;
1795 layout_add(bytes_P_extra_layout, round_parallel_detail::window_sums_storage_bytes<Curve>(),
alignof(
Element));
1797 layout_add(bytes_P_extra_layout,
sizeof(uint32_t) * n,
alignof(uint32_t));
1798 layout_add(bytes_P_extra_layout,
1800 alignof(AffineElement));
1808 const size_t arena_base_misalign =
static_cast<size_t>(arena.base_addr & (WORKER_SLAB_ALIGN - 1));
1809 const size_t bytes_P_min = align_up(bytes_P_prefix,
alignof(
Element)) + bytes_P_extra_layout;
1810 const size_t bytes_P = align_up(bytes_P_min + arena_base_misalign, WORKER_SLAB_ALIGN) - arena_base_misalign;
1813 const size_t bytes_W = per_worker_bytes * worker_total;
1819 const size_t bytes_S_total = arena.capacity - bytes_P - bytes_W;
1824 size_t zone_P_cursor = bytes_P_prefix;
1825 size_t zone_S_cursor = 0;
1826 auto zone_P_alloc = [&]<
typename T>(
size_t count) ->
std::span<T> {
1827 return arena.template bump_alloc<T>(count, zone_P_cursor, bytes_P, 0);
1829 auto zone_S_alloc = [&]<
typename T>(
size_t count) ->
std::span<T> {
1830 return arena.template bump_alloc<T>(count, zone_S_cursor, bytes_S_total, bytes_P + bytes_W);
1840 for (
size_t t = 0; t < worker_total; ++t) {
1842 const size_t slab_base = t * per_worker_bytes;
1843 auto& s = thread_scratch[t];
1846 size_t ts_fixed_cur = 0;
1847 auto ts_fixed_alloc = [&]<
typename T>(
size_t count) ->
std::span<T> {
1848 return arena.template bump_alloc<T>(count, ts_fixed_cur, per_worker_union_bytes, bytes_P + slab_base);
1850 s.curr_pts = ts_fixed_alloc.template operator()<AffineElement>(chunk_capacity);
1851 s.curr_buckets = ts_fixed_alloc.template operator()<uint32_t>(chunk_capacity);
1855 using BaseParams =
typename BaseField::Params;
1857 constexpr size_t packed_runs =
1859 const size_t pack_cap = (BATCH_CAPACITY / VecField::SIZE) + 1;
1861 for (
auto& run : packed) {
1862 run = ts_fixed_alloc.template operator()<VecField>(pack_cap);
1865 s.lhs = { packed[0], packed[1] };
1866 s.rhs = { packed[2], packed[3] };
1867 s.out = { packed[0], packed[1] };
1872 s.pair_dest = ts_fixed_alloc.template operator()<uint32_t>(BATCH_CAPACITY);
1873 s.overflow_slots = ts_fixed_alloc.template operator()<uint32_t>(global_max_overflow_per_window);
1874 s.overflow_pts = ts_fixed_alloc.template operator()<AffineElement>(global_max_overflow_per_window);
1881 auto pa_alloc = [&]<
typename T>(
size_t count) ->
std::span<T> {
1882 return arena.template bump_alloc<T>(count, pa_cur, per_worker_union_bytes, bytes_P + slab_base);
1884 auto& ps = phase_a_scratch[t];
1886 ps.cluster_members = pa_alloc.template operator()<uint32_t>(phase_a_cluster_members_cap);
1887 ps.cluster_offsets = pa_alloc.template operator()<uint32_t>(phase_a_cluster_offsets_cap);
1888 ps.dirty_slots = pa_alloc.template operator()<uint16_t>(PWAL::PHASE_A_DIRTY_SLOTS_CAP);
1889 ps.bucket_rep = pa_alloc.template operator()<uint32_t>(PWAL::PHASE_A_BUCKET_REP_CAP);
1891 ps.chunk_pts = pa_alloc.template operator()<AffineElement>(PWAL::PHASE_A_CHUNK_CAP);
1892 ps.chunk_ids = pa_alloc.template operator()<uint32_t>(PWAL::PHASE_A_CHUNK_CAP);
1898 size_t ts_tail_cur = per_worker_union_bytes;
1899 auto ts_tail_alloc = [&]<
typename T>(
size_t count) ->
std::span<T> {
1900 return arena.template bump_alloc<T>(count, ts_tail_cur, per_worker_bytes, bytes_P + slab_base);
1902 const size_t dense_total = windows_per_batch * dense_stride_est;
1903 const size_t dense_pair_max = dense_total / 2;
1904 auto dense_x = ts_tail_alloc.template operator()<BaseField>(dense_total);
1905 auto dense_y = ts_tail_alloc.template operator()<BaseField>(dense_total);
1907 s.is_present = ts_tail_alloc.template operator()<uint8_t>(dense_total);
1909 s.affine_bucket_indices = ts_tail_alloc.template operator()<uint32_t>(dense_pair_max);
1910 s.affine_bucket_inversion_scratch = ts_tail_alloc.template operator()<BaseField>(dense_pair_max);
1914 s.affine_bucket_stride = dense_stride_est;
1918 const size_t schedule_total = windows_per_batch * n;
1919 auto schedule = zone_S_alloc.template operator()<uint32_t>(schedule_total);
1944 static_assert(
alignof(
Element) <= 32,
"HIST slot O layout assumes alignof(Element) <= 32");
1946 "HIST slot O layout assumes alignof(ChunkOutput) <= 32");
1948 auto align_up_local = [](
size_t off,
size_t a) ->
size_t {
return (off +
a - 1) & ~(
a - 1); };
1951 const size_t hist_h_bytes_total = (
size_t{ 4 } * windows_per_batch * num_threads * B_eff);
1955 size_t o_layout_cur = 0;
1957 const size_t off_chunk_outputs = o_layout_cur;
1959 o_layout_cur = align_up_local(o_layout_cur,
alignof(
typename Curve::Element));
1960 const size_t off_window_partial_sums = o_layout_cur;
1961 o_layout_cur +=
sizeof(
typename Curve::Element) * num_threads * windows_per_batch;
1962 const size_t hist_o_bytes_total = o_layout_cur;
1964 const size_t hist_slot_bytes_total =
std::max(hist_h_bytes_total, hist_o_bytes_total);
1970 const size_t hist_slot_cells = (hist_slot_bytes_total +
sizeof(AffineElement) - 1) /
sizeof(AffineElement);
1971 auto hist_slot_cells_span = zone_S_alloc.template operator()<AffineElement>(hist_slot_cells);
1973 std::byte*
const hist_slot_bytes =
reinterpret_cast<std::byte*
>(hist_slot_cells_span.data());
1987 auto digit_cursors =
1988 std::span<uint32_t>{
reinterpret_cast<uint32_t*
>(hist_slot_bytes), windows_per_batch * num_threads * B_eff };
2000 windows_per_batch * num_threads
2004 reinterpret_cast<typename
Curve::Element*
>(hist_slot_bytes + off_window_partial_sums),
2005 num_threads * windows_per_batch
2023 static_assert(
alignof(AffineElement) == 64,
"DENSE slot D layout assumes alignof(AffineElement) == 64");
2024 const size_t bp_total = windows_per_batch * bucket_partials_per_window_max;
2025 size_t d_layout_cur = 0;
2026 const size_t off_dense = d_layout_cur;
2027 d_layout_cur +=
sizeof(AffineElement) * bp_total;
2028 const size_t off_present = d_layout_cur;
2029 d_layout_cur +=
sizeof(uint8_t) * bp_total;
2030 const size_t dense_slot_bytes_total = d_layout_cur;
2031 const size_t dense_slot_cells = (dense_slot_bytes_total +
sizeof(AffineElement) - 1) /
sizeof(AffineElement);
2034 auto dense_slot_cells_span = zone_S_alloc.template operator()<AffineElement>(dense_slot_cells);
2036 std::byte*
const dense_slot_bytes =
reinterpret_cast<std::byte*
>(dense_slot_cells_span.data());
2039 auto bucket_partials_dense =
2042 auto bucket_partials_present =
2043 std::span<uint8_t>{
reinterpret_cast<uint8_t*
>(dense_slot_bytes + off_present), bp_total };
2046 auto bucket_start_all = zone_S_alloc.template operator()<
size_t>(windows_per_batch * (B_eff + 1));
2047 auto chunk_start_all = zone_S_alloc.template operator()<
size_t>(windows_per_batch * (num_threads + 1));
2055 auto chunk_bucket_lo_all = zone_S_alloc.template operator()<
size_t>(windows_per_batch * (num_threads + 1));
2056 auto chunk_bucket_hi_all = zone_S_alloc.template operator()<
size_t>(windows_per_batch * num_threads);
2061 auto bucket_partials_offsets = zone_S_alloc.template operator()<
size_t>((num_threads * windows_per_batch) + 1);
2068 auto rebalanced_bucket_lo_partition = zone_S_alloc.template operator()<
size_t>(num_threads + 1);
2069 auto orig_thread_lo = zone_S_alloc.template operator()<
size_t>(windows_per_batch * num_threads);
2070 auto orig_thread_hi = zone_S_alloc.template operator()<
size_t>(windows_per_batch * num_threads);
2073 auto window_sums = zone_P_alloc.template operator()<
typename Curve::Element>(WINDOW_SUMS_CAP);
2074 std::fill_n(window_sums.begin(), WINDOW_SUMS_CAP, Curve::Group::point_at_infinity);
2082 dedup_state.
redirect_lookup = zone_P_alloc.template operator()<uint32_t>(n);
2088 for (
size_t i : chunk.
range(n)) {
2096 constexpr uint32_t BUCKET_MASK = (uint32_t{ 1 } << 31) - 1;
2104 bool phase_a_done =
false;
2106 auto run_batch = [&](
size_t batch_start,
size_t windows_in_batch,
size_t B_R)
noexcept {
2109 const size_t bucket_stride = B_eff;
2113 constexpr size_t SCALAR_UINT64_LIMBS =
sizeof(ScalarField) /
sizeof(uint64_t);
2121 constexpr size_t SCALAR_U32_LIMBS =
sizeof(ScalarField) /
sizeof(uint32_t);
2122 for (
size_t w = 0; w < windows_in_batch; ++w) {
2123 const size_t global_w = batch_start + w;
2124 const size_t window_bits_w = sched.window_bits_per_window[global_w];
2125 per_window_bits[w] =
static_cast<uint8_t
>(window_bits_w);
2127 sched.bit_base[global_w], window_bits_w, SCALAR_UINT64_LIMBS);
2129 sched.bit_base[global_w], window_bits_w, SCALAR_U32_LIMBS);
2131 const uint32_t lo_mask = slice_params_u32[w].lo_mask;
2132 const uint32_t hi_mask = slice_params_u32[w].hi_mask;
2133 const uint32_t val_mask = (uint32_t{ 1 } <<
static_cast<uint32_t
>(window_bits_w)) - 1;
2139 constexpr size_t SIMD_BATCH = 64;
2140 static_assert(SIMD_BATCH % 4 == 0,
"SIMD_BATCH must be divisible by 4");
2141 constexpr size_t LIMBS_PER_SCALAR =
sizeof(ScalarField) /
sizeof(uint32_t);
2142 const auto* scalars_u32 =
reinterpret_cast<const uint32_t*
>(scalars.data());
2144 auto fill_packed_digit_buffer = [&](
size_t w,
size_t i, uint32_t* packed_buf)
noexcept {
2145 const auto& sp32 = slice_params_u32[w];
2146 const uint32_t window_bits_w =
static_cast<uint32_t
>(per_window_bits[w]);
2148 for (
size_t k = 0; k < SIMD_BATCH; k += 4) {
2151 scalars_u32 + ((i + k + 0) * LIMBS_PER_SCALAR),
2152 scalars_u32 + ((i + k + 1) * LIMBS_PER_SCALAR),
2153 scalars_u32 + ((i + k + 2) * LIMBS_PER_SCALAR),
2154 scalars_u32 + ((i + k + 3) * LIMBS_PER_SCALAR),
2159 val_mask_vectors[w],
2163 for (
size_t k = 0; k < SIMD_BATCH; k += 4) {
2166 scalars_u32 + ((i + k + 0) * LIMBS_PER_SCALAR),
2167 scalars_u32 + ((i + k + 1) * LIMBS_PER_SCALAR),
2168 scalars_u32 + ((i + k + 2) * LIMBS_PER_SCALAR),
2169 scalars_u32 + ((i + k + 3) * LIMBS_PER_SCALAR),
2174 val_mask_vectors[w],
2178 for (
size_t k = 0; k < SIMD_BATCH; k += 4) {
2181 scalars_u32 + ((i + k + 0) * LIMBS_PER_SCALAR),
2182 scalars_u32 + ((i + k + 1) * LIMBS_PER_SCALAR),
2183 scalars_u32 + ((i + k + 2) * LIMBS_PER_SCALAR),
2184 scalars_u32 + ((i + k + 3) * LIMBS_PER_SCALAR),
2192 val_mask_vectors[w],
2201 const bool phase_a_done_at_batch_start = phase_a_done;
2202 const bool dedup_known_for_batch =
2203 dedup_active && phase_a_done_at_batch_start && dedup_state.
n_dedup_extras != 0;
2208 auto stage1_digit_extract = [&]<
bool DedupKnown>(
size_t tid)
noexcept {
2209 [[maybe_unused]]
const uint32_t*
const rl_data = dedup_state.
redirect_lookup.data();
2210 for (
size_t w = 0; w < windows_in_batch; ++w) {
2211 uint32_t* my_counts = digit_cursors.data() + (((w * num_threads) + tid) * bucket_stride);
2212 std::memset(my_counts, 0, B_R *
sizeof(uint32_t));
2214 const size_t start = tid * n / num_threads;
2215 const size_t end = (tid + 1) * n / num_threads;
2221 auto compute_include_mask = [&](
size_t block_start)
noexcept -> uint64_t {
2222 uint64_t include_mask = 0;
2223 for (
size_t k = 0; k < SIMD_BATCH; ++k) {
2224 const size_t scalar_idx = block_start + k;
2225 const uint8_t m = msb_per_scalar[scalar_idx];
2226 bool include = (m != MSB_ZERO_SENTINEL);
2227 if constexpr (DedupKnown) {
2229 const uint32_t patch = rl_data[scalar_idx];
2234 include_mask |=
static_cast<uint64_t
>(include) << k;
2236 return include_mask;
2240 while (i + SIMD_BATCH <= end) {
2241 const uint64_t include_mask = compute_include_mask(i);
2242 if (include_mask == 0) {
2246 const bool all_included = include_mask == ~uint64_t{ 0 };
2247 for (
size_t w = 0; w < windows_in_batch; ++w) {
2248 fill_packed_digit_buffer(w, i, packed_buf.data());
2249 uint32_t* my_counts = digit_cursors.data() + (((w * num_threads) + tid) * bucket_stride);
2251 for (
size_t k = 0; k < SIMD_BATCH; ++k) {
2252 ++my_counts[packed_buf[k] & BUCKET_MASK];
2255 uint64_t scatter_mask = include_mask;
2256 for (
size_t k = 0; k < SIMD_BATCH; ++k) {
2257 if ((scatter_mask & uint64_t{ 1 }) != 0) {
2258 ++my_counts[packed_buf[k] & BUCKET_MASK];
2269 for (; i < end; ++i) {
2270 const uint8_t m = msb_per_scalar[i];
2271 if (m == MSB_ZERO_SENTINEL) {
2274 if constexpr (DedupKnown) {
2275 const uint32_t patch = rl_data[i];
2281 for (
size_t w = 0; w < windows_in_batch; ++w) {
2282 uint32_t* my_counts = digit_cursors.data() + (((w * num_threads) + tid) * bucket_stride);
2284 const uint32_t window_bits_w =
static_cast<uint32_t
>(per_window_bits[w]);
2285 const uint32_t packed =
2295 ++my_counts[packed & BUCKET_MASK];
2301 if (dedup_known_for_batch) {
2303 num_threads, [&](
size_t tid) { stage1_digit_extract.template operator()<
true>(tid); });
2306 num_threads, [&](
size_t tid) { stage1_digit_extract.template operator()<
false>(tid); });
2323 const size_t d_start = tid * B_R / num_threads;
2324 const size_t d_end = (tid + 1) * B_R / num_threads;
2325 for (
size_t w = 0; w < windows_in_batch; ++w) {
2326 size_t*
const bucket_start_w = bucket_start_all.data() + (w * (bucket_stride + 1));
2327 for (
size_t d = d_start; d < d_end; ++d) {
2331 uint32_t running = 0;
2332 for (
size_t t = 0; t < num_threads; ++t) {
2333 const size_t k = (((w * num_threads) + t) * bucket_stride) + d;
2334 const uint32_t cnt = digit_cursors[k];
2335 digit_cursors[k] = running;
2338 bucket_start_w[d + 1] = running;
2349 auto build_bucket_offsets_for_window = [&](
size_t w)
noexcept {
2350 size_t* bucket_start = bucket_start_all.data() + (w * (bucket_stride + 1));
2351 bucket_start[0] = 0;
2352 bucket_start[1] = 0;
2353 for (
size_t d = 1; d < B_R; ++d) {
2354 bucket_start[d + 1] += bucket_start[d];
2357 const size_t offset_threads = std::min(num_threads, windows_in_batch);
2358 if (offset_threads <= 1) {
2359 for (
size_t w = 0; w < windows_in_batch; ++w) {
2360 build_bucket_offsets_for_window(w);
2364 for (
size_t w = tid; w < windows_in_batch; w += offset_threads) {
2365 build_bucket_offsets_for_window(w);
2388 auto stage4_emit = [&]<
bool DedupKnown>(
size_t tid)
noexcept {
2389 [[maybe_unused]]
const uint32_t*
const rl_data = dedup_state.
redirect_lookup.data();
2390 const size_t start = tid * n / num_threads;
2391 const size_t end = (tid + 1) * n / num_threads;
2395 for (
size_t w = 0; w < windows_in_batch; ++w) {
2396 cursors[w] = digit_cursors.data() + (((w * num_threads) + tid) * bucket_stride);
2397 bucket_starts[w] = bucket_start_all.data() + (w * (bucket_stride + 1));
2398 schedules[w] = schedule.data() + (w * n);
2402 constexpr size_t STAGE4_SCALAR_TILE = 2048;
2406 for (
size_t tile_start = start; tile_start < end; tile_start += STAGE4_SCALAR_TILE) {
2407 const size_t tile_end = std::min(end, tile_start + STAGE4_SCALAR_TILE);
2408 const size_t tile_len = tile_end - tile_start;
2409 for (
size_t j = 0; j < tile_len; ++j) {
2410 const size_t scalar_idx = tile_start + j;
2411 const uint8_t m = msb_per_scalar[scalar_idx];
2412 bool include = (m != MSB_ZERO_SENTINEL);
2413 if constexpr (DedupKnown) {
2414 uint32_t out_base =
static_cast<uint32_t
>(scalar_idx);
2416 const uint32_t patch = rl_data[scalar_idx];
2422 out_base_tile[j] = out_base;
2424 active_tile[j] =
static_cast<uint8_t
>(include);
2427 for (
size_t w = 0; w < windows_in_batch; ++w) {
2428 uint32_t* my_cursor = cursors[w];
2429 const size_t* bucket_start = bucket_starts[w];
2430 uint32_t* sched_w = schedules[w];
2431 size_t i = tile_start;
2432 while (i + SIMD_BATCH <= tile_end) {
2433 const size_t rel = i - tile_start;
2434 uint64_t include_mask = 0;
2435 for (
size_t k = 0; k < SIMD_BATCH; ++k) {
2436 include_mask |=
static_cast<uint64_t
>(active_tile[rel + k]) << k;
2438 if (include_mask == 0) {
2442 fill_packed_digit_buffer(w, i, packed_buf.data());
2443 uint64_t scatter_mask = include_mask;
2444 for (
size_t k = 0; k < SIMD_BATCH; ++k) {
2445 if ((scatter_mask & uint64_t{ 1 }) != 0) {
2446 const uint32_t packed = packed_buf[k];
2447 const uint32_t bucket_idx = packed & BUCKET_MASK;
2448 if (bucket_idx != 0) {
2449 const uint32_t idx =
2450 static_cast<uint32_t
>(bucket_start[bucket_idx]) + my_cursor[bucket_idx]++;
2452 if constexpr (DedupKnown) {
2453 out |= out_base_tile[rel + k];
2455 out |=
static_cast<uint32_t
>(i + k);
2464 for (; i < tile_end; ++i) {
2465 const size_t rel = i - tile_start;
2466 if (active_tile[rel] == 0) {
2479 static_cast<uint32_t
>(per_window_bits[w]));
2480 const uint32_t bucket_idx = packed & BUCKET_MASK;
2481 if (bucket_idx != 0) {
2482 const uint32_t idx =
2483 static_cast<uint32_t
>(bucket_start[bucket_idx]) + my_cursor[bucket_idx]++;
2485 if constexpr (DedupKnown) {
2486 out |= out_base_tile[rel];
2488 out |=
static_cast<uint32_t
>(i);
2499 if (dedup_known_for_batch) {
2501 num_threads, [&](
size_t tid) { stage4_emit.template operator()<
true>(tid); });
2504 num_threads, [&](
size_t tid) { stage4_emit.template operator()<
false>(tid); });
2520 if (dedup_active && windows_in_batch > 0 && !phase_a_done) {
2522 uint32_t* sched_w0 = schedule.data();
2532 const uint32_t cids_per_thread =
2544 const size_t*
const w0_bucket_start = bucket_start_all.data();
2545 std::atomic<size_t> dedup_cluster_count{ 0 };
2547 const size_t b_lo = 1 + ((tid * (B_R - 1)) / num_threads);
2548 const size_t b_hi = 1 + (((tid + 1) * (B_R - 1)) / num_threads);
2549 const uint32_t cid_lo =
static_cast<uint32_t
>(tid) * cids_per_thread;
2550 const uint32_t cid_max = cid_lo + cids_per_thread;
2551 const size_t local_clusters = round_parallel_detail::dedup_phase_a_worker_hash<Curve>(
2556 std::span<const ScalarField>(scalars.data(), n),
2560 msb_per_scalar.data(),
2564 phase_a_scratch[tid]);
2565 if (local_clusters != 0) {
2571 phase_a_done =
true;
2581 auto partition_chunks_for_window = [&](
size_t w)
noexcept {
2582 const size_t* bucket_start = bucket_start_all.data() + (w * (bucket_stride + 1));
2583 const size_t*
const bucket_start_end = bucket_start + B_R + 1;
2584 size_t* chunk_start = chunk_start_all.data() + (w * (num_threads + 1));
2585 size_t* chunk_bucket_lo = chunk_bucket_lo_all.data() + (w * (num_threads + 1));
2586 size_t* chunk_bucket_hi = chunk_bucket_hi_all.data() + (w * num_threads);
2587 const size_t m = bucket_start[B_R];
2588 const size_t* search_begin = bucket_start + 1;
2590 chunk_start[0] = lo;
2591 for (
size_t t = 0; t < num_threads; ++t) {
2592 const size_t hi = ((t + 1) == num_threads) ? m : (((t + 1) * m) / num_threads);
2593 chunk_start[t + 1] = hi;
2595 const size_t*
const lo_it =
std::upper_bound(search_begin, bucket_start_end, lo);
2596 const size_t lo_bucket =
static_cast<size_t>(lo_it - bucket_start - 1);
2597 const size_t*
const hi_it =
std::upper_bound(lo_it, bucket_start_end, hi - 1);
2598 const size_t hi_bucket =
static_cast<size_t>(hi_it - bucket_start - 1);
2599 chunk_bucket_lo[t] = lo_bucket;
2600 chunk_bucket_hi[t] = hi_bucket;
2601 search_begin = hi_it;
2603 chunk_bucket_lo[t] = B_R;
2604 chunk_bucket_hi[t] = 0;
2608 chunk_bucket_lo[num_threads] = B_R;
2611 bool chunk_partition_done =
false;
2612 if (dedup_active && windows_in_batch > 0 && phase_a_done && !phase_a_done_at_batch_start) {
2615 const size_t bs_stride = bucket_stride + 1;
2616 const size_t br = B_R;
2617 const size_t cap_R = n;
2619 num_threads, [&, rl_data, bs_stride, br, cap_R](
size_t tid)
noexcept {
2620 for (
size_t w = tid; w < windows_in_batch; w += num_threads) {
2621 uint32_t* sched_w = schedule.data() + (w * cap_R);
2622 size_t* bucket_start_w = bucket_start_all.data() + (w * bs_stride);
2623 round_parallel_detail::dedup_patch_schedule_window<Curve>(sched_w, bucket_start_w, br, rl_data);
2624 partition_chunks_for_window(w);
2627 chunk_partition_done =
true;
2636 if (!chunk_partition_done) {
2637 for (
size_t w = 0; w < windows_in_batch; ++w) {
2638 partition_chunks_for_window(w);
2654 auto next_pow2 = [](
size_t x) ->
size_t {
2664 size_t max_chunk_len = 0;
2665 for (
size_t t = 0; t < num_threads; ++t) {
2666 for (
size_t w = 0; w < windows_in_batch; ++w) {
2667 const size_t* chunk_start = chunk_start_all.data() + (w * (num_threads + 1));
2668 const size_t entries_in_chunk = chunk_start[t + 1] - chunk_start[t];
2669 if (entries_in_chunk == 0) {
2672 max_chunk_len =
std::max(max_chunk_len, entries_in_chunk);
2684 size_t global_stride = 0;
2689 const size_t active_digits = (B_R > 0) ? (B_R - 1) : 0;
2690 for (
size_t t = 0; t <= num_threads; ++t) {
2691 rebalanced_bucket_lo_partition[t] = 1 + (t * active_digits) / num_threads;
2693 rebalanced_bucket_lo_partition[num_threads] = B_R;
2694 size_t max_buckets_per_task = 0;
2695 for (
size_t t = 0; t + 1 <= num_threads; ++t) {
2696 const size_t hi_d = (t + 1 == num_threads) ? (B_R - 1) : (rebalanced_bucket_lo_partition[t + 1] - 1);
2697 const size_t lo_d = rebalanced_bucket_lo_partition[t];
2699 max_buckets_per_task =
std::max(max_buckets_per_task, hi_d - lo_d + 1);
2702 global_stride = next_pow2(max_buckets_per_task);
2707 for (
size_t w = 0; w < windows_in_batch; ++w) {
2708 const size_t* chunk_bucket_lo = chunk_bucket_lo_all.data() + (w * (num_threads + 1));
2709 const size_t* chunk_bucket_hi = chunk_bucket_hi_all.data() + (w * num_threads);
2710 const size_t* chunk_start_w = chunk_start_all.data() + (w * (num_threads + 1));
2711 for (
size_t tprime = 0; tprime < num_threads; ++tprime) {
2712 const size_t lo_d = rebalanced_bucket_lo_partition[tprime];
2714 (tprime + 1 == num_threads) ? (B_R - 1) : (rebalanced_bucket_lo_partition[tprime + 1] - 1);
2715 size_t lo_orig = num_threads;
2717 for (
size_t t = 0; t < num_threads; ++t) {
2718 const size_t entries = chunk_start_w[t + 1] - chunk_start_w[t];
2722 const size_t cl = chunk_bucket_lo[t];
2723 const size_t ch = chunk_bucket_hi[t];
2724 if (ch < lo_d || cl > hi_d) {
2727 if (lo_orig == num_threads) {
2732 orig_thread_lo[(w * num_threads) + tprime] = lo_orig;
2733 orig_thread_hi[(w * num_threads) + tprime] = hi_orig;
2741 size_t bucket_partials_cursor = 0;
2742 for (
size_t t = 0; t < num_threads; ++t) {
2743 for (
size_t w = 0; w < windows_in_batch; ++w) {
2744 bucket_partials_offsets[(t * windows_in_batch) + w] = bucket_partials_cursor;
2745 const size_t* chunk_bucket_lo_w = chunk_bucket_lo_all.data() + (w * (num_threads + 1));
2746 const size_t* chunk_bucket_hi_w = chunk_bucket_hi_all.data() + (w * num_threads);
2747 const size_t* chunk_start_w = chunk_start_all.data() + (w * (num_threads + 1));
2748 const size_t entries = chunk_start_w[t + 1] - chunk_start_w[t];
2750 bucket_partials_cursor += chunk_bucket_hi_w[t] - chunk_bucket_lo_w[t] + 1;
2754 bucket_partials_offsets[num_threads * windows_in_batch] = bucket_partials_cursor;
2755 const size_t bucket_partials_total = bucket_partials_cursor;
2756 BB_ASSERT_LTE(bucket_partials_total, bucket_partials_dense.size());
2757 std::memset(bucket_partials_present.data(), 0, bucket_partials_total);
2762 for (
size_t t = 0; t < worker_total; ++t) {
2763 thread_scratch[t].affine_bucket_stride = global_stride;
2772 auto bucket_partials_per_thread_lambda = [&](
size_t tid) {
2773 auto& s = thread_scratch[tid];
2774 for (
size_t w = 0; w < windows_in_batch; ++w) {
2775 const size_t* chunk_start_w = chunk_start_all.data() + (w * (num_threads + 1));
2776 const size_t cs_lo = chunk_start_w[tid];
2777 const size_t cs_hi = chunk_start_w[tid + 1];
2778 if (cs_lo == cs_hi) {
2781 const uint32_t* sched_w = schedule.data() + (w * n);
2782 const size_t* bucket_start = bucket_start_all.data() + (w * (bucket_stride + 1));
2783 AffineElement* dst_dense =
2784 bucket_partials_dense.data() + bucket_partials_offsets[(tid * windows_in_batch) + w];
2785 uint8_t* dst_present =
2786 bucket_partials_present.data() + bucket_partials_offsets[(tid * windows_in_batch) + w];
2787 const size_t* chunk_bucket_lo = chunk_bucket_lo_all.data() + (w * (num_threads + 1));
2788 const uint32_t my_lo =
static_cast<uint32_t
>(chunk_bucket_lo[tid]);
2789 const size_t my_hi = chunk_bucket_hi_all[(w * num_threads) + tid];
2790 size_t bucket_cursor = my_lo;
2792 for (
size_t pos = cs_lo; pos < cs_hi;) {
2793 const size_t end = std::min(pos + SUBCHUNK_ENTRIES_CAP, cs_hi);
2794 reduce_chunk<Curve>(s,
2802 std::span<const AffineElement>(dedup_state.
extra_points));
2803 const size_t len = s.result_len;
2804 for (
size_t k = 0; k <
len; ++k) {
2805 const uint32_t d = s.curr_buckets[k];
2806 const size_t slot = d - my_lo;
2807 if (dst_present[
slot]) {
2808 s.overflow_slots[s.overflow_len] =
static_cast<uint32_t
>(
slot);
2809 s.overflow_pts[s.overflow_len] = s.curr_pts[k];
2812 dst_dense[
slot] = s.curr_pts[k];
2813 dst_present[
slot] = 1;
2818 merge_overflow<Curve>(s, dst_dense);
2830 auto bucket_reduce_cross_thread_lambda = [&](
size_t tprime) {
2831 auto& s = thread_scratch[tprime];
2832 Element* my_partials = window_partial_sums.data() + (tprime * windows_per_batch);
2833 for (
size_t w = 0; w < windows_in_batch; ++w) {
2834 my_partials[w] = Curve::Group::point_at_infinity;
2837 const size_t stride = s.affine_bucket_stride;
2838 std::memset(s.is_present.data(), 0, windows_in_batch * stride);
2840 const size_t lo_d = rebalanced_bucket_lo_partition[tprime];
2842 (tprime + 1 == num_threads) ? (B_R - 1) : (rebalanced_bucket_lo_partition[tprime + 1] - 1);
2843 const uint32_t lo_d_u =
static_cast<uint32_t
>(lo_d);
2844 const uint32_t hi_d_u =
static_cast<uint32_t
>(hi_d);
2846 bool any_nonempty =
false;
2847 for (
size_t w = 0; w < windows_in_batch; ++w) {
2848 auto&
info = s.chunk_infos[w];
2849 auto&
out = chunk_outputs[(w * num_threads) + tprime];
2854 info.buckets_padded = 0;
2858 const size_t orig_lo = orig_thread_lo[(w * num_threads) + tprime];
2859 const size_t orig_hi = orig_thread_hi[(w * num_threads) + tprime];
2860 if (orig_lo == num_threads) {
2864 info.buckets_padded = 0;
2868 const size_t base = w * stride;
2869 bool has_data =
false;
2876 const size_t* chunk_bucket_lo_w = chunk_bucket_lo_all.data() + (w * (num_threads + 1));
2877 const size_t* chunk_bucket_hi_w = chunk_bucket_hi_all.data() + (w * num_threads);
2878 for (
size_t t = orig_lo; t <= orig_hi; ++t) {
2879 const size_t cl = chunk_bucket_lo_w[t];
2880 const size_t ch = chunk_bucket_hi_w[t];
2883 if (d_lo_clip > d_hi_clip) {
2886 const AffineElement* src_dense =
2887 bucket_partials_dense.data() + bucket_partials_offsets[(t * windows_in_batch) + w];
2888 const uint8_t* src_present =
2889 bucket_partials_present.data() + bucket_partials_offsets[(t * windows_in_batch) + w];
2890 for (
size_t d = d_lo_clip; d <= d_hi_clip; ++d) {
2891 const size_t src_slot = d - cl;
2892 if (src_present[src_slot] == 0) {
2895 const size_t dst_slot = base + (d - lo_d);
2896 if (s.is_present[dst_slot] == 0) {
2898 s.dense_buckets.x[dst_slot] = src_dense[src_slot].x;
2899 s.dense_buckets.y[dst_slot] = src_dense[src_slot].y;
2900 s.is_present[dst_slot] = 1;
2907 Element(AffineElement(s.dense_buckets.x[dst_slot], s.dense_buckets.y[dst_slot]));
2908 acc +=
Element(src_dense[src_slot]);
2909 const AffineElement merged(acc);
2910 s.dense_buckets.x[dst_slot] = merged.x;
2911 s.dense_buckets.y[dst_slot] = merged.y;
2920 info.buckets_padded = 0;
2924 any_nonempty =
true;
2925 const size_t M = hi_d - lo_d + 1;
2926 const uint32_t buckets_padded =
2927 (M == 1) ? 1 : (uint32_t{ 1 } << (32 - __builtin_clz(
static_cast<uint32_t
>(M - 1))));
2931 info.buckets_padded = buckets_padded;
2937 if (!any_nonempty) {
2941 round_parallel_detail::recursive_affine_bucket_reduce_strided<Curve>(s,
2942 s.chunk_infos.data(),
2944 chunk_outputs.data() + tprime,
2949 for (
size_t w = 0; w < windows_in_batch; ++w) {
2950 auto&
out = chunk_outputs[(w * num_threads) + tprime];
2951 if (
out.empty == 0) {
2952 my_partials[w] = round_parallel_detail::chunk_contribution<Curve>(
out);
2972 const size_t reduce_threads = std::min(num_threads, windows_in_batch);
2974 const size_t lo = rid * windows_in_batch / reduce_threads;
2975 const size_t hi = (rid + 1) * windows_in_batch / reduce_threads;
2976 for (
size_t w = lo; w < hi; ++w) {
2977 Element sum = Curve::Group::point_at_infinity;
2978 for (
size_t tid = 0; tid < num_threads; ++tid) {
2979 sum += window_partial_sums[(tid * windows_per_batch) + w];
2981 window_sums[batch_start + w] =
sum;
2988 const size_t B_R = (
size_t{ 1 } << (window_bits - 1)) + 1;
2989 for (
size_t batch_start = 0; batch_start < sched.num_windows; batch_start += windows_per_batch) {
2990 const size_t windows_in_batch = std::min(windows_per_batch, sched.num_windows - batch_start);
2991 run_batch(batch_start, windows_in_batch, B_R);
2997 Element result = (sched.num_windows == 0) ? Curve::Group::point_at_infinity : window_sums[sched.num_windows - 1];
2998 for (
size_t w_rev = sched.num_windows - 1; w_rev > 0; --w_rev) {
2999 const size_t window_bits_w = sched.window_bits_per_window[w_rev - 1];
3000 for (
size_t d = 0; d < window_bits_w; ++d) {
3003 result += window_sums[w_rev - 1];
3010 for (
size_t i : chunk.
range(n_input)) {
3011 input_scalars[i].self_to_montgomery_form();