Skip to content

Commit 2f410ad

Browse files
committed
Merge bitcoin/bitcoin#32263: cluster mempool: add TxGraph work controls
62ed1f9 txgraph: check that DoWork finds optimal if given high budget (tests) (Pieter Wuille) f3c2fc8 txgraph: add work limit to DoWork(), try optimal (feature) (Pieter Wuille) e96b00d txgraph: make number of acceptable iterations configurable (feature) (Pieter Wuille) cfe9958 txgraph: track amount of work done in linearization (preparation) (Pieter Wuille) 6ba316e txgraph: 1-or-2-tx split-off clusters are optimal (optimization) (Pieter Wuille) fad0eb0 txgraph: reset quality when merging clusters (bugfix) (Pieter Wuille) Pull request description: Part of #30289. Builds on top of #31553. So far, the `TxGraph::DoWork()` function took no parameters, and just made all clusters reach the "acceptable" internal quality level by performing a minimum number of improvement iterations on it, but: * Did not attempt to go beyond that. * Was broken, as the QualityLevel of optimal clusters that merge together was not being reset. Fix this by adding an argument to `DoWork()` to control how much work it is allowed to do right now, which will first be used to get all clusters to the acceptable level, and if more budget remains, use it to try to get some or all clusters optimal. The function will now return `true` if all clusters are known to be optimal (and thus no further work remains). This is verified in the tests, by remembering whether the graph is optimal, and if it is at the end of the simulation run, verify that the overall linearization cannot be improved further. ACKs for top commit: instagibbs: ACK 62ed1f9 ismaelsadeeq: Code review ACK 62ed1f9 glozow: ACK 62ed1f9 Tree-SHA512: 5f57d4052e369f3444e72e724f04c02004e0f66e365faa59c9f145323e606508380fc97bb038b68783a62ae9c10757f1b628b3b00b2ce9a46161fca2d4336d73
2 parents 953c90d + 62ed1f9 commit 2f410ad

File tree

9 files changed

+183
-55
lines changed

9 files changed

+183
-55
lines changed

src/bench/cluster_linearize.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,8 +229,8 @@ void BenchLinearizeOptimally(benchmark::Bench& bench, const std::array<uint8_t,
229229
reader >> Using<DepGraphFormatter>(depgraph);
230230
uint64_t rng_seed = 0;
231231
bench.run([&] {
232-
auto res = Linearize(depgraph, /*max_iterations=*/10000000, rng_seed++);
233-
assert(res.second);
232+
auto [_lin, optimal, _cost] = Linearize(depgraph, /*max_iterations=*/10000000, rng_seed++);
233+
assert(optimal);
234234
});
235235
};
236236

src/bench/txgraph.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ void BenchTxGraphTrim(benchmark::Bench& bench)
4646
static constexpr int NUM_DEPS_PER_BOTTOM_TX = 100;
4747
/** Set a very large cluster size limit so that only the count limit is triggered. */
4848
static constexpr int32_t MAX_CLUSTER_SIZE = 100'000 * 100;
49+
/** Set a very high number for acceptable iterations, so that we certainly benchmark optimal
50+
* linearization. */
51+
static constexpr uint64_t NUM_ACCEPTABLE_ITERS = 100'000'000;
4952

5053
/** Refs to all top transactions. */
5154
std::vector<TxGraph::Ref> top_refs;
@@ -57,7 +60,7 @@ void BenchTxGraphTrim(benchmark::Bench& bench)
5760
std::vector<size_t> top_components;
5861

5962
InsecureRandomContext rng(11);
60-
auto graph = MakeTxGraph(MAX_CLUSTER_COUNT, MAX_CLUSTER_SIZE);
63+
auto graph = MakeTxGraph(MAX_CLUSTER_COUNT, MAX_CLUSTER_SIZE, NUM_ACCEPTABLE_ITERS);
6164

