execution/commitment: streaming commitment trie (--experimental.streaming-commitment) - #21709
Conversation
Plan for parallelizing hph.Process via a new ParallelPatriciaHashed sibling type. Builds on the existing depth-1 ConcurrentPatriciaHashed PoC and pushes split-points to arbitrary depths using a path-compressed prefix trie built during TouchPlainKey, a maphash.NonConcurrentMap of split-points consulted at fold time, and a last-finisher-continues barrier protocol over atomic.Int32 arrival counters.
First building block for ParallelPatriciaHashed. The trie tracks touched hashed keys during TouchPlainKey and exposes a subtreeCount on every node so the future Prepare pass can identify split-point candidates in O(1).
Introduces the per-batch data carrier that drives parallel commitment. parallelUpdate wraps the prefix trie from the prior commit and adds a NonConcurrentMap of split-points, a leaf-task queue, and a mutex-guarded deferred-update slice. splitPoint holds the per-prefix barrier state (atomic arrival counter + 16-cell deposit grid laid out to match DeferredBranchUpdate.cells). leafTask carries the work-unit metadata. newParallelUpdate / Insert / Reset / Close / appendDeferred are wired up; Prepare and the worker fold-time barrier follow in later tasks. Race-detector tests cover construction, Insert delegation, full Reset state-clear, appendDeferred under 16-way concurrent contention, and splitPoint.arrived correctness.
Implements the freeze-time DFS that partitions touched-key prefix trie into split-points (fanout >= 2 && subtree >= 32 keys) and leaf-tasks for the parallel commitment worker pool. For each split-point, fetches the on-disk branch via ctx.Branch and pre-populates sp.cells for untouched nibble siblings so the last-finisher fold produces a correct branch hash. leafQueue is sorted by keyCount descending for better worker utilisation.
Add the ModeParallel mode constant and integrate the parallelUpdate prefix-trie state into the Updates struct lifecycle. TouchPlainKey, TouchPlainKeyDirect, and TouchHashedKey route through the per-nibble ETL collectors and into the parallel trie; NewUpdates/SetMode/Reset/ Close/Size handle the new mode symmetrically. IsConcurrentCommitment now returns true for either ModeParallel or legacy sortPerNibble so the ConcurrentPatriciaHashed PoC stays functional.
Introduce ParallelPatriciaHashed: a configuration-and-lifecycle wrapper around a template HexPatriciaHashed plus a worker sync.Pool. The skeleton implements Reset, Release, ResetContext, SetTrace, SetTraceDomain, EnableWarmupCache, GetCapture, SetCapture, EnableCsvMetrics, RootTrie, Variant, and a RootHash that publishes the last-finisher's atomic value (falling back to the template for the no-updates path). Adds VariantParallelHexPatricia and a "parallel" key in ParseTrieVariant. Process and the worker fold-time barrier are added in Tasks 6 and 7.
Task 6: orchestration without the worker fold-time barrier. Adds ParallelPatriciaHashed.Process, the per-nibble ETL dispatcher, the warmupSplitAncestors helper, and the assertEquivalentRoot test helper that drives both ModeDirect and ModeParallel through the same update set and asserts byte-equal roots. The barrier protocol (Task 7) is required to merge multiple worker roots; Process explicitly rejects multi-leafTask-per-nibble inputs until then.
…atriciaHashed Workers under a shared split-point now converge via cellEncodeData deposits into sp.cells with sp.arrived atomic coordination. The last-finisher rebuilds the split-point's grid row from every deposit and folds upward, either depositing again at a further-enclosing split-point or — at the topmost — publishing the root hash via p.rootHash CAS. Design diverges from the original plan in two places, both documented in docs/plans/20260516-parallel-hph.md: - the per-fold-step splitMap query doesn't fire reliably on empty-DB tries whose row depths don't match split-point depths, so the loop folds fully first and then walks the enclosing-split-point chain via the leafTask's prefix - sp.arrived initialises to fanout (not fanout-1) so the truly-last Add(-1) returns 0, eliminating the race where fanout=2 left both workers as last-finishers
Add a ModeParallel arm to verify_test.go that drives random update batches through sequential HexPatriciaHashed (ModeDirect) and ParallelPatriciaHashed (ModeParallel) and asserts byte-equal root hashes. The cardinal correctness rule: same-root-as-sequential. Covers five batch shapes: accounts-only, storage-heavy single account, storage spread across accounts, mix of inserts and deletes, and empty batches. TestVerifyParallel_AllShapes runs one of each as a subtest; TestVerifyParallel_RandomBatches sweeps 1100 randomized batches across shapes; FuzzParallelEquivalence exposes the harness to go test -fuzz. The harness exposed an indexing bug in depositRootIntoSplitPoint when a leafTask covers a single account's deep storage subtree: fold produces a cell whose extLen exceeds the 64-byte extension array, panicking on slice. Documented as a Task 9 follow-up; the affected shape is gated behind shapeRequiresStorageDeepBarrierFix and t.Skip'd until fixed.
Task 9 of the parallel HPH plan: nine end-to-end edge-case tests covering deletions, bloatnet shape, single-account-many-storage, empty batches, single-touched-key, mixed account/storage, and same-account multi-field updates. All run through the cardinal-correctness assertEquivalentRoot helper. Three underlying barrier-protocol gaps surfaced and were fixed: - foldDrainWithBarrier: in multi-phase scenarios the worker's followAndUpdate unfolds the shared root branch from DB into its row stack; folding past the split-point's child depth then absorbs that shared branch into the worker's deposit, overwriting siblings via deferred updates. Identify the first enclosing split-point up front, stop folding while depths[deepest] > len(sp.prefix)+1, and deposit grid[deepest][childNibble] directly when a row remains at deposit depth. Fresh-DB workers (every existing test) still collapse to activeRows==0 and use the original hph.root path. - Prepare: a split-point at a node where a key terminates (e.g. an account at depth 64 alongside its storage subtree) silently drops the terminating key — splitPoint.cells has no terminator slot. Detect via subtreeCount > sum(child.subtreeCount) and collapse the subtree to a single leafTask instead. - depositRootIntoSplitPoint: hph.root.extLen can exceed 64 when the worker folds a deep storage subtree (cell.extension is [64]byte and fold silently truncates). Snapshot currentKey + depths[0] before the final row-0 fold; on overflow, reconstruct the trimmed extension from the snapshot and skip computeCellHash (the cell's hash is already the branch hash from foldBranch's keccak2.Read). These fixes unblock the fuzz harness's shapeStorageHeavySingle, which is re-enabled in TestVerifyParallel_RandomBatches and FuzzParallelEquivalence.
…l-commitment Adds a runtime feature flag (default off) that routes NewSharedDomains to construct a ParallelPatriciaHashed trie + ModeParallel Updates buffer. InitializeTrieAndUpdates forces ModeParallel for VariantParallelHexPatricia so the Updates buffer always allocates parallelUpdate state Prepare reads. commitmentdb/commitment_context.go gains a switch arm for the parallel trie: each Process spins up a concurrent TrieContextFactory (per-worker collectors) and injects it via SetTrieContextFactory. LatestCommitmentState/encode/restore state paths now also accept VariantParallelHexPatricia via RootTrie(). Integration tests in db/state/execctx/parallel_commitment_flag_test.go cover both flag positions and assert root-hash equivalence between the sequential and parallel paths on a basic commit.
ParallelPatriciaHashed workers concurrently access PatriciaContext.Branch and PutBranch via the deferred-update path. MockState guards its branchData/accountData/storageData maps with a mutex only when SetConcurrentCommitment(true) is set, but the new parallel helpers omitted the toggle, so `go test -race ./execution/commitment/...` flagged map races on every multi-worker test. Production callers use the commitmentdb context which is naturally serialized; this is purely a test-side fix. Closes Task 11 (acceptance verification) in docs/plans/20260516-parallel-hph.md.
Adds execution/commitment/agents.md describing the three trie variants (HexPatriciaHashed, ConcurrentPatriciaHashed, ParallelPatriciaHashed), when to use ModeParallel, and the --experimental.parallel-commitment CLI flag. Moves the completed parallel-hph plan into docs/plans/completed/.
- Drop unused produceCellForBarrier and its test; barrier deposits use depositRootIntoSplitPoint / depositGridCellIntoSplitPoint. - Remove workerSlot.used field (write-only, never read). - Document --experimental.parallel-commitment in the gitbook reference and note its precedence over --experimental.concurrent-commitment.
ParallelPatriciaHashed was returning workers to its pool via hph.Reset(), which only clears root state — activeRows, depths, branchBefore, touchMap, afterMap, currentKey, and the warmup cache all survived. Across Process calls without an explicit ParallelPatriciaHashed.Reset(), a recycled worker would start its next followAndUpdate with stale grid state. Switch to resetForReuse() at all three pool-return sites (cleanupAll, the per-task Get, and foldDrainWithBarrier's defer). The warmuper was started but its cache pointer was never propagated to the workers — workers inherited trace/traceDomain/enableWarmupCache only and re-read every branch from the DB. Expose warmuper.Cache() on the template when EnableWarmupCache is set, and have each worker pick it up alongside its branchEncoder before runNibbleBucket dispatches keys.
Fail loudly when ParallelPatriciaHashed is combined with deferred commitment updates (fork validation + parallel block apply paths). Previously the parallel trie applied worker-accumulated deferred branch updates inline at the end of Process, bypassing the deferred-flush mechanism and silently breaking per-block changeset attribution.
- commitmentdb: handle *ParallelPatriciaHashed in trace state capture so trie-trace recordings include the pre-Process internal state for replay instead of starting from empty. - db/state/squeeze: rebuildCommitmentShard selects VariantParallelHexPatricia and enables ParaTrieDB when ExperimentalParallelCommitment is set, mirroring the concurrent path so the flag is honored during snapshot rebuild. - commitment: clear pu.deferredCombined and pendingRoot on the errgroup-wait failure path so a retry on the same Updates buffer cannot re-apply stale worker deferred branches or surface a never-persisted root via RootHash(). - commitment: thread logPrefix into Process error wrapping so staged-sync callers can correlate parallel-commitment failures with stage logs.
New benchmark file `parallel_patricia_hashed_bench_test.go` defines two deterministic corpus builders (100K accounts-only, 500K storage-heavy), two bench helpers that bracket measured Process() with StopTimer/StartTimer, and a top-level `Benchmark_Commitment_DirectVsParallel` driver that sub-benches by corpus and worker count. Smoke run + lint deferred to Task 2.
Smoke-run executed successfully on Apple M2 Max: - All 10 sub-benches passed with non-zero ns/op - Package tests pass (go test ./execution/commitment/...) - make lint clean (2 runs)
Append measured ratios from Benchmark_Commitment_DirectVsParallel to execution/commitment/agents.md (Apple M2 Max, Go 1.25.7): 500K-StorageHeavy peaks at ~2x with 4 workers and plateaus; 100K-AccountsOnly does not benefit from ModeParallel.
…Timer - add b.ReportAllocs() to both bench helpers so B/op and allocs/op are visible; convention across other benches in this package. - drop b.ResetTimer() from the outer per-corpus b.Run dispatchers; they don't time anything themselves (the inner sub-benches manage their own timers via b.Loop()), so the calls were no-ops.
Benchmark sweep across {8, 32, 64, 128, 256} shows m64 strictly dominates
the previous default (32):
100K-AccountsOnly @ NumCPU=8: m32 1.06x -> m64 1.46x faster than Direct
500K-StorageHeavy @ NumCPU=8: m32 1.96x -> m64 1.92x (within noise)
100K-AccountsOnly memory @w12: 5.87 GB -> 3.07 GB (48% less)
m32 was leaving meaningful headroom on flat workloads by emitting too
many fine-grained split-points; barrier coordination overhead exceeded
the parallelism gain. m64 produces fewer, larger leafTasks so per-worker
setup cost amortizes over more useful work. Storage-heavy workloads are
unaffected because storage hash distribution produces a small fixed
number of useful splits per account regardless of threshold.
Tests that hardcoded the previous 32-key threshold are updated to scale
from MinSplitKeys (matching the pattern already used by
TestParallelBarrier_ChainedSplitPoints).
The bench was creating a fresh *ParallelPatriciaHashed per loop iteration and calling Release() at the end. Release() calls resetPool() which drops the entire sync.Pool of HexPatriciaHashed workers. The pool that takes ~1000 hph allocations to fill (16 buckets × ~62 tasks each, running 8 in parallel under NumCPU=8) was being thrown away every iteration, so iter 2-10 paid the same allocation cost as iter 1. Production usage is one long-lived *ParallelPatriciaHashed servicing many blocks — the pool stays warm. The bench should mirror that. After this change, on 500K-StorageHeavy/w8: alloc bytes drop 59% (859MB -> 354MB), time improves ~4%. On 100K-AccountsOnly the wins are larger: w8 alloc bytes -64% and time -7%, w12 alloc -65% and time -14%. The storage-heavy time floor is dominated by something else (CPU profile points at runtime.usleep / scheduler thrashing — separate concern, noted in agents.md).
Resolve conflicts after main's TrieConfig-threading + SharedDomains options refactor: - domain_shared.go: fold ExperimentalParallelCommitment precedence into PickTrieVariant (keep main's options-pattern NewSharedDomains) - commitment_context.go: keep main's warmupBase template; re-add ParallelPatriciaHashed warmup case - adapt NewHexPatriciaHashed / NewParallelPatriciaHashed call sites to the new TrieConfig parameter - InitializeTrieAndUpdates(mode, tmpdir, cfg) signature; branchEncoder.setDeferUpdates now unexported - drop generated docs/gitbook README (removed on main); flag stays self-documenting via its Usage string
Replace "Task N" plan references in parallel-hph doc comments and a runtime error string with descriptions of the actual behavior, per the repo comment-style guidance. Delete the empty, skipped TestParallelProcessSkeleton_MultipleNibblesNoSplit: a leftover skeleton whose multi-bucket-no-split rejection is already covered by TestParallelBarrier_ProcessRejectsMultiBucketWithoutSplit.
|
branch now broken after recent merge from main or after simplification effort. |
- adopt #21380 aggregator-scope BranchCache; drop per-trie WarmupCache plumbing (warmup_cache.go + obsolete tests removed; Warmuper page-cache prefetch kept) - adopt main's GenerateWitness(produceExclusionProofs) signature + witness tests - drop removed VariantBinPatriciaTrie; keep parallel/streaming trie variants
…ent-read SIGSEGV) Hold one BeginTemporalRo on paraTrieDB for the whole parallel/concurrent fold so a background merge cannot reclaim+munmap a .kv that an in-flight worker's TrieContext.Branch read still aliases. common.Copy at the Branch boundary stays (#21630).
The aggregator-scope BranchCache (#21380) is shared across the parallel commitment workers' patriciaContexts and caches values aliasing freed .kv mmap; the mem-batch flush (SharedDomains.Commit memmove) then faults reading an unmapped region. Gate its creation off when parallel/streaming commitment is active. The fold-scoped file-view pin handles the fold-read path separately.
Parallel-commitment crash root cause (validated on mainnet)Correction of the earlier version of this comment. Validating Root cause. The shared mem-batch ( Fix Two earlier partial mitigations, both superseded by the fix above:
Validation. Copy-on-put crosses mainnet block 25320897 and runs on past 25.35M with 0 SIGSEGV and 0 wrong root, now with the BranchCache enabled under |
…se-after-munmap) TemporalMemBatch.putLatest stored the caller's val slice without copying. Under parallel commitment, val can alias a .kv mmap of the foreground exec tx's file generation; sd.mem is read first by every worker's TrieContext (shared SharedDomains), so a concurrent worker (or the commitment flush) reads that pointer while a background merge munmaps the generation -> SIGSEGV in TrieContext.Branch / SharedDomains.Commit. Copy at put time (mmap still mapped) so every later reader gets heap-owned bytes. Copy-on-get under latestStateLock could not fix this (the source was already freed).
… refoldTotal is the live metric)
Copy-on-put (mem-batch owns its bytes) is the actual fix for the parallel-commitment mmap use-after-munmap; the umbrella file-view pin only covered the fold-start generation and was already proven insufficient (crash recurred with it). Worker file reads are covered by each worker's own roTx. Removing the pin; verified on the mainnet repro.
…est under copy-on-put)
|
race-tests
This PR's net diff to all involved files ( |
ParallelPatriciaHashed.Process (non-streaming) now calls flushTrieStateRates(), matching the sequential and streaming paths. The load/skip atomics are bumped by the worker fold/unfold hot path but were never published on a pure-parallel node, so trie_state_load_rate / trie_state_skip_rate stalled. Also correct the subtreeCount doc (it counts distinct keys; re-inserting an existing key merges without bumping) and fix a "seralized" test-message typo. Addresses Copilot review nits on #21709.
| // trie root and a concurrent-read SIGSEGV on mainnet block 25142734. The | ||
| // top-level per-account-nibble mount fold stays parallel; only the second-tier | ||
| // storage split is off until its correctness + file-view pinning is fixed. | ||
| var deepStorageFold = true |
| case ModeParallel: | ||
| if len(hashedKey) == 0 { | ||
| return | ||
| } | ||
| dedupKey := string(hashedKey) | ||
| if _, ok := t.keys[dedupKey]; !ok { | ||
| // Hashed-only touch has no plainKey; the parallel fold rejects such a terminator. | ||
| t.parallel.Insert(hashedKey, nil, nil) | ||
| t.keys[dedupKey] = struct{}{} | ||
| } |
…-fold fix #21945) (#22129) Refreshes `bal-devnet-7_warmup` onto `bal-devnet-7`, pulling in the parallel deep-storage-fold trie-root fix #21945 (and its feature line #21709, #21941). ### `sstore_bloated` MGas/s | variant | base (par-kvi, BAL) | ethrex | now (+#21945) | |---|--:|--:|--:| | `sstore_bloated` (F) — new slots | 92 | 461 | 399 | | `sstore_bloated` (T) — existing slots | 7 | 82 | 30 | ### Notes Rendered as a merge, so review the conflict resolutions rather than the upstream commits: - `commitment_convert.go` / `_blackbox_test.go`: took bal-devnet-7's merged form (#21933) over the pre-merge draft. - `db/seg/decompress.go`: comment-only, took bal-devnet-7 (#21927). - `rawdbreset/reset_stages.go`: combined out-of-tx `ClearTables` with bal-devnet-7's branchCache invalidation. - `stage_execute.go`: kept warmup's `PruneExecutionStage` (`haveMore` signature, required by callers). bal-devnet-7's #20860 prune-timeout budget-sharing is **not** carried into this function. - `cmd/integration/commands/stages.go`: took bal-devnet-7's `stageExec` (drops the removed `--no-commit` flag), adapted its prune calls to the `haveMore` signature. --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: lystopad <oleksandr.lystopad@erigon.tech> Co-authored-by: kewei <kewei.train@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Andrew Ashikhmin <34320705+yperbasis@users.noreply.github.com> Co-authored-by: Alex Sharov <AskAlexSharov@gmail.com> Co-authored-by: lupin012 <58134934+lupin012@users.noreply.github.com> Co-authored-by: Mark Holt <135143369+mh0lt@users.noreply.github.com> Co-authored-by: Mark Holt <erigon@dev-bm-e3-ethmainnet-n4.erigon.io> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: awskii <awskii@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: erigon-copilot[bot] <265817861+erigon-copilot[bot]@users.noreply.github.com> Co-authored-by: erigon-copilot[bot] <erigon-copilot[bot]@users.noreply.github.com> Co-authored-by: Giulio Rebuffo <giulio.rebuffo@gmail.com> Co-authored-by: Ilya Mikheev <54912776+JkLondon@users.noreply.github.com> Co-authored-by: JkLondon <me@ilyamikheev.com> Co-authored-by: bloxster <bloxster@proton.me> Co-authored-by: Bloxster <gianni.morselli@erigon.tech> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: yperbasis <andrey.ashikhmin@gmail.com> Co-authored-by: noop <noop@noop> Co-authored-by: Sudeep Kumar <sudeep.kumar@erigon.tech> Co-authored-by: bloxster <bloxster@users.noreply.github.com> Co-authored-by: info@weblogix.biz <admin@10gbps.weblogix.it> Co-authored-by: Sahil Sojitra <88416181+Sahil-4555@users.noreply.github.com> Co-authored-by: awskii <artem.tsskiy@gmail.com> Co-authored-by: Michelangelo Riccobene <michelangelo.riccobene@gmail.com> Co-authored-by: Matt Joiner <anacrolix@gmail.com> Co-authored-by: milen <94537774+taratorio@users.noreply.github.com>
Draft / checkpoint — streaming + parallel commitment trie, with
mainmerged in and the parallel-path stability bugs fixed and validated on mainnet.What
Adds
StreamingCommitter, a commitment engine that overlaps trie folding with block execution (touched keys folded into per-top-nibble splits during execution; root stitched at block end). Selected via--experimental.streaming-commitment(precedence: streaming > parallel > concurrent). The branch stacks concurrent → parallel → streaming; streaming reuses the parallel engine's prefix-trie / split machinery and delegatesProcessto the committer.Stability fixes (parallel/streaming)
Live mainnet validation surfaced a deterministic wrong root and two
.kvmmap use-after-munmap SIGSEGVs, all now fixed:TemporalMemBatch.putLateststored values without copying; under parallel commitment they alias a.kvmmap of the foreground exec tx's file generation, which a background merge munmaps mid-fold.sd.memis read first by every worker'sTrieContext(sharedSharedDomains), so a concurrent worker (TrieContext.Branch) or the commit flush (SharedDomains.Commit) reads the freed pointer → fault. Fix: copy-on-put sosd.memowns heap bytes (8196bac851). (Copy-on-get underlatestStateLockcan't fix this — the source is already munmapped before the copy runs.)6fadc5d9aa), and gating main's State Cache Consolidation (PR #1 of the perf stack) #21380 BranchCache off under parallel/streaming (1fc7e2a605) — it gives ~nothing under parallel (which warms itself) and complicates shared-context lifetimes.Status
ModeDirect/ModeParallelacross multi-depth, incremental storage collapse (partial + full delete), and whale corpora;-race -countclean.make lintclean;make erigon integrationbuilds.Perf (1M-whale benchmark, 18 cores)
Known follow-ups
DomainPuton the exec hot path (bounded; same order as what the state/branch caches already pay).