Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
pippenger_arena_layout.hpp
Go to the documentation of this file.
1// Per-worker arena layout for the round-parallel Pippenger MSM (Zone W slab).
2//
3// Canonical source of truth for the per-worker byte walk that was previously
4// duplicated across `compute_arena_bytes_for_msm`, the live allocator inside
5// `pippenger_round_parallel`, and `pippenger_bn254_arena_layout_fits_for_test`.
6// The historical arena drift bugs (cluster_offsets miscount, wasm
7// aligned_local overflow, NO_GLV abort, t1 abort) all traced to disagreements
8// between those copies; this struct removes that class by computing the layout
9// once.
10//
11// The constructor's layout walk mirrors the live allocator's `layout_add`
12// sequence exactly, including alignment slop. The sizer's previous
13// arithmetic-only formula did not honour per-allocation alignment, so it
14// systematically under-counted by a few bytes per slab; the struct fixes that
15// by construction.
16//
17// Phase A and Stage 6 fields overlay the same per-worker bytes because the
18// parallel_for invocations are disjoint (Phase A runs on the first window
19// batch, Stage 6 runs per batch thereafter, and never on the same worker
20// concurrently). `per_worker_union_bytes = max(ts_fixed, pa_layout)`.
21
22#pragma once
23
26
27#include <algorithm>
28#include <array>
29#include <bit>
30#include <cstddef>
31#include <cstdint>
32#include <utility>
33
35
36// ============================================================================
37// Round-parallel internals exposed to the test suite.
38//
39// `pippenger_bn254_arena_layout_fits_for_test` is a TU-local helper that walks
40// the actual Zone P / Zone W / Zone S allocator for representative inputs and
41// asserts the result fits in `compute_arena_bytes_for_msm`'s promise. Its body
42// lives in `scalar_multiplication.test.cpp`, which means the helpers it needs
43// (`choose_window_bits`, `build_window_schedule`, `ChunkOutput`,
44// `DEDUP_MAX_*`, `MAX_SCHEDULE_WINDOWS`, `compute_arena_bytes_for_msm`) need
45// header-visible declarations.
46// ============================================================================
47
48// Per-window count cap shared by `WindowSchedule` arrays and the live
49// allocator's `window_sums_storage` slot.
50inline constexpr size_t MAX_SCHEDULE_WINDOWS = 128;
51
52// Dedup pre-pass caps. DEDUP_MAX_CLUSTERS bounds `extra_points` at ≤ 1 MB;
53// DEDUP_MAX_MEMBERS bounds the per-worker `cluster_members` slab.
54inline constexpr size_t DEDUP_MAX_CLUSTERS = 16384;
55inline constexpr size_t DEDUP_MAX_MEMBERS = 32768;
56
57// Uniform window schedule produced by `build_window_schedule`. Holds the per-window `c` value
58// (`window_bits_per_window`) and its bit offset (`bit_base`) for downstream sizing/dispatch. The
59// per-window bucket count is not stored: the schedule is uniform, so the widest window's bucket
60// count is always `(1 << (window_bits - 1)) + 1`, computed directly where needed.
62 size_t num_windows = 0;
63 std::array<uint8_t, MAX_SCHEDULE_WINDOWS> window_bits_per_window{}; // window_bits_w for each w
64 std::array<uint16_t, MAX_SCHEDULE_WINDOWS> bit_base{}; // B_w = Σ_{k<w} c_k, B_0 = 0
65};
66
67// Bytes of the per-MSM `window_sums` accumulator slot (Stage 7): one group Element per schedule
68// window. Single source of truth for the arena sizer's `fixed_overhead`, the live allocator's
69// `fixed_overhead`, and the canonical Zone P layout walk, so the three cannot drift.
70template <typename Curve> [[nodiscard]] constexpr size_t window_sums_storage_bytes() noexcept
71{
72 return sizeof(typename Curve::Element) * MAX_SCHEDULE_WINDOWS;
73}
74
75// Per-chunk recursive-affine bucket-reduce output (Stage 6b output cell).
76template <typename Curve> struct ChunkOutput {
77 typename Curve::Element R{};
78 typename Curve::Element L{};
79 uint32_t lo = 0;
80 uint32_t hi = 0;
81 uint8_t empty = 1;
82};
83
84// Pick the optimal window size `c` for the MSM schedule.
85[[nodiscard]] inline uint32_t choose_window_bits(size_t num_points,
86 size_t num_bits,
87 size_t n_input,
88 size_t num_logical_threads) noexcept
89{
90 constexpr uint32_t MAX_C = 20;
91 uint32_t best = 2;
92
93 static_cast<void>(n_input);
94 static_cast<void>(num_logical_threads);
95 // Choose c minimizing the modeled cost rounds * (A*n + B*buckets); B/A = 3 weights a bucket as
96 // ~3x a point (empirically calibrated).
97 static constexpr uint64_t BAC_A = 4;
98 static constexpr uint64_t BAC_B = 12;
99 uint64_t best_cost = static_cast<uint64_t>(-1);
100 for (uint32_t window_bits = 2; window_bits < MAX_C; ++window_bits) {
101 const uint64_t rounds = (num_bits + 2 + window_bits - 1) / window_bits;
102 const uint64_t buckets = (uint64_t{ 1 } << (window_bits - 1)) + 1;
103 const uint64_t n = num_points;
104 const uint64_t cost = rounds * ((BAC_A * n) + (BAC_B * buckets));
105 if (cost < best_cost) {
106 best_cost = cost;
107 best = window_bits;
108 }
109 }
110
111 return best;
112}
113
114// Build a uniform window schedule for the given bit budget and chosen `c`. Every window
115// is `window_bits` wide except the final one, which takes the remaining bits. The +2 on
116// the bit budget accommodates the carry-less top bit of the Constantine recoder.
117inline WindowSchedule build_window_schedule(size_t num_bits, size_t window_bits) noexcept
118{
119 WindowSchedule sched{};
120
121 size_t bits_remaining = num_bits + 2;
122 size_t bit_offset = 0;
123 size_t w = 0;
124 while (bits_remaining > 0 && w < MAX_SCHEDULE_WINDOWS) {
125 const size_t window_bits_w = std::min<size_t>(window_bits, bits_remaining);
126 sched.bit_base[w] = static_cast<uint16_t>(bit_offset);
127 sched.window_bits_per_window[w] = static_cast<uint8_t>(window_bits_w);
128 bit_offset += window_bits_w;
129 bits_remaining -= window_bits_w;
130 ++w;
131 }
132 sched.num_windows = w;
133 return sched;
134}
135
136// Maximum number of independent additions batched per modular inversion in the
137// affine-arithmetic group ops (used by Stage 6a/6b). Sizes per-worker
138// `points_to_add`, `inversion_scratch`, and `pair_dest` arrays.
139inline constexpr size_t BATCH_CAPACITY = 256;
140
141// Lookahead distance (in schedule entries) for the Stage 6a software prefetch of the
142// data-dependent point gather. 16 recovers 4-10% of MSM wall at n >= 2^18, neutral below.
143inline constexpr size_t GATHER_PREFETCH_DIST = 16;
144
145// Phase A's chunked tree-reduce limit. Capped so the per-worker scratch slab
146// (chunk_pts + chunk_ids) stays under ~128 KB.
147inline constexpr size_t DEDUP_MAX_CHUNK_MEMBERS = 2048;
148
149inline constexpr size_t MIN_BATCH_CAPACITY = 32;
150inline constexpr size_t MIN_AFFINE_THREAD_RATIO = 2;
151inline constexpr size_t SUBCHUNK_ENTRIES_CAP = 2048;
152inline constexpr size_t BATCH_MEM_BUDGET = 32ULL * 1024ULL * 1024ULL;
153
154// Per-bucket-chunk metadata produced by Stage 6a, consumed by Stage 6b's
155// cross-thread reduce.
156// lo, hi — lowest / highest non-empty digit in the chunk (inclusive)
157// buckets_padded — next power of two ≥ (hi - lo + 1)
158// empty — 1 iff the chunk had no entries (Stage 6b skips it)
160 uint32_t lo = 0;
161 uint32_t hi = 0;
162 uint32_t buckets_padded = 0;
163 uint8_t empty = 1;
164};
165
166template <typename Curve> struct PerWorkerArenaLayout {
168 using BaseField = typename Curve::BaseField;
169
170 // Caps shared between sizer and allocator. Centralised here so the two
171 // sites can't diverge.
172 static constexpr size_t PHASE_A_DIRTY_SLOTS_CAP = 4096; // HT_SIZE
173 // Per-bucket dedup working-set caps. Loose upper bounds on the distinct duplicate values
174 // (reps) and total non-rep members staged per bucket; sized to cover the densest observed
175 // chonk-wire mega-buckets (~700 distinct long values, ~3-4 members each). When exceeded the
176 // worker leaves the overflow un-deduped (still correct), so these only bound work, not output.
177 static constexpr size_t PHASE_A_BUCKET_REP_CAP = 1024;
178 static constexpr size_t PHASE_A_STAGED_CAP = 4096;
180 static constexpr size_t WORKER_SLAB_ALIGN = alignof(AffineElement);
181
182 // The packed batch-affine drain holds 8 VectorField runs in the fixed ThreadScratch region:
183 // lhs.x, lhs.y, rhs.x, rhs.y (the two input point sets) plus dx, dy, xsum, inv (the add's working
184 // buffers; out shares lhs's backing). The sizer walk below and the allocator in
185 // scalar_multiplication_fast.cpp must agree on this count, so it is centralised here.
186 static constexpr size_t PACKED_DRAIN_VECTORFIELD_RUNS = 8;
187
188 // Computed byte sizes (filled by constructor's layout walk).
189 size_t ts_fixed_layout = 0; // ThreadScratch wpb-independent fields, with align slop
190 size_t pa_layout = 0; // PhaseAScratch fields, with align slop
191 size_t per_worker_union_bytes = 0; // = align_up(max(ts_fixed_layout, pa_layout), WORKER_SLAB_ALIGN)
192 size_t per_worker_per_wpb_layout = 0; // Stage 6 wpb-dependent tail
193 size_t per_worker_bytes = 0; // = align_up(union + tail, WORKER_SLAB_ALIGN)
194
195 // Constructor performs the canonical layout walk. `windows_per_batch` and
196 // `dense_stride_est` may be zero — only the wpb-independent parts then
197 // have meaningful values, useful for the sizer's pre-wpb-solve step.
198 PerWorkerArenaLayout(size_t chunk_capacity,
199 size_t global_max_overflow_per_window,
200 bool dedup_active,
201 size_t phase_a_cluster_members_cap,
202 size_t phase_a_cluster_offsets_cap,
203 size_t windows_per_batch,
204 size_t dense_stride_est) noexcept
205 {
206 auto align_up = [](size_t off, size_t align) -> size_t { return (off + align - 1) & ~(align - 1); };
207 auto layout_add = [&](size_t& off, size_t bytes, size_t align) { off = align_up(off, align) + bytes; };
208
209 // ThreadScratch fixed (curr_pts / curr_buckets / 8 packed batch-affine VectorField runs /
210 // pair_dest / overflow_slots / overflow_pts).
211 layout_add(ts_fixed_layout, sizeof(AffineElement) * chunk_capacity, alignof(AffineElement));
212 layout_add(ts_fixed_layout, sizeof(uint32_t) * chunk_capacity, alignof(uint32_t));
213 // Packed batch-affine drain: PACKED_DRAIN_VECTORFIELD_RUNS runs, mirroring the ts_fixed_alloc
214 // walk in scalar_multiplication_fast.cpp one-for-one.
216 const size_t pack_cap = (BATCH_CAPACITY / VecField::SIZE) + 1;
217 for (size_t k = 0; k < PACKED_DRAIN_VECTORFIELD_RUNS; ++k) {
218 layout_add(ts_fixed_layout, sizeof(VecField) * pack_cap, alignof(VecField));
219 }
220 layout_add(ts_fixed_layout, sizeof(uint32_t) * BATCH_CAPACITY, alignof(uint32_t));
221 layout_add(ts_fixed_layout, sizeof(uint32_t) * global_max_overflow_per_window, alignof(uint32_t));
222 layout_add(ts_fixed_layout, sizeof(AffineElement) * global_max_overflow_per_window, alignof(AffineElement));
223
224 // PhaseA (cluster_members / cluster_offsets / dirty_slots / bucket_rep
225 // / staged / chunk_pts / chunk_ids). Only allocated when dedup_active.
226 if (dedup_active) {
227 layout_add(pa_layout, sizeof(uint32_t) * phase_a_cluster_members_cap, alignof(uint32_t));
228 layout_add(pa_layout, sizeof(uint32_t) * phase_a_cluster_offsets_cap, alignof(uint32_t));
229 layout_add(pa_layout, sizeof(uint16_t) * PHASE_A_DIRTY_SLOTS_CAP, alignof(uint16_t));
230 layout_add(pa_layout, sizeof(uint32_t) * PHASE_A_BUCKET_REP_CAP, alignof(uint32_t));
231 layout_add(pa_layout,
234 layout_add(pa_layout, sizeof(AffineElement) * PHASE_A_CHUNK_CAP, alignof(AffineElement));
235 layout_add(pa_layout, sizeof(uint32_t) * PHASE_A_CHUNK_CAP, alignof(uint32_t));
236 }
237
239
240 // Stage 6 wpb-dependent tail (dense_buckets / is_present / pair
241 // scratch / chunk_infos). Skipped when windows_per_batch == 0 (sizer's
242 // pre-wpb-solve call).
243 if (windows_per_batch != 0) {
244 const size_t dense_total = windows_per_batch * dense_stride_est;
245 const size_t dense_pair_max = dense_total / 2;
246 // dense_buckets is a column (SoA) view: two BaseField coordinate arrays, not one
247 // AffineElement array. Same total bytes (AffineElement == 2 * BaseField), but the live
248 // allocator bumps them as two separate spans, so the sizer must too.
249 layout_add(per_worker_per_wpb_layout, sizeof(BaseField) * dense_total, alignof(BaseField));
250 layout_add(per_worker_per_wpb_layout, sizeof(BaseField) * dense_total, alignof(BaseField));
251 layout_add(per_worker_per_wpb_layout, sizeof(uint8_t) * dense_total, alignof(uint8_t));
252 layout_add(per_worker_per_wpb_layout,
253 sizeof(std::pair<uint32_t, uint32_t>) * dense_pair_max,
255 layout_add(per_worker_per_wpb_layout, sizeof(uint32_t) * dense_pair_max, alignof(uint32_t));
256 layout_add(per_worker_per_wpb_layout, sizeof(BaseField) * dense_pair_max, alignof(BaseField));
257 layout_add(per_worker_per_wpb_layout,
258 sizeof(AffineBucketChunkInfo) * windows_per_batch,
259 alignof(AffineBucketChunkInfo));
260 }
261
263 }
264};
265
266// Stride upper bound for `s.dense_buckets`: next_pow2(⌈(B-1)/T⌉), with a floor of 2.
267[[nodiscard]] inline size_t compute_dense_stride(size_t B_eff, size_t num_threads) noexcept
268{
269 const size_t per_thread = (B_eff > 1) ? ((B_eff - 1 + num_threads - 1) / num_threads) : size_t{ 1 };
270 return std::max<size_t>(2, std::bit_ceil(per_thread));
271}
272
273// Upper bound on Σ_t buckets_per_thread[t][w] per window: B + T - 1 (adjacent threads
274// may share one boundary bucket). Returns 0 when B_eff == 0.
275[[nodiscard]] inline size_t compute_bucket_partials_max(size_t B_eff, size_t num_threads) noexcept
276{
277 return (B_eff > 0) ? (B_eff - 1 + num_threads - 1) : size_t{ 0 };
278}
279
280// Per-OS-thread Stage 6a seam overflow capacity (per-window upper bound).
281[[nodiscard]] inline size_t compute_global_max_overflow_per_window(size_t n,
282 size_t num_threads,
283 size_t subchunk_entries_cap) noexcept
284{
285 const size_t global_max_chunk_len = (n + num_threads - 1) / num_threads;
286 return (global_max_chunk_len + subchunk_entries_cap - 1) / subchunk_entries_cap;
287}
288
289// Per-window byte cost for one window in a windows-per-batch slab. Identical formula
290// at three sites (sizer outer, sizer per-schedule lambda, live allocator); centralised
291// here so they cannot drift.
292//
293// schedule = 4·n
294// HIST slot = max(4·t·B, sizeof(ChunkOutput)·t + 96·t) [H ∪ O overlay]
295// DENSE slot = 65 · bucket_partials_max(B, t) [bucket_partials_dense + present]
296// bucket_start = 8·(B+1)
297// chunk arrays = 8·(t+1) + 8·(t+1) + 8·t + 8·t + 8·t + 16·worker + 8·t
298// dense_buckets = 87·worker·stride [s.dense_buckets + aux]
299template <typename Curve>
300[[nodiscard]] inline size_t compute_per_window_bytes(
301 size_t num_threads, size_t B_eff, size_t n, size_t dense_stride, size_t worker_total) noexcept
302{
303 const size_t bucket_partials_max = compute_bucket_partials_max(B_eff, num_threads);
304 const size_t hist_h_bytes_pw = size_t{ 4 } * num_threads * B_eff;
305 const size_t hist_o_bytes_pw = (sizeof(ChunkOutput<Curve>) * num_threads) + (size_t{ 96 } * num_threads);
306 const size_t hist_slot_bytes_pw = std::max(hist_h_bytes_pw, hist_o_bytes_pw);
307 const size_t dense_slot_bytes_pw = size_t{ 65 } * bucket_partials_max;
308 return (size_t{ 4 } * n) + hist_slot_bytes_pw + dense_slot_bytes_pw + (size_t{ 8 } * (B_eff + 1)) +
309 (size_t{ 8 } * (num_threads + 1)) + (size_t{ 8 } * (num_threads + 1)) + (size_t{ 8 } * num_threads) +
310 (size_t{ 8 } * num_threads) + (size_t{ 8 } * num_threads) + (size_t{ 16 } * worker_total) +
311 (size_t{ 8 } * num_threads) + (size_t{ 87 } * worker_total * dense_stride);
312}
313
314// Phase-1 prologue bytes living in the per-MSM arena (msb_per_scalar, glv_scalars,
315// glv_points, per_thread_msb_hist). Two-copy duplicate eliminated.
316[[nodiscard]] inline size_t compute_phase_one_prologue_bytes(size_t n,
317 bool use_glv,
318 bool inline_glv_double,
319 size_t profile_threads) noexcept
320{
321 return n // msb_per_scalar
322 + (use_glv ? size_t{ 32 } * n : size_t{ 0 }) // glv_scalars_storage
323 + (inline_glv_double ? size_t{ 64 } * n : size_t{ 0 }) // glv_points_storage
324 + (profile_threads * size_t{ 1024 }); // per_thread_msb_hist
325}
326
330};
331
332// Phase A per-worker caps. `members_cap = min(DEDUP_MAX_MEMBERS, n)` is tight (each
333// scalar contributes ≤ 1 cluster_member entry). `offsets_cap = cids_per_thread + 2`
334// covers the leading-zero sentinel + post-last terminator.
335[[nodiscard]] inline PhaseACaps compute_phase_a_caps(size_t n, size_t num_threads) noexcept
336{
337 return { std::min(DEDUP_MAX_MEMBERS, n), (DEDUP_MAX_CLUSTERS / num_threads) + 2 };
338}
339
340// Solve `wpb · per_window_bytes ≤ available_budget`, clamped to W_R and ≥ 1.
341// Mirrors the three identical wpb-pickers in the sizer and live allocator.
342[[nodiscard]] inline size_t solve_wpb(size_t per_window_bytes, size_t available_budget, size_t W_R) noexcept
343{
344 if (W_R == 0) {
345 return 1;
346 }
347 if (per_window_bytes == 0 || available_budget == 0) {
348 return std::max<size_t>(1, W_R);
349 }
350 return std::min(std::max<size_t>(1, available_budget / per_window_bytes), W_R);
351}
352
353} // namespace bb::scalar_multiplication::round_parallel_detail
typename Group::element Element
Definition grumpkin.hpp:63
typename Group::affine_element AffineElement
Definition grumpkin.hpp:64
WindowSchedule build_window_schedule(size_t num_bits, size_t window_bits) noexcept
size_t compute_global_max_overflow_per_window(size_t n, size_t num_threads, size_t subchunk_entries_cap) noexcept
size_t compute_phase_one_prologue_bytes(size_t n, bool use_glv, bool inline_glv_double, size_t profile_threads) noexcept
size_t solve_wpb(size_t per_window_bytes, size_t available_budget, size_t W_R) noexcept
size_t compute_per_window_bytes(size_t num_threads, size_t B_eff, size_t n, size_t dense_stride, size_t worker_total) noexcept
size_t compute_bucket_partials_max(size_t B_eff, size_t num_threads) noexcept
PhaseACaps compute_phase_a_caps(size_t n, size_t num_threads) noexcept
size_t compute_dense_stride(size_t B_eff, size_t num_threads) noexcept
uint32_t choose_window_bits(size_t num_points, size_t num_bits, size_t n_input, size_t num_logical_threads) noexcept
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
PerWorkerArenaLayout(size_t chunk_capacity, size_t global_max_overflow_per_window, bool dedup_active, size_t phase_a_cluster_members_cap, size_t phase_a_cluster_offsets_cap, size_t windows_per_batch, size_t dense_stride_est) noexcept