6265
// Construct the top chains.
6366
for (int chain = 0; chain < NUM_TOP_CHAINS; ++chain) {

src/cluster_linearize.h

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,19 +1030,20 @@ class SearchCandidateFinder
10301030
* linearize.
10311031
* @param[in] old_linearization An existing linearization for the cluster (which must be
10321032
* topologically valid), or empty.
1033-
* @return A pair of:
1033+
* @return A tuple of:
10341034
* - The resulting linearization. It is guaranteed to be at least as
10351035
* good (in the feerate diagram sense) as old_linearization.
10361036
* - A boolean indicating whether the result is guaranteed to be
10371037
* optimal.
1038+
* - How many optimization steps were actually performed.
10381039
*
10391040
* Complexity: possibly O(N * min(max_iterations + N, sqrt(2^N))) where N=depgraph.TxCount().
10401041
*/
10411042
template<typename SetType>
1042-
std::pair<std::vector<DepGraphIndex>, bool> Linearize(const DepGraph<SetType>& depgraph, uint64_t max_iterations, uint64_t rng_seed, std::span<const DepGraphIndex> old_linearization = {}) noexcept
1043+
std::tuple<std::vector<DepGraphIndex>, bool, uint64_t> Linearize(const DepGraph<SetType>& depgraph, uint64_t max_iterations, uint64_t rng_seed, std::span<const DepGraphIndex> old_linearization = {}) noexcept
10431044
{
10441045
Assume(old_linearization.empty() || old_linearization.size() == depgraph.TxCount());
1045-
if (depgraph.TxCount() == 0) return {{}, true};
1046+
if (depgraph.TxCount() == 0) return {{}, true, 0};
10461047

10471048
uint64_t iterations_left = max_iterations;
10481049
std::vector<DepGraphIndex> linearization;
@@ -1113,7 +1114,7 @@ std::pair<std::vector<DepGraphIndex>, bool> Linearize(const DepGraph<SetType>& d
11131114
}
11141115
}
11151116

1116-
return {std::move(linearization), optimal};
1117+
return {std::move(linearization), optimal, max_iterations - iterations_left};
11171118
}
11181119

11191120
/** Improve a given linearization.

src/test/fuzz/cluster_linearize.cpp

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1154,7 +1154,8 @@ FUZZ_TARGET(clusterlin_linearize)
11541154

11551155
// Invoke Linearize().
11561156
iter_count &= 0x7ffff;
1157-
auto [linearization, optimal] = Linearize(depgraph, iter_count, rng_seed, old_linearization);
1157+
auto [linearization, optimal, cost] = Linearize(depgraph, iter_count, rng_seed, old_linearization);
1158+
assert(cost <= iter_count);
11581159
SanityCheck(depgraph, linearization);
11591160
auto chunking = ChunkLinearization(depgraph, linearization);
11601161

@@ -1166,24 +1167,9 @@ FUZZ_TARGET(clusterlin_linearize)
11661167
}
11671168

11681169
// If the iteration count is sufficiently high, an optimal linearization must be found.
1169-
// Each linearization step can use up to 2^(k-1) iterations, with steps k=1..n. That sum is
1170-
// 2^n - 1.
1171-
const uint64_t n = depgraph.TxCount();
1172-
if (n <= 19 && iter_count > (uint64_t{1} << n)) {
1170+
if (iter_count >= MaxOptimalLinearizationIters(depgraph.TxCount())) {
11731171
assert(optimal);
11741172
}
1175-
// Additionally, if the assumption of sqrt(2^k)+1 iterations per step holds, plus ceil(k/4)
1176-
// start-up cost per step, plus ceil(n^2/64) start-up cost overall, we can compute the upper
1177-
// bound for a whole linearization (summing for k=1..n) using the Python expression
1178-
// [sum((k+3)//4 + int(math.sqrt(2**k)) + 1 for k in range(1, n + 1)) + (n**2 + 63) // 64 for n in range(0, 35)]:
1179-
static constexpr uint64_t MAX_OPTIMAL_ITERS[] = {
1180-
0, 4, 8, 12, 18, 26, 37, 51, 70, 97, 133, 182, 251, 346, 480, 666, 927, 1296, 1815, 2545,
1181-
3576, 5031, 7087, 9991, 14094, 19895, 28096, 39690, 56083, 79263, 112041, 158391, 223936,
1182-
316629, 447712
1183-
};
1184-
if (n < std::size(MAX_OPTIMAL_ITERS) && iter_count >= MAX_OPTIMAL_ITERS[n]) {
1185-
Assume(optimal);
1186-
}
11871173

11881174
// If Linearize claims optimal result, run quality tests.
11891175
if (optimal) {
@@ -1322,7 +1308,7 @@ FUZZ_TARGET(clusterlin_postlinearize_tree)
13221308

13231309
// Try to find an even better linearization directly. This must not change the diagram for the
13241310
// same reason.
1325-
auto [opt_linearization, _optimal] = Linearize(depgraph_tree, 100000, rng_seed, post_linearization);
1311+
auto [opt_linearization, _optimal, _cost] = Linearize(depgraph_tree, 100000, rng_seed, post_linearization);
13261312
auto opt_chunking = ChunkLinearization(depgraph_tree, opt_linearization);
13271313
auto cmp_opt = CompareChunks(opt_chunking, post_chunking);
13281314
assert(cmp_opt == 0);

src/test/fuzz/txgraph.cpp

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <cluster_linearize.h>
66
#include <test/fuzz/FuzzedDataProvider.h>
77
#include <test/fuzz/fuzz.h>
8+
#include <test/util/cluster_linearize.h>
89
#include <test/util/random.h>
910
#include <txgraph.h>
1011
#include <util/bitset.h>
@@ -58,6 +59,8 @@ struct SimTxGraph
5859
SetType modified;
5960
/** The configured maximum total size of transactions per cluster. */
6061
uint64_t max_cluster_size;
62+
/** Whether the corresponding real graph is known to be optimally linearized. */
63+
bool real_is_optimal{false};
6164

