Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
vector_field_push_span.hpp
Go to the documentation of this file.
1#pragma once
2
5
6#include <array>
7#include <cstddef>
8#include <cstdint>
9#include <span>
10#include <tuple>
11#include <type_traits>
12#include <utility>
13
14// Packed, SIMD-native storage of field elements for the fast-Pippenger rewrite.
15//
16// A VectorField<Params> packs W (= 5) field elements together and runs each arithmetic operation on
17// all W at once with a single SIMD instruction. A VectorFieldPushSpan is a growable, indexed sequence
18// of field elements already laid out in that packed form, over memory it borrows but does not own
19// (a std::span). Hence the name: a span you can also push elements into.
20//
21// Two ways to use it, mirroring how Pippenger fills a batch and then consumes it:
22//
23// - Fill with push(): append one element at a time. Every W elements completes a VectorField,
24// which is written into the backing. Use push() only when the elements arrive separately (e.g.
25// gathered from scattered memory); feeding a tight compute loop one element at a time just to
26// re-pack them throws away the SIMD speedup.
27// - Read and compute with operator[](g): returns the g-th completed VectorField, so a[g] * b[g]
28// multiplies W pairs of elements in a single instruction. A compute loop reads and writes whole
29// VectorFields this way.
30//
31// The final fewer-than-W elements do not fill a VectorField; they wait in a small staging array (the
32// "tail") and the caller handles them one at a time via tail_elem() / tail_data(). reset() rewinds
33// the span to empty so the same borrowed memory can be refilled for the next batch.
34//
35// ⚠ FOOTGUN — the push cursor (count/lane) is STICKY and is NOT cleared between logical uses. push() /
36// push_point() APPEND at the current cursor; only reset() (and gather_from, which resets first) returns
37// it to empty. So whenever a span — or, worse, a borrowed BACKING shared by two spans — is reused for a
38// fresh fill, the new producer MUST reset() before its first push(), or the appends pile onto the stale
39// cursor and write past the backing: an out-of-bounds store, asserted in debug, SILENT CORRUPTION in
40// release. This is especially easy to hit when two stages share one backing and one stage advances the
41// cursor the other does not expect. Rule: a fill helper that resets internally (gather_from) is safe to
42// reuse; a raw push()/push_point() loop must reset() the span first.
43//
44// When SIMD is compiled out, a VectorField is just W plain field elements, so every loop here becomes
45// an ordinary scalar loop — one source compiles for both native and WASM.
46namespace bb {
47
48template <typename Params> struct VectorFieldPushSpan {
50 using Field = typename Vec::Field;
51 static constexpr size_t W = Vec::SIZE;
52
53 std::span<Vec> vector_fields{}; // borrowed backing of completed VectorFields; owned elsewhere
54 std::array<Field, W> partial{}; // elements of the VectorField currently being filled (the tail)
55 size_t count = 0; // total elements pushed
56 size_t lane = 0; // elements filled in the current VectorField, in [0, W)
57
59 explicit VectorFieldPushSpan(std::span<Vec> vector_fields_) noexcept
60 : vector_fields(vector_fields_)
61 {}
62
63 // Move-only. The cursor (count / lane / partial) tracks a partly-filled VectorField inside the
64 // borrowed backing, which is shared by reference. A copy would duplicate the cursor while still
65 // sharing the backing, so the two copies would write over each other's VectorFields. Pass by
66 // reference; to refill the same storage, reset() it rather than copy.
70 VectorFieldPushSpan& operator=(VectorFieldPushSpan&&) noexcept = default;
71 ~VectorFieldPushSpan() = default;
72
73 // Append one element. When the current VectorField fills (W elements), write it to the backing
74 // and start the next.
75 [[gnu::always_inline]] void push(const Field& v) noexcept
76 {
77 partial[lane] = v;
78 ++lane;
79 ++count;
80 if (lane == W) {
81 BB_ASSERT((count - 1) / W < vector_fields.size());
82 vector_fields[(count - 1) / W] = Vec(partial.data());
83 lane = 0;
84 }
85 }
86
87 // The g-th completed VectorField, for SIMD arithmetic over all W of its elements at once.
88 [[gnu::always_inline]] Vec& operator[](size_t g) noexcept
89 {
90 BB_ASSERT(g < vector_fields.size());
91 return vector_fields[g];
92 }
93 [[gnu::always_inline]] const Vec& operator[](size_t g) const noexcept
94 {
95 BB_ASSERT(g < vector_fields.size());
96 return vector_fields[g];
97 }
98
99 size_t num_full_vectors() const noexcept { return count / W; } // completed VectorFields
100 size_t tail() const noexcept { return count % W; } // trailing elements, in partial[0..tail())
101 const Field* tail_data() const noexcept { return partial.data(); }
102 Field& tail_elem(size_t t) noexcept { return partial[t]; } // t < tail()
103 const Field& tail_elem(size_t t) const noexcept { return partial[t]; } // t < tail()
104 size_t size() const noexcept { return count; }
105 size_t capacity() const noexcept { return vector_fields.size() * W; } // max elements the backing holds
106
107 // True if `other` views the same backing storage (same first VectorField). Lets in-place
108 // primitives assert their no-alias preconditions (e.g. batch_invert needs in and out distinct)
109 // without reaching into the backing span directly.
110 [[nodiscard]] bool shares_backing(const VectorFieldPushSpan& other) const noexcept
111 {
112 return vector_fields.data() == other.vector_fields.data();
113 }
114
115 // True if this span's full-group data overlaps `other`'s backing — i.e. they share backing storage
116 // AND there is something in it. This is the collision an in-place primitive must avoid (batch_invert
117 // writes prefix products into `out` while still reading `in`, so it requires !in.aliases(out)).
118 // Tail-only or empty spans never alias: the tail lives in each span's own `partial`, never the
119 // shared backing, so there is nothing there to clobber.
120 [[nodiscard]] bool aliases(const VectorFieldPushSpan& other) const noexcept
121 {
122 // A span always aliases itself, whatever its fill level: this catches batch_invert(span, span)
123 // on a tail-only (1..W-1 element) span, which shares_backing() alone misses because the tail
124 // lives in each span's own `partial`, not the shared backing.
125 return this == &other || (num_full_vectors() != 0 && shares_backing(other));
126 }
127
128 // Rewind to empty so the borrowed storage can be refilled; leaves the backing memory untouched.
129 void reset() noexcept
130 {
131 count = 0;
132 lane = 0;
133 }
134
135 // Copy src's logical extent (count / lane) onto this span without touching either backing. A
136 // primitive that fills a result span element-for-element from an input of the same shape calls
137 // this so the result reports the same size() / tail() as its input.
138 void adopt_cursor(const VectorFieldPushSpan& src) noexcept
139 {
140 count = src.count;
141 lane = src.lane;
142 }
143};
144
145// Column (struct-of-arrays) view of affine points: x[i] and y[i] are point i's coordinates, each
146// coordinate array contiguous in its own column. This is NOT a span<AffineElement> (which interleaves
147// x, y per point) — the per-column contiguity is exactly what lets VectorField::gather / scatter pull
148// or place W lanes from one column in a single shuffle. Borrowed storage; the view does not own it.
149template <typename Field> struct AffineColumnSpan {
150 std::span<Field> x;
151 std::span<Field> y;
152 size_t size() const noexcept { return x.size(); }
153};
154
155// A sequence of affine points in packed SIMD form, kept as two parallel coordinate spans (x and y).
156// A point is split across them: point W*g + j has its x-coordinate in lane j of x's g-th VectorField
157// and its y-coordinate in lane j of y's g-th VectorField — so a lane holds one coordinate, not a
158// whole point. push_point() advances both coordinate cursors together. Move-only, like its members.
159//
160// TODO(open): read-only inputs (e.g. an already-packed SRS) only ever read this, never push. Should
161// they instead use a lighter read-only view ({span<const Vec>, count}, tail padded into the backing)
162// rather than carry this fill cursor? The answer turns on where a persistent input's tail should live.
163template <typename Params> struct VectorAffineElementPushSpan {
165 using Field = typename Vec::Field;
166
169
172 : x(x_fields)
173 , y(y_fields)
174 {}
175
176 [[gnu::always_inline]] void push_point(const Field& px, const Field& py) noexcept
177 {
178 x.push(px);
179 y.push(py);
180 }
181
182 size_t num_full_vectors() const noexcept { return x.num_full_vectors(); }
183 size_t tail() const noexcept { return x.tail(); }
184 size_t size() const noexcept { return x.size(); }
185 size_t capacity() const noexcept { return x.capacity(); }
186
187 // Fill this packed span by gathering `count` points out of a column view: the k-th pushed point is
188 // buckets at index index_of(k). reset()s first. The scalar -> packed half of the bridge between
189 // scalar bucket storage and packed compute.
190 template <typename IndexFn> void gather_from(const AffineColumnSpan<Field>& buckets, IndexFn index_of, size_t count)
191 {
192 reset();
193 for (size_t k = 0; k < count; ++k) {
194 const size_t b = index_of(k);
195 push_point(buckets.x[b], buckets.y[b]);
196 }
197 }
198
199 // Visit each pushed point (x, y) in push order, with its running index k, invoking
200 // kernel(const Field& x, const Field& y, size_t k). Each full VectorField group is unpacked once
201 // (to_array); the sub-W tail is read element-wise. Centralises the bulk/tail split and the running
202 // index so drain-style callers supply only the per-point body.
203 template <typename Kernel> void for_each_point(Kernel kernel) const
204 {
205 size_t k = 0;
206 for (size_t g = 0; g < x.num_full_vectors(); ++g) {
207 const auto xs = x[g].to_array();
208 const auto ys = y[g].to_array();
209 for (size_t l = 0; l < Vec::SIZE; ++l) {
210 kernel(xs[l], ys[l], k++);
211 }
212 }
213 for (size_t t = 0; t < x.tail(); ++t) {
214 kernel(x.tail_elem(t), y.tail_elem(t), k++);
215 }
216 }
217
218 // Drain every completed point back into a column view: the k-th point (push order) lands at column
219 // index index_of(k). The packed -> scalar half of the bridge; for_each_point owns the per-group
220 // unpack and the sub-W tail, so this is just the indexed store.
221 template <typename IndexFn> void scatter_to(AffineColumnSpan<Field>& buckets, IndexFn index_of) const
222 {
223 for_each_point([&](const Field& xe, const Field& ye, size_t k) {
224 const size_t b = index_of(k);
225 buckets.x[b] = xe;
226 buckets.y[b] = ye;
227 });
228 }
229
230 void reset() noexcept
231 {
232 x.reset();
233 y.reset();
234 }
236 {
237 x.adopt_cursor(src.x);
238 y.adopt_cursor(src.y);
239 }
240};
241
242namespace detail {
243// Stamp the shape (count / lane) of `first` onto `span`, but only if `span` is writable. zip_for_each
244// walks every span in lockstep with the first, so afterwards each output span should report the first's
245// extent. Const (input) spans are skipped; re-stamping a non-const input is a no-op (it already has that
246// extent). This folds the easy-to-forget manual adopt_cursor into zip_for_each itself.
247template <typename Span, typename First>
248[[gnu::always_inline]] inline void adopt_shape_if_writable(Span& span, const First& first) noexcept
249{
250 if constexpr (!std::is_const_v<Span>) {
251 span.adopt_cursor(first);
252 }
253}
254
255template <typename Tuple, typename Kernel, size_t... I>
256[[gnu::always_inline]] inline void zip_for_each_impl(Tuple& spans,
257 Kernel kernel,
258 [[maybe_unused]] std::index_sequence<I...> seq)
259{
260 const auto& first = std::get<0>(spans);
261 const size_t num_full = first.num_full_vectors();
262 const size_t ntail = first.tail();
263 for (size_t g = 0; g < num_full; ++g) {
264 kernel(std::get<I>(spans)[g]...);
265 }
266 for (size_t t = 0; t < ntail; ++t) {
267 kernel(std::get<I>(spans).tail_elem(t)...);
268 }
269 // Output spans inherit the first span's extent automatically — no manual adopt_cursor at call sites.
270 (adopt_shape_if_writable(std::get<I>(spans), first), ...);
271}
272} // namespace detail
273
274// Apply `kernel` to corresponding elements of several VectorFieldPushSpans in lockstep. The last
275// argument is the kernel; the rest are the spans. At each position the kernel receives one value from
276// every span: a VectorField for the full groups, a Field for the trailing tail. Write the kernel as
277// plain field arithmetic over generic parameters — outputs auto&, inputs const auto& — so it never
278// mentions lanes, indices, or the bulk/tail split, and the same kernel serves SIMD and scalar builds.
279// The element count comes from the first span, so pass a filled input first. Nothing is converted
280// (spans are read and written in place); each output span automatically adopts the first span's extent
281// (count / tail) when the walk finishes, so the caller need not adopt_cursor afterward.
282template <typename... Args> [[gnu::always_inline]] inline void zip_for_each(Args&&... args)
283{
284 static_assert(sizeof...(Args) >= 2, "zip_for_each needs at least one span and a kernel");
285 constexpr size_t n = sizeof...(Args);
286 auto spans = std::forward_as_tuple(std::forward<Args>(args)...);
288}
289
290// Direction of an order-sensitive walk. Backed by bool so Forward/Backward map onto the false/true a
291// bare reverse flag would use, which keeps a migration from such a flag mechanical.
292enum class Direction : bool { Forward = false, Backward = true };
293
294// Map-accumulate over a push-span (cf. Haskell's mapAccumL / mapAccumR): thread an accumulator through
295// the elements, calling step(acc, in_elem, out_elem) at each one — step writes a per-element output and
296// advances the accumulator. Full groups carry the VectorField accumulator (bulk_acc),
297// the tail carries the Field one (tail_acc), so `step` is written once as a generic lambda over both.
298// The two accumulators are seeded by the bulk_acc / tail_acc arguments, and the final accumulators are returned. When
299// Dir is Direction::Backward the groups and the tail are walked back-to-front.
300//
301// WARNING: The q1s1 layout interleaves elements across the W lanes, so the per-lane partials recombine to the
302// whole-stream result only when step's operation is _commutative_ (e.g. field + or *); an
303// order-dependent op (a true global prefix) would not survive the lane split.
304template <Direction Dir, typename Params, typename Step>
308 VectorField<Params> bulk_acc,
309 field<Params> tail_acc,
310 Step step)
311{
312 const size_t num_full = in.num_full_vectors();
313 const size_t ntail = in.tail();
314 if constexpr (Dir == Direction::Backward) {
315 for (size_t g = num_full; g-- > 0;) {
316 step(bulk_acc, in[g], out[g]);
317 }
318 for (size_t t = ntail; t-- > 0;) {
319 step(tail_acc, in.tail_elem(t), out.tail_elem(t));
320 }
321 } else {
322 for (size_t g = 0; g < num_full; ++g) {
323 step(bulk_acc, in[g], out[g]);
324 }
325 for (size_t t = 0; t < ntail; ++t) {
326 step(tail_acc, in.tail_elem(t), out.tail_elem(t));
327 }
328 }
329 out.adopt_cursor(in); // result reports the same size() / tail() as the input — callers need not adopt
330 return { bulk_acc, tail_acc };
331}
332
333// Walk `pairs` over a column (SoA) view, handing the kernel references straight into each pair's dst
334// and src buckets — kernel(x_dst&, y_dst&, x_src, y_src, i) — so the body is plain field arithmetic
335// with no index or pointer bookkeeping. The dst coordinates are mutable (the kernel may update them in
336// place); the src coordinates are read-only. The walker owns the software prefetch: it warms the random
337// bucket addresses PREFETCH_AHEAD steps further along the walk (write hint for dst, read hint for src).
338// Direction::Backward walks high index to low. always_inline so the kernel fuses into the loop with no
339// call overhead. (Linear scratch the kernel may touch is left to the hardware prefetcher.)
340template <Direction Dir, typename Field, typename Kernel>
341[[gnu::always_inline]] inline void for_each_indexed_pair(AffineColumnSpan<Field>& buckets,
343 size_t n,
344 Kernel kernel) noexcept
345{
346 constexpr int64_t PREFETCH_AHEAD = 4;
347 Field* const px = buckets.x.data();
348 Field* const py = buckets.y.data();
349 for (size_t s = 0; s < n; ++s) {
350 const size_t i = (Dir == Direction::Backward) ? n - 1 - s : s;
351 const int64_t pf = (Dir == Direction::Backward) ? static_cast<int64_t>(i) - PREFETCH_AHEAD
352 : static_cast<int64_t>(i) + PREFETCH_AHEAD;
353 if (pf >= 0 && static_cast<size_t>(pf) < n) {
354 __builtin_prefetch(px + pairs[pf].first, 1, 3);
355 __builtin_prefetch(px + pairs[pf].second, 0, 3);
356 __builtin_prefetch(py + pairs[pf].first, 1, 3);
357 __builtin_prefetch(py + pairs[pf].second, 0, 3);
358 }
359 const uint32_t dst = pairs[i].first;
360 const uint32_t src = pairs[i].second;
361 kernel(px[dst], py[dst], px[src], py[src], i);
362 }
363}
364
365// As for_each_indexed_pair, but one bucket per step (for doublings): kernel(x&, y&, i) over
366// buckets[indices[i]], with the same prefetch and direction handling.
367template <Direction Dir, typename Field, typename Kernel>
368[[gnu::always_inline]] inline void for_each_indexed_point(AffineColumnSpan<Field>& buckets,
369 const uint32_t* indices,
370 size_t n,
371 Kernel kernel) noexcept
372{
373 constexpr int64_t PREFETCH_AHEAD = 4;
374 Field* const px = buckets.x.data();
375 Field* const py = buckets.y.data();
376 for (size_t s = 0; s < n; ++s) {
377 const size_t i = (Dir == Direction::Backward) ? n - 1 - s : s;
378 const int64_t pf = (Dir == Direction::Backward) ? static_cast<int64_t>(i) - PREFETCH_AHEAD
379 : static_cast<int64_t>(i) + PREFETCH_AHEAD;
380 if (pf >= 0 && static_cast<size_t>(pf) < n) {
381 __builtin_prefetch(px + indices[pf], 1, 3);
382 __builtin_prefetch(py + indices[pf], 1, 3);
383 }
384 const uint32_t b = indices[i];
385 kernel(px[b], py[b], i);
386 }
387}
388
389} // namespace bb
#define BB_ASSERT(expression,...)
Definition assert.hpp:70
FF b
void adopt_shape_if_writable(Span &span, const First &first) noexcept
void zip_for_each_impl(Tuple &spans, Kernel kernel, std::index_sequence< I... > seq)
Entry point for Barretenberg command-line interface.
Definition api.hpp:5
void zip_for_each(Args &&... args)
std::pair< VectorField< Params >, field< Params > > map_accumulate(const VectorFieldPushSpan< Params > &in, VectorFieldPushSpan< Params > &out, VectorField< Params > bulk_acc, field< Params > tail_acc, Step step)
int64_t index_of(std::vector< T > const &vec, T const &item)
Definition container.hpp:58
void for_each_indexed_point(AffineColumnSpan< Field > &buckets, const uint32_t *indices, size_t n, Kernel kernel) noexcept
void for_each_indexed_pair(AffineColumnSpan< Field > &buckets, const std::pair< uint32_t, uint32_t > *pairs, size_t n, Kernel kernel) noexcept
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
bb::VectorAffineElementPushSpan< BaseParams > out
size_t size() const noexcept
VectorAffineElementPushSpan(std::span< Vec > x_fields, std::span< Vec > y_fields) noexcept
void scatter_to(AffineColumnSpan< Field > &buckets, IndexFn index_of) const
void gather_from(const AffineColumnSpan< Field > &buckets, IndexFn index_of, size_t count)
void push_point(const Field &px, const Field &py) noexcept
void adopt_cursor(const VectorAffineElementPushSpan &src) noexcept
field< Params > Field
static constexpr size_t SIZE
void adopt_cursor(const VectorFieldPushSpan &src) noexcept
void push(const Field &v) noexcept
bool shares_backing(const VectorFieldPushSpan &other) const noexcept
size_t capacity() const noexcept
VectorFieldPushSpan(VectorFieldPushSpan &&) noexcept=default
const Field & tail_elem(size_t t) const noexcept
const Field * tail_data() const noexcept
Vec & operator[](size_t g) noexcept
VectorFieldPushSpan(std::span< Vec > vector_fields_) noexcept
size_t num_full_vectors() const noexcept
const Vec & operator[](size_t g) const noexcept
Field & tail_elem(size_t t) noexcept
bool aliases(const VectorFieldPushSpan &other) const noexcept
std::array< Field, W > partial
VectorFieldPushSpan & operator=(const VectorFieldPushSpan &)=delete
VectorFieldPushSpan(const VectorFieldPushSpan &)=delete
General class for prime fields see Prime field documentation["field documentation"] for general imple...