-
Notifications
You must be signed in to change notification settings - Fork 611
feat: new op queue logic #11999
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+283
−0
Merged
feat: new op queue logic #11999
Changes from 15 commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
2ba0cd3
new op queue with basic functionality and a test
ledwards2225 453a72b
basic support for populating ultra ops table
ledwards2225 95cbd1f
improve naming
ledwards2225 bce7d70
class is reduced to UltraOpsTable
ledwards2225 af74321
add iterator; range based loop works
ledwards2225 272ad02
implement the ultra ops table via inheritance
ledwards2225 28be472
test updates
ledwards2225 e671a42
change model so subtable must be created rather than completed
ledwards2225 672e831
name changefile
ledwards2225 7cc7295
simpler table concept
ledwards2225 9d818c4
a simpler inheritence structure for both ultra and raw tables
ledwards2225 0d5664a
comments update
ledwards2225 11b1751
file name change
ledwards2225 9414c70
Merge branch 'master' into lde/new_op_queue
ledwards2225 e31e190
separate test file
ledwards2225 b04384c
simplify by using an std container and composition over inheritance
ledwards2225 5a37316
ditch the deque
ledwards2225 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
142 changes: 142 additions & 0 deletions
142
barretenberg/cpp/src/barretenberg/stdlib_circuit_builders/op_queue/ecc_ops_table.hpp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| #pragma once | ||
|
|
||
| #include "barretenberg/ecc/curves/bn254/bn254.hpp" | ||
| #include "barretenberg/eccvm/eccvm_builder_types.hpp" | ||
| #include "barretenberg/stdlib_circuit_builders/op_queue/ecc_op_queue.hpp" | ||
| namespace bb { | ||
|
|
||
| /** | ||
| * @brief A table of ECC operations | ||
| * @details The table is constructed via concatenation of subtables of ECC operations. The table concatentation protocol | ||
| * (Merge protocol) requires that the concatenation be achieved via PRE-pending successive tables. To avoid the need for | ||
| * expensive memory reallocations associated with physically prepending, the subtables are stored in a linked list that | ||
| * can be traversed to reconstruct the columns of the aggregate tables as needed (e.g. in corresponding polynomials). | ||
| * | ||
| * @tparam OpFormat Format of the ECC operations stored in the table | ||
| */ | ||
| template <typename OpFormat> class EccOpsTable { | ||
| using Curve = curve::BN254; | ||
| using Fr = Curve::ScalarField; | ||
|
|
||
| protected: | ||
| // A segment of the table. (In practice, each subtable stores the operations of a single circuit) | ||
| struct Subtable { | ||
| std::vector<OpFormat> data; | ||
| std::unique_ptr<Subtable> prev; // the previously constructed subtable in the linked list | ||
|
|
||
| Subtable(size_t size_hint = 0) { data.reserve(size_hint); } | ||
|
|
||
| size_t size() const { return data.size(); } | ||
| }; | ||
|
|
||
| std::unique_ptr<Subtable> current_subtable = nullptr; // the subtable to which new ops are appended | ||
|
|
||
| public: | ||
| size_t size() const | ||
| { | ||
| size_t total_size = 0; | ||
| for (auto* subtable = current_subtable.get(); subtable != nullptr; subtable = subtable->prev.get()) { | ||
| total_size += subtable->size(); | ||
| } | ||
| return total_size; | ||
| } | ||
|
|
||
| // An iterator to allow range based iteration over the full table | ||
| class Iterator { | ||
| Subtable* subtable = nullptr; | ||
| size_t index = 0; | ||
| size_t subtable_size = 0; | ||
|
|
||
| public: | ||
| Iterator() = default; | ||
| Iterator(Subtable* sub, size_t idx) | ||
| : subtable(sub) | ||
| , index(idx) | ||
| , subtable_size(sub ? sub->size() : 0) | ||
| {} | ||
|
|
||
| OpFormat operator*() { return subtable->data[index]; } | ||
|
|
||
| Iterator& operator++() | ||
| { | ||
| if (++index < subtable_size) { // return row within the current subtable | ||
| return *this; | ||
| } | ||
| if (subtable->prev) { // update to next subtable | ||
| subtable = subtable->prev.get(); | ||
| subtable_size = subtable->size(); | ||
| index = 0; | ||
| } else { | ||
| *this = Iterator(); // end of iteration | ||
| } | ||
| return *this; | ||
| } | ||
|
|
||
| bool operator!=(const Iterator& other) const { return subtable != other.subtable || index != other.index; } | ||
| }; | ||
|
|
||
| Iterator begin() { return Iterator(current_subtable.get(), 0); } | ||
| Iterator end() { return {}; } // end marked by default iterator | ||
|
|
||
| void create_new_subtable(size_t size_hint = 0) | ||
| { | ||
| auto new_subtable = std::make_unique<Subtable>(size_hint); // new subtable | ||
| new_subtable->prev = std::move(current_subtable); // old one is the prev of the new one | ||
| current_subtable = std::move(new_subtable); // set the new one to current | ||
| } | ||
|
|
||
| // push a new operation to the current subtable | ||
| void push(const OpFormat& op) { current_subtable->data.push_back(op); } | ||
| }; | ||
|
|
||
| /** | ||
| * @brief Stores a table of elliptic curve operations represented in the Ultra format | ||
| * @details An ECC operation OP involing point P(X,Y) and scalar z is represented in the Ultra format as a tuple of the | ||
| * form {OP, X_lo, X_hi, Y_lo, Y_hi, z1, z2}, where the coordinates are split into hi and lo limbs and z1, z2 are the | ||
| * endomorphism scalars associated with z. Because the Ultra/Mega arithmetization utilizes 4 wires, each op occupies two | ||
| * rows in a width-4 execution trace, arranged as follows: | ||
| * | ||
| * OP | X_lo | X_hi | Y_lo | ||
| * 0 | Y_hi | z1 | z2 | ||
| * | ||
| * The table data is stored in the UltraOp tuple format but is converted to four columns of Fr scalars for use in the | ||
| * polynomials in the proving system. | ||
| */ | ||
| class UltraEccOpsTable : public EccOpsTable<UltraOp> { | ||
| static constexpr size_t TABLE_WIDTH = 4; | ||
| using Curve = curve::BN254; | ||
| using Fr = Curve::ScalarField; | ||
|
|
||
| public: | ||
| /** | ||
| * @brief Populate the provided array of columns with the width-4 representation of the table data | ||
| * @todo multithreaded this functionality | ||
| * @param target_columns | ||
| */ | ||
| void populate_column_data(std::array<std::span<Fr>, TABLE_WIDTH>& target_columns) | ||
| { | ||
| size_t i = 0; | ||
| for (auto* subtable = current_subtable.get(); subtable != nullptr; subtable = subtable->prev.get()) { | ||
| for (const auto& op : subtable->data) { | ||
| target_columns[0][i] = op.op; | ||
| target_columns[1][i] = op.x_lo; | ||
| target_columns[2][i] = op.x_hi; | ||
| target_columns[3][i] = op.y_lo; | ||
| i++; | ||
| target_columns[0][i] = 0; // only the first 'op' field is utilized | ||
| target_columns[1][i] = op.y_hi; | ||
| target_columns[2][i] = op.z_1; | ||
| target_columns[3][i] = op.z_2; | ||
| i++; | ||
| } | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * @brief The table of ECC ops used by the ECCVM circuit builder | ||
| * @note just an alias for a specialized EccOpsTable | ||
| */ | ||
| using RawEccOpsTable = EccOpsTable<eccvm::VMOperation<curve::BN254::Group>>; | ||
|
|
||
| } // namespace bb | ||
173 changes: 173 additions & 0 deletions
173
barretenberg/cpp/src/barretenberg/stdlib_circuit_builders/op_queue/ecc_ops_table.test.cpp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| #include "barretenberg/stdlib_circuit_builders/op_queue/ecc_ops_table.hpp" | ||
| #include "barretenberg/common/zip_view.hpp" | ||
| #include "barretenberg/polynomials/polynomial.hpp" | ||
| #include "barretenberg/stdlib_circuit_builders/op_queue/ecc_op_queue.hpp" | ||
| #include <gtest/gtest.h> | ||
|
|
||
| #include <ranges> | ||
|
|
||
| using namespace bb; | ||
|
|
||
| class EccOpsTableTest : public ::testing::Test { | ||
| using Curve = curve::BN254; | ||
| using Scalar = fr; | ||
| using ECCVMOperation = eccvm::VMOperation<Curve::Group>; | ||
|
|
||
| public: | ||
| // Mock ultra ops table that constructs a concatenated table from successively prepended subtables | ||
| struct MockUltraOpsTable { | ||
| std::array<std::vector<Scalar>, 4> columns; | ||
| void append(const UltraOp& op) | ||
| { | ||
| columns[0].push_back(op.op); | ||
| columns[1].push_back(op.x_lo); | ||
| columns[2].push_back(op.x_hi); | ||
| columns[3].push_back(op.y_lo); | ||
|
|
||
| columns[0].push_back(0); | ||
| columns[1].push_back(op.y_hi); | ||
| columns[2].push_back(op.z_1); | ||
| columns[3].push_back(op.z_2); | ||
| } | ||
|
|
||
| // Construct the= ultra ops table such that the subtables appear in reverse order, as if prepended | ||
| MockUltraOpsTable(const auto& subtable_ops) | ||
| { | ||
| for (auto& ops : std::ranges::reverse_view(subtable_ops)) { | ||
| for (const auto& op : ops) { | ||
| append(op); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| size_t size() const { return columns[0].size(); } | ||
| }; | ||
|
|
||
| // Mock raw ops table that constructs a concatenated table from successively prepended subtables | ||
| struct MockRawOpsTable { | ||
| std::vector<ECCVMOperation> raw_ops; | ||
| void append(const ECCVMOperation& op) { raw_ops.push_back(op); } | ||
|
|
||
| MockRawOpsTable(const auto& subtable_ops) | ||
| { | ||
| for (auto& ops : std::ranges::reverse_view(subtable_ops)) { | ||
| for (const auto& op : ops) { | ||
| append(op); | ||
| } | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| static UltraOp random_ultra_op() | ||
| { | ||
| UltraOp op; | ||
| op.op_code = NULL_OP; | ||
| op.op = Scalar::random_element(); | ||
| op.x_lo = Scalar::random_element(); | ||
| op.x_hi = Scalar::random_element(); | ||
| op.y_lo = Scalar::random_element(); | ||
| op.y_hi = Scalar::random_element(); | ||
| op.z_1 = Scalar::random_element(); | ||
| op.z_2 = Scalar::random_element(); | ||
| op.return_is_infinity = false; | ||
| return op; | ||
| } | ||
|
|
||
| static ECCVMOperation random_raw_op() | ||
| { | ||
| return ECCVMOperation{ .mul = true, | ||
| .base_point = curve::BN254::Group::affine_element::random_element(), | ||
| .z1 = uint256_t(Scalar::random_element()), | ||
| .z2 = uint256_t(Scalar::random_element()), | ||
| .mul_scalar_full = Scalar::random_element() }; | ||
| } | ||
| }; | ||
|
|
||
| // Ensure UltraOpsTable correctly constructs a concatenated table from successively prepended subtables | ||
| TEST(EccOpsTableTest, UltraOpsTable) | ||
| { | ||
| using Fr = fr; | ||
|
|
||
| constexpr size_t NUM_ROWS_PER_OP = 2; // Each ECC op is represented by two rows in the ultra ops table | ||
|
|
||
| // Construct sets of ultra ops, each representing those added by a single circuit | ||
| const size_t NUM_SUBTABLES = 3; | ||
| std::array<std::vector<UltraOp>, NUM_SUBTABLES> subtable_ultra_ops; | ||
| std::array<size_t, NUM_SUBTABLES> subtable_op_counts = { 4, 2, 7 }; | ||
| for (auto [subtable_ops, op_count] : zip_view(subtable_ultra_ops, subtable_op_counts)) { | ||
| for (size_t i = 0; i < op_count; ++i) { | ||
| subtable_ops.push_back(EccOpsTableTest::random_ultra_op()); | ||
| } | ||
| } | ||
|
|
||
| // Construct the mock ultra ops table which contains the subtables ordered in reverse (as if prepended) | ||
| EccOpsTableTest::MockUltraOpsTable expected_ultra_ops_table(subtable_ultra_ops); | ||
|
|
||
| // Construct the concatenated table internal to the op queue | ||
| UltraEccOpsTable ultra_ops_table; | ||
| for (const auto& subtable_ops : subtable_ultra_ops) { | ||
| ultra_ops_table.create_new_subtable(); | ||
| for (const auto& op : subtable_ops) { | ||
| ultra_ops_table.push(op); | ||
| } | ||
| } | ||
|
|
||
| // Check that the ultra ops table internal to the op queue has the correct size | ||
| auto expected_num_ops = std::accumulate(subtable_op_counts.begin(), subtable_op_counts.end(), size_t(0)); | ||
| EXPECT_EQ(ultra_ops_table.size(), expected_num_ops); | ||
|
|
||
| // Construct polynomials corresponding to the columns of the ultra ops table | ||
| const size_t expected_table_size = expected_num_ops * NUM_ROWS_PER_OP; | ||
| std::array<Polynomial<Fr>, 4> ultra_ops_table_polynomials; | ||
| std::array<std::span<fr>, 4> column_spans; | ||
| for (auto [column_span, column] : zip_view(column_spans, ultra_ops_table_polynomials)) { | ||
| column = Polynomial<Fr>(expected_table_size); | ||
| column_span = column.coeffs(); | ||
| } | ||
| ultra_ops_table.populate_column_data(column_spans); | ||
|
|
||
| // Check that the ultra ops table constructed by the op queue matches the expected table | ||
| for (auto [expected_column, poly] : zip_view(expected_ultra_ops_table.columns, ultra_ops_table_polynomials)) { | ||
| for (auto [expected_value, value] : zip_view(expected_column, poly.coeffs())) { | ||
| EXPECT_EQ(expected_value, value); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Ensure RawOpsTable correctly constructs a concatenated table from successively prepended subtables | ||
| TEST(EccOpsTableTest, RawOpsTable) | ||
| { | ||
| using ECCVMOperation = bb::eccvm::VMOperation<curve::BN254::Group>; | ||
|
|
||
| // Construct sets of raw ops, each representing those added by a single circuit | ||
| const size_t NUM_SUBTABLES = 3; | ||
| std::array<std::vector<ECCVMOperation>, NUM_SUBTABLES> subtable_raw_ops; | ||
| std::array<size_t, NUM_SUBTABLES> subtable_op_counts = { 4, 2, 7 }; | ||
| for (auto [subtable_ops, op_count] : zip_view(subtable_raw_ops, subtable_op_counts)) { | ||
| for (size_t i = 0; i < op_count; ++i) { | ||
| subtable_ops.push_back(EccOpsTableTest::random_raw_op()); | ||
| } | ||
| } | ||
|
|
||
| // Construct the mock raw ops table which contains the subtables ordered in reverse (as if prepended) | ||
| EccOpsTableTest::MockRawOpsTable expected_raw_ops_table(subtable_raw_ops); | ||
|
|
||
| // Construct the concatenated raw ops table | ||
| RawEccOpsTable raw_ops_table; | ||
| for (const auto& subtable_ops : subtable_raw_ops) { | ||
| raw_ops_table.create_new_subtable(); | ||
| for (const auto& op : subtable_ops) { | ||
| raw_ops_table.push(op); | ||
| } | ||
| } | ||
|
|
||
| // Check that the table has the correct size | ||
| auto expected_num_ops = std::accumulate(subtable_op_counts.begin(), subtable_op_counts.end(), size_t(0)); | ||
| EXPECT_EQ(raw_ops_table.size(), expected_num_ops); | ||
|
|
||
| // Check that the table matches the manually constructed mock table | ||
| size_t idx = 0; | ||
| for (const auto& op : raw_ops_table) { | ||
| EXPECT_EQ(expected_raw_ops_table.raw_ops[idx++], op); | ||
| } | ||
| } |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: could get away without storing size
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah I did this to save on the size() call every time in a hot loop but maybe thats overkill. I would def prefer just calling size()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah so what's going to happen:
vs
the pointer we are dereferencing will be in cache, so it's a trivial difference
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are also returning a rather big struct, which, fair if we are reading one field from it might be a hot loop, but if we are doing anything with a field the loop overhead is not that big. Comes down to how many times we will be iterating during a proving lifetime, I guess. Could be worth the cycles potentially