6265
/** Construct a new SimTxGraph with the specified maximum cluster count and size. */
6366
explicit SimTxGraph(DepGraphIndex cluster_count, uint64_t cluster_size) :
@@ -139,6 +142,7 @@ struct SimTxGraph
139142
{
140143
assert(graph.TxCount() < MAX_TRANSACTIONS);
141144
auto simpos = graph.AddTransaction(feerate);
145+
real_is_optimal = false;
142146
MakeModified(simpos);
143147
assert(graph.Positions()[simpos]);
144148
simmap[simpos] = std::make_shared<TxGraph::Ref>();
@@ -158,6 +162,7 @@ struct SimTxGraph
158162
if (chl_pos == MISSING) return;
159163
graph.AddDependencies(SetType::Singleton(par_pos), chl_pos);
160164
MakeModified(par_pos);
165+
real_is_optimal = false;
161166
// This may invalidate our cached oversized value.
162167
if (oversized.has_value() && !*oversized) oversized = std::nullopt;
163168
}
@@ -168,6 +173,7 @@ struct SimTxGraph
168173
auto pos = Find(ref);
169174
if (pos == MISSING) return;
170175
// No need to invoke MakeModified, because this equally affects main and staging.
176+
real_is_optimal = false;
171177
graph.FeeRate(pos).fee = fee;
172178
}
173179

