Skip to content

Commit fd097eb

Browse files
author
AztecBot
committed
chore: sync public-v5-next with upstream v5-next
2 parents 186b8c2 + 3ea5dee commit fd097eb

127 files changed

Lines changed: 3104 additions & 1328 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.test_patterns.yml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -410,11 +410,6 @@ tests:
410410
owners:
411411
- *martin
412412

413-
- regex: "yarn-project/scripts/run_test.sh bb-prover/src/avm_proving_tests/avm_"
414-
error_regex: "timeout: sending signal"
415-
owners:
416-
- *charlie
417-
418413
- regex: "run_test.sh simple tx_stats_bench"
419414
error_regex: "✕ verifies transactions at 10 TPS"
420415
owners:

barretenberg/cpp/src/barretenberg/vm2/tracegen/lib/lookup_builder.hpp

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@ template <typename LookupSettings_> class IndexedLookupTraceBuilder : public Int
5151
}
5252

5353
trace.set(LookupSettings::COUNTS, dst_row, trace.get(LookupSettings::COUNTS, dst_row) + 1);
54-
// Set the fine grained inner selector if it's not already one.
55-
if (LookupSettings::DST_SELECTOR != this->outer_dst_selector &&
56-
trace.get(LookupSettings::DST_SELECTOR, dst_row) != 1) {
57-
trace.set(LookupSettings::DST_SELECTOR, dst_row, 1);
54+
if (LookupSettings::DST_SELECTOR != this->outer_dst_selector) {
55+
// This step might write to the same cell from multiple threads, so we use atomic limbs to avoid UB.
56+
// Since we are always writing a 1, the end result will be 1 even under concurrency.
57+
trace.set(LookupSettings::DST_SELECTOR, dst_row, 1, /*use_atomic_limbs=*/true);
5858
}
5959
});
6060
}
@@ -198,10 +198,10 @@ template <typename LookupSettings> class LookupIntoDynamicTableSequential : publ
198198
if (dst_selector == 1 && src_values == trace.get_multiple(LookupSettings::DST_COLUMNS, dst_row)) {
199199
trace.set(LookupSettings::COUNTS, dst_row, trace.get(LookupSettings::COUNTS, dst_row) + 1);
200200

201-
// Set the fine grained inner selector if it's not already one.
202-
if (LookupSettings::DST_SELECTOR != this->outer_dst_selector &&
203-
trace.get(LookupSettings::DST_SELECTOR, dst_row) != 1) {
204-
trace.set(LookupSettings::DST_SELECTOR, dst_row, 1);
201+
if (LookupSettings::DST_SELECTOR != this->outer_dst_selector) {
202+
// This step might write to the same cell from multiple threads, so we use atomic limbs to avoid
203+
// UB. Since we are always writing a 1, the end result will be 1 even under concurrency.
204+
trace.set(LookupSettings::DST_SELECTOR, dst_row, 1, /*use_atomic_limbs=*/true);
205205
}
206206

207207
found = true;

barretenberg/cpp/src/barretenberg/vm2/tracegen/trace_container.cpp

Lines changed: 121 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
#include "barretenberg/vm2/tracegen/trace_container.hpp"
22

33
#include <algorithm>
4-
#include <mutex>
4+
#include <ranges>
55

6+
#include "barretenberg/common/assert.hpp"
7+
#include "barretenberg/common/compiler_hints.hpp"
68
#include "barretenberg/common/log.hpp"
79
#include "barretenberg/common/ref_vector.hpp"
810
#include "barretenberg/vm2/common/field.hpp"
@@ -12,7 +14,19 @@ namespace bb::avm2::tracegen {
1214
namespace {
1315

1416
// We need a zero value to return (a reference to) when a value is not found.
15-
static const FF zero = FF::zero();
17+
const FF zero = FF::zero();
18+
19+
// Writes each 64-bit limb of the field with a relaxed atomic store. Each limb is naturally aligned (FF is
20+
// alignas(32), 4x uint64_t), so this lowers to 4 plain `movq` stores on x86-64 — no lock, no libatomic call
21+
// (unlike a whole-field std::atomic_ref<FF>, whose 32 bytes exceed the lock-free width). Lets set() make a
22+
// same-cell concurrent write data-race-free when every writer stores the same value.
23+
inline void store_per_limb(FF& cell, const FF& value)
24+
{
25+
static_assert(sizeof(FF) == 4 * sizeof(uint64_t));
26+
for (size_t i = 0; i < 4; ++i) {
27+
std::atomic_ref<uint64_t>(cell.data[i]).store(value.data[i], std::memory_order_relaxed);
28+
}
29+
}
1630

1731
} // namespace
1832

@@ -23,9 +37,15 @@ TraceContainer::TraceContainer()
2337
const FF& TraceContainer::get(Column col, uint32_t row) const
2438
{
2539
auto& column_data = (*trace)[static_cast<size_t>(col)];
26-
std::shared_lock lock(column_data.mutex);
27-
const auto it = column_data.rows.find(row);
28-
return it == column_data.rows.end() ? zero : it->second;
40+
const size_t shard_idx = row / INTERVAL_SIZE;
41+
if (shard_idx >= NUM_SHARDS) {
42+
return zero;
43+
}
44+
ColumnInterval* shard = column_data.slots[shard_idx].load(std::memory_order_acquire);
45+
if (shard == nullptr) {
46+
return zero;
47+
}
48+
return shard->rows[row % INTERVAL_SIZE];
2949
}
3050

3151
const FF& TraceContainer::get_column_or_shift(ColumnAndShifts col, uint32_t row) const
@@ -36,19 +56,52 @@ const FF& TraceContainer::get_column_or_shift(ColumnAndShifts col, uint32_t row)
3656
return get(static_cast<Column>(col), row);
3757
}
3858

39-
void TraceContainer::set(Column col, uint32_t row, const FF& value)
59+
TraceContainer::ColumnInterval& TraceContainer::get_or_create_shard(SparseColumn& column_data, size_t shard_idx)
60+
{
61+
ColumnInterval* shard = column_data.slots[shard_idx].load(std::memory_order_acquire);
62+
if (shard != nullptr) {
63+
return *shard;
64+
}
65+
// Slow path: this slot has never been written. Construct a shard and install it with a single CAS.
66+
// Creators of different shards target different atomics and run fully in parallel with no lock. If we
67+
// lose the race for this slot (only possible when two chunks share a shard at a boundary), we discard
68+
// our spare copy and use the winner's.
69+
auto fresh = std::make_unique<ColumnInterval>();
70+
ColumnInterval* expected = nullptr;
71+
if (column_data.slots[shard_idx].compare_exchange_strong(
72+
expected, fresh.get(), std::memory_order_acq_rel, std::memory_order_acquire)) {
73+
return *fresh.release();
74+
}
75+
return *expected; // CAS failure loaded the winning pointer into `expected` (acquire).
76+
}
77+
78+
void TraceContainer::set(Column col, uint32_t row, const FF& value, bool use_atomic_limbs)
4079
{
4180
auto& column_data = (*trace)[static_cast<size_t>(col)];
42-
std::unique_lock lock(column_data.mutex);
81+
const size_t shard_idx = row / INTERVAL_SIZE;
82+
BB_ASSERT_LT(shard_idx, NUM_SHARDS, "row exceeds the maximum trace size");
83+
const uint32_t offset = row % INTERVAL_SIZE;
84+
4385
if (!value.is_zero()) {
44-
column_data.rows.insert_or_assign(row, value);
45-
column_data.max_row_number = std::max(column_data.max_row_number, static_cast<int64_t>(row));
86+
// Lock-free: a single atomic load finds the shard (created on first write), then we write our
87+
// own dense cell directly. Different rows are distinct array elements, so concurrent writers of
88+
// this column (or even of the same shard, at a chunk boundary) never race and never serialize.
89+
auto& cell = get_or_create_shard(column_data, shard_idx).rows[offset];
90+
if (BB_UNLIKELY(use_atomic_limbs)) {
91+
store_per_limb(cell, value);
92+
} else {
93+
cell = value;
94+
}
4695
} else {
47-
auto num_erased = column_data.rows.erase(row);
48-
if (column_data.max_row_number == row && num_erased > 0) {
49-
// This shouldn't happen often. We delay recalculation of the max row number
50-
// until someone actually needs it.
51-
column_data.row_number_dirty = true;
96+
// Zero value: clear if present. We never create a shard, so sparse (mostly-zero) columns are not
97+
// materialized (an unset cell already reads as zero).
98+
ColumnInterval* shard = column_data.slots[shard_idx].load(std::memory_order_acquire);
99+
if (shard != nullptr) {
100+
if (BB_UNLIKELY(use_atomic_limbs)) {
101+
store_per_limb(shard->rows[offset], zero);
102+
} else {
103+
shard->rows[offset] = FF::zero();
104+
}
52105
}
53106
}
54107
}
@@ -62,24 +115,39 @@ void TraceContainer::set(uint32_t row, std::span<const std::pair<Column, FF>> va
62115

63116
void TraceContainer::reserve_column(Column col, size_t size)
64117
{
118+
if (size == 0) {
119+
return;
120+
}
65121
auto& column_data = (*trace)[static_cast<size_t>(col)];
66-
std::unique_lock lock(column_data.mutex);
67-
column_data.rows.reserve(size);
122+
const size_t num_shards = std::min((size + INTERVAL_SIZE - 1) / INTERVAL_SIZE, NUM_SHARDS);
123+
// Each shard's dense row array is full size on creation, so reserving just materializes the shards up
124+
// front (e.g. for precomputed columns). Lock-free: get_or_create_shard installs each via CAS.
125+
for (size_t k = 0; k < num_shards; ++k) {
126+
get_or_create_shard(column_data, k);
127+
}
68128
}
69129

70130
uint32_t TraceContainer::get_column_rows(Column col) const
71131
{
132+
// The number of rows is (highest non-zero absolute row + 1). We find it by scanning shards from the
133+
// top down and, within the first non-empty shard, scanning its rows from the top. Shards are
134+
// top-dense, so this terminates almost immediately. This is only called after the parallel fill
135+
// phase, so no lock is needed. Lower shards cannot hold a higher row, so the first hit is the answer.
72136
auto& column_data = (*trace)[static_cast<size_t>(col)];
73-
std::unique_lock lock(column_data.mutex);
74-
if (column_data.row_number_dirty) {
75-
// Trigger recalculation of max row number.
76-
auto keys = std::views::keys(column_data.rows);
77-
const auto it = std::ranges::max_element(keys);
78-
// We use -1 to indicate that the column is empty.
79-
column_data.max_row_number = it == keys.end() ? -1 : static_cast<int64_t>(*it);
80-
column_data.row_number_dirty = false;
81-
}
82-
return static_cast<uint32_t>(column_data.max_row_number + 1);
137+
for (size_t k = NUM_SHARDS; k-- > 0;) {
138+
ColumnInterval* shard_ptr = column_data.slots[k].load(std::memory_order_acquire);
139+
if (shard_ptr == nullptr) {
140+
continue;
141+
}
142+
const auto& rows = shard_ptr->rows;
143+
const uint32_t base = static_cast<uint32_t>(k) * INTERVAL_SIZE;
144+
for (uint32_t off = INTERVAL_SIZE; off-- > 0;) {
145+
if (!rows[off].is_zero()) {
146+
return base + off + 1;
147+
}
148+
}
149+
}
150+
return 0;
83151
}
84152

85153
uint32_t TraceContainer::get_num_witness_rows() const
@@ -103,9 +171,18 @@ uint32_t TraceContainer::get_num_rows() const
103171
void TraceContainer::visit_column(Column col, const std::function<void(uint32_t, const FF&)>& visitor) const
104172
{
105173
auto& column_data = (*trace)[static_cast<size_t>(col)];
106-
std::shared_lock lock(column_data.mutex);
107-
for (const auto& [row, value] : column_data.rows) {
108-
visitor(row, value);
174+
for (size_t k = 0; k < NUM_SHARDS; ++k) {
175+
ColumnInterval* shard_ptr = column_data.slots[k].load(std::memory_order_acquire);
176+
if (shard_ptr == nullptr) {
177+
continue;
178+
}
179+
auto& shard = *shard_ptr;
180+
const uint32_t base = static_cast<uint32_t>(k) * INTERVAL_SIZE;
181+
for (uint32_t off = 0; off < INTERVAL_SIZE; ++off) {
182+
if (!shard.rows[off].is_zero()) {
183+
visitor(base + off, shard.rows[off]);
184+
}
185+
}
109186
}
110187
}
111188

@@ -120,20 +197,28 @@ void TraceContainer::invert_column(Column col)
120197
{
121198
RefVector<FF> ff_vector;
122199
auto& column_data = (*trace)[static_cast<size_t>(col)];
123-
std::unique_lock lock(column_data.mutex);
124-
for (auto& [row, value] : column_data.rows) {
125-
ff_vector.push_back(value);
200+
for (size_t k = 0; k < NUM_SHARDS; ++k) {
201+
ColumnInterval* shard_ptr = column_data.slots[k].load(std::memory_order_acquire);
202+
if (shard_ptr == nullptr) {
203+
continue;
204+
}
205+
auto& shard = *shard_ptr;
206+
for (auto& value : shard.rows) {
207+
if (!value.is_zero()) {
208+
ff_vector.push_back(value);
209+
}
210+
}
126211
}
127212
FF::batch_invert<RefVector<FF>>(ff_vector);
128213
}
129214

130215
void TraceContainer::clear_column(Column col)
131216
{
217+
// Lock-free: exchange hands each non-null shard pointer to exactly one caller, which frees it.
132218
auto& column_data = (*trace)[static_cast<size_t>(col)];
133-
std::unique_lock lock(column_data.mutex);
134-
column_data.rows.clear();
135-
column_data.max_row_number = -1;
136-
column_data.row_number_dirty = false;
219+
for (size_t k = 0; k < NUM_SHARDS; ++k) {
220+
delete column_data.slots[k].exchange(nullptr, std::memory_order_acq_rel);
221+
}
137222
}
138223

139224
} // namespace bb::avm2::tracegen

0 commit comments

Comments
 (0)