@@ -177,6 +183,7 @@ struct SimTxGraph
177183
auto pos = Find(ref);
178184
if (pos == MISSING) return;
179185
MakeModified(pos);
186+
real_is_optimal = false;
180187
graph.RemoveTransactions(SetType::Singleton(pos));
181188
simrevmap.erase(simmap[pos].get());
182189
// Retain the TxGraph::Ref corresponding to this position, so the Ref destruction isn't
@@ -203,6 +210,7 @@ struct SimTxGraph
203210
} else {
204211
MakeModified(pos);
205212
graph.RemoveTransactions(SetType::Singleton(pos));
213+
real_is_optimal = false;
206214
simrevmap.erase(simmap[pos].get());
207215
simmap[pos].reset();
208216
// This may invalidate our cached oversized value.
@@ -309,9 +317,11 @@ FUZZ_TARGET(txgraph)
309317
auto max_cluster_count = provider.ConsumeIntegralInRange<DepGraphIndex>(1, MAX_CLUSTER_COUNT_LIMIT);
310318
/** The maximum total size of transactions in a (non-oversized) cluster. */
311319
auto max_cluster_size = provider.ConsumeIntegralInRange<uint64_t>(1, 0x3fffff * MAX_CLUSTER_COUNT_LIMIT);
320+
/** The number of iterations to consider a cluster acceptably linearized. */
321+
auto acceptable_iters = provider.ConsumeIntegralInRange<uint64_t>(0, 10000);
312322

313323
// Construct a real graph, and a vector of simulated graphs (main, and possibly staging).
314-
auto real = MakeTxGraph(max_cluster_count, max_cluster_size);
324+
auto real = MakeTxGraph(max_cluster_count, max_cluster_size, acceptable_iters);
315325
std::vector<SimTxGraph> sims;
316326
sims.reserve(2);
317327
sims.emplace_back(max_cluster_count, max_cluster_size);
@@ -465,6 +475,7 @@ FUZZ_TARGET(txgraph)
465475
if (top_sim.graph.Ancestors(pos_par)[pos_chl]) break;
466476
}
467477
top_sim.AddDependency(par, chl);
478+
top_sim.real_is_optimal = false;
468479
real->AddDependency(*par, *chl);
469480
break;
470481
} else if ((block_builders.empty() || sims.size() > 1) && top_sim.removed.size() < 100 && command-- == 0) {
@@ -719,7 +730,40 @@ FUZZ_TARGET(txgraph)
719730
break;
720731
} else if (command-- == 0) {
721732
// DoWork.
722-
real->DoWork();
733+
uint64_t iters = provider.ConsumeIntegralInRange<uint64_t>(0, alt ? 10000 : 255);
734+
bool ret = real->DoWork(iters);
735+
uint64_t iters_for_optimal{0};
736+
for (unsigned level = 0; level < sims.size(); ++level) {
737+
// DoWork() will not optimize oversized levels, or the main level if a builder
738+
// is present. Note that this impacts the DoWork() return value, as true means
739+
// that non-optimal clusters may remain within such oversized or builder-having
740+
// levels.
741+
if (sims[level].IsOversized()) continue;
742+
if (level == 0 && !block_builders.empty()) continue;
743+
// If neither of the two above conditions holds, and DoWork() returned true,
744+
// then the level is optimal.
745+
if (ret) {
746+
sims[level].real_is_optimal = true;
747+
}
748+
// Compute how many iterations would be needed to make everything optimal.
749+
for (auto component : sims[level].GetComponents()) {
750+
auto iters_opt_this_cluster = MaxOptimalLinearizationIters(component.Count());
751+
if (iters_opt_this_cluster > acceptable_iters) {
752+
// If the number of iterations required to linearize this cluster
753+
// optimally exceeds acceptable_iters, DoWork() may process it in two
754+
// stages: once to acceptable, and once to optimal.
755+
iters_for_optimal += iters_opt_this_cluster + acceptable_iters;
756+
} else {
757+
iters_for_optimal += iters_opt_this_cluster;
758+
}
759+
}
760+
}
761+
if (!ret) {
762+
// DoWork can only have more work left if the requested number of iterations
763+
// was insufficient to linearize everything optimally within the levels it is
764+
// allowed to touch.
765+
assert(iters <= iters_for_optimal);
766+
}
723767
break;
724768
} else if (sims.size() == 2 && !sims[0].IsOversized() && !sims[1].IsOversized() && command-- == 0) {
725769
// GetMainStagingDiagrams()
@@ -1003,6 +1047,16 @@ FUZZ_TARGET(txgraph)
10031047
}
10041048
assert(todo.None());
10051049

1050+
// If the real graph claims to be optimal (the last DoWork() call returned true), verify
1051+
// that calling Linearize on it does not improve it further.
1052+
if (sims[0].real_is_optimal) {
1053+
auto real_diagram = ChunkLinearization(sims[0].graph, vec1);
1054+
auto [sim_lin, _optimal, _cost] = Linearize(sims[0].graph, 300000, rng.rand64(), vec1);
1055+
auto sim_diagram = ChunkLinearization(sims[0].graph, sim_lin);
1056+
auto cmp = CompareChunks(real_diagram, sim_diagram);
1057+
assert(cmp == 0);
1058+
}
1059+
10061060
// For every transaction in the total ordering, find a random one before it and after it,
10071061
// and compare their chunk feerates, which must be consistent with the ordering.
10081062
for (size_t pos = 0; pos < vec1.size(); ++pos) {

src/test/txgraph_tests.cpp

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313

1414
BOOST_AUTO_TEST_SUITE(txgraph_tests)
1515

16+
/** The number used as acceptable_iters argument in these tests. High enough that everything
17+
* should be optimal, always. */
18+
static constexpr uint64_t NUM_ACCEPTABLE_ITERS = 100'000'000;
19+
1620
BOOST_AUTO_TEST_CASE(txgraph_trim_zigzag)
1721
{
1822
// T T T T T T T T T T T T T T (50 T's)
@@ -35,7 +39,7 @@ BOOST_AUTO_TEST_CASE(txgraph_trim_zigzag)
3539
static constexpr int32_t MAX_CLUSTER_SIZE = 100'000 * 100;
3640

3741
// Create a new graph for the test.
38-
auto graph = MakeTxGraph(MAX_CLUSTER_COUNT, MAX_CLUSTER_SIZE);
42+
auto graph = MakeTxGraph(MAX_CLUSTER_COUNT, MAX_CLUSTER_SIZE, NUM_ACCEPTABLE_ITERS);
3943

4044
// Add all transactions and store their Refs.
4145
std::vector<TxGraph::Ref> refs;
@@ -98,7 +102,7 @@ BOOST_AUTO_TEST_CASE(txgraph_trim_flower)
98102
/** Set a very large cluster size limit so that only the count limit is triggered. */
99103
static constexpr int32_t MAX_CLUSTER_SIZE = 100'000 * 100;
100104

101-
auto graph = MakeTxGraph(MAX_CLUSTER_COUNT, MAX_CLUSTER_SIZE);
105+
auto graph = MakeTxGraph(MAX_CLUSTER_COUNT, MAX_CLUSTER_SIZE, NUM_ACCEPTABLE_ITERS);
102106

103107
// Add all transactions and store their Refs.
104108
std::vector<TxGraph::Ref> refs;
@@ -184,7 +188,7 @@ BOOST_AUTO_TEST_CASE(txgraph_trim_huge)
184188
std::vector<size_t> top_components;
185189

186190
FastRandomContext rng;
187-
auto graph = MakeTxGraph(MAX_CLUSTER_COUNT, MAX_CLUSTER_SIZE);
191+
auto graph = MakeTxGraph(MAX_CLUSTER_COUNT, MAX_CLUSTER_SIZE, NUM_ACCEPTABLE_ITERS);
188192

189193
// Construct the top chains.
190194
for (int chain = 0; chain < NUM_TOP_CHAINS; ++chain) {
@@ -256,7 +260,7 @@ BOOST_AUTO_TEST_CASE(txgraph_trim_big_singletons)
256260
static constexpr int NUM_TOTAL_TX = 100;
257261

258262
// Create a new graph for the test.
259-
auto graph = MakeTxGraph(MAX_CLUSTER_COUNT, MAX_CLUSTER_SIZE);
263+
auto graph = MakeTxGraph(MAX_CLUSTER_COUNT, MAX_CLUSTER_SIZE, NUM_ACCEPTABLE_ITERS);
260264

261265
// Add all transactions and store their Refs.
262266
std::vector<TxGraph::Ref> refs;

src/test/util/cluster_linearize.h

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,29 @@ void SanityCheck(const DepGraph<SetType>& depgraph, std::span<const DepGraphInde
394394
}
395395
}
396396

397+
inline uint64_t MaxOptimalLinearizationIters(DepGraphIndex cluster_count)
398+
{
399+
// We assume sqrt(2^k)+1 candidate-finding iterations per candidate to be found, plus ceil(k/4)
400+
// startup cost when up to k unlinearization transactions remain, plus ceil(n^2/64) overall
401+
// startup cost in Linearize. Thus, we can compute the upper bound for a whole linearization
402+
// (summing for k=1..n) using the Python expression:
403+
//
404+
// [sum((k+3)//4 + math.isqrt(2**k) + 1 for k in range(1, n + 1)) + (n**2 + 63) // 64 for n in range(0, 65)]
405+
//
406+
// Note that these are just assumptions, as the proven upper bound grows with 2^k, not
407+
// sqrt(2^k).
408+
static constexpr uint64_t MAX_OPTIMAL_ITERS[65] = {
409+
0, 4, 8, 12, 18, 26, 37, 51, 70, 97, 133, 182, 251, 346, 480, 666, 927, 1296, 1815, 2545,
410+
3576, 5031, 7087, 9991, 14094, 19895, 28096, 39690, 56083, 79263, 112041, 158391, 223936,
411+
316629, 447712, 633086, 895241, 1265980, 1790280, 2531747, 3580335, 5063259, 7160424,
412+
10126257, 14320575, 20252230, 28640853, 40504150, 57281380, 81007962, 114562410, 162015557,
413+
229124437, 324030718, 458248463, 648061011, 916496483, 1296121563, 1832992493, 2592242635,
414+
3665984477, 5184484745, 7331968412, 10368968930, 14663936244
415+
};
416+
assert(cluster_count < sizeof(MAX_OPTIMAL_ITERS) / sizeof(MAX_OPTIMAL_ITERS[0]));
417+
return MAX_OPTIMAL_ITERS[cluster_count];
418+
}
419+
397420
} // namespace
398421

399422
#endif // BITCOIN_TEST_UTIL_CLUSTER_LINEARIZE_H

0 commit comments

Comments
 (0)