Skip to content

Add runtime-selected 16-lane SHA-256 backend for Merkle hashing - #4157

Open
masih wants to merge 11 commits into
mainfrom
masih/1789163252-tmhash-simd-merkle
Open

masih wants to merge 11 commits into
mainfrom
masih/1789163252-tmhash-simd-merkle

Conversation

@masih

@masih masih commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Tendermint's Merkle hashing (merkle.HashFromByteSlices: tx hashes, part sets, commit signatures, results, validator sets) hashes every leaf and every tree level as independent SHA-256 calls, which is the batch shape a multi-lane kernel wants. crypto/sha256 already uses single-lane SHA-NI, so the achievable win is smaller than for LtHash (#4151) and had to be measured rather than assumed.

This adds sei-tendermint/crypto/tmhash with the same runtime-selected backend pattern as #4151. SumBatch(prefix, msgs, out) is served by a default backend (a reused sha256.New(), always compiled) or, under GOEXPERIMENT=simd on a CPU with AVX-512F/VBMI/VBMI2, by a generated 16-lane archsimd.Uint32x16 SHA-256 kernel that loads, prefixes, pads and transposes sixteen messages in-register and falls back to scalar for remainders and mixed lengths; SEI_TMHASH_BACKEND=default pins the portable path. HashFromByteSlices keeps its signature and, when a multi-lane backend is active and there are at least sixteen leaves, builds the tree level by level, pairing adjacent nodes and carrying an odd trailing node up, which is the RFC 6962 getSplitPoint shape. A differential test checks totals 1 to 130 against the recursive implementation, and the tmhash tests check every backend against crypto/sha256 across block and padding boundaries, so the output is byte-identical.

Two Go 1.27 findings are handled here. The compiler never emits VZEROUPPER after archsimd code, so the legacy-SSE SHA-NI path that followed ran several times slower with dirty ZMM state; the SIMD backend calls a one-instruction assembly vzeroupper before handing off. Separately, with GOEXPERIMENT=simd on an AVX-512 machine the runtime's async preemption restores the ZMM registers without VZEROUPPER, which slowed all SHA-NI code in the same binary 2 to 4x; the CI job therefore takes the default column from a plain build and the SIMD column from the experiment build. That second effect applies to every legacy-SSE path in the process and should weigh on any decision to ship a GOEXPERIMENT=simd binary.

On an Intel Xeon Platinum 8559C (benchstat, n=8) the kernel is 2.0x faster than SHA-NI on 1024 inner nodes (124 µs to 61 µs), 1.6x on 256-byte leaves and 1.3x on 1 KiB leaves; the whole 1024 x 32-byte-leaf tree goes from 223 µs to 122 µs. This is a per-block cost of a few thousand hashes, so the node-level effect is modest. The SIMD hash backends workflow runs both packages' tests with and without the experiment and posts the benchstat table as a job summary and PR comment.

Adds sei-tendermint/crypto/tmhash with a default crypto/sha256 backend and a
16-lane AVX-512 kernel built under goexperiment.simd, and routes
merkle.HashFromByteSlices through level-batched hashing when a multi-lane
backend is active. Output is byte-identical to the recursive tree.
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedSep 15, 2026, 9:06 AM

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

LtHash default vs SIMD (uci-default)

CPU: AMD EPYC 7R13 Processor

This runner CPU lacks AVX-512F + VBMI2, so only the default backend ran.

goos: linux
goarch: amd64
pkg: github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv/lthash
cpu: AMD EPYC 7R13 Processor
             │   default    │
             │    sec/op    │
Expand-16      3.220µ ± 14%
MixIn-16       343.7n ±  1%
MixOut-16      354.5n ± 12%
HashKV-16      3.786µ ±  9%
HashChunk-16   7.113m ±  3%
geomean        6.380µ

          │    default    │
          │      B/s      │
Expand-16   607.1Mi ± 12%

@codecov

codecov Bot commented Sep 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.22222% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.40%. Comparing base (b5fc29c) to head (38fbc9c).

Files with missing lines Patch % Lines
sei-tendermint/crypto/tmhash/backend_default.go 58.33% 5 Missing ⚠️
sei-tendermint/crypto/tmhash/backend.go 83.33% 2 Missing ⚠️
sei-tendermint/crypto/merkle/tree.go 95.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #4157      +/-   ##
==========================================
- Coverage   66.68%   65.40%   -1.28%     
==========================================
  Files        2201     2082     -119     
  Lines      169523   157308   -12215     
==========================================
- Hits       113039   102891   -10148     
+ Misses      56343    54276    -2067     
  Partials      141      141              
Flag Coverage Δ
sei-chain-pr 76.61% <82.22%> (?)
sei-db 74.50% <ø> (-0.25%) ⬇️
sei-db-state-db ?

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
sei-tendermint/crypto/tmhash/backend_nosimd.go 100.00% <100.00%> (ø)
sei-tendermint/crypto/merkle/tree.go 94.54% <95.00%> (+0.25%) ⬆️
sei-tendermint/crypto/tmhash/backend.go 83.33% <83.33%> (ø)
sei-tendermint/crypto/tmhash/backend_default.go 58.33% <58.33%> (ø)

... and 201 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

tmhash / merkle default vs SIMD (uci-default)

CPU: AMD EPYC 7R13 Processor

This runner CPU lacks AVX-512F + VBMI + VBMI2, so only the default backend ran.

goos: linux
goarch: amd64
pkg: github.com/sei-protocol/sei-chain/sei-tendermint/crypto/merkle
cpu: AMD EPYC 7R13 Processor
                                                 │   default   │
                                                 │   sec/op    │
HashFromByteSlices/leaves=1024/leaf=32-16          262.7µ ± 1%
HashFromByteSlices/leaves=1024/leaf=512-16         579.4µ ± 0%
HashFromByteSlices/leaves=100/leaf=32-16           25.61µ ± 2%
HashFromByteSlicesBatched/leaves=1024/leaf=32-16   261.2µ ± 7%
geomean                                            178.6µ

                                                 │   default    │
                                                 │     B/s      │
HashFromByteSlices/leaves=1024/leaf=32-16          119.0Mi ± 1%
HashFromByteSlices/leaves=1024/leaf=512-16         863.0Mi ± 0%
HashFromByteSlices/leaves=100/leaf=32-16           119.2Mi ± 3%
HashFromByteSlicesBatched/leaves=1024/leaf=32-16   119.9Mi ± 8%
geomean                                            195.7Mi

pkg: github.com/sei-protocol/sei-chain/sei-tendermint/crypto/tmhash
                   │   default   │
                   │   sec/op    │
SumBatchInner-16     135.7µ ± 0%
SumBatchLeaf256-16   250.7µ ± 1%
SumBatchLeaf1K-16    706.1µ ± 0%
geomean              288.5µ

                   │    default    │
                   │      B/s      │
SumBatchInner-16      467.9Mi ± 0%
SumBatchLeaf256-16   1001.1Mi ± 1%
SumBatchLeaf1K-16     1.384Gi ± 0%
geomean               872.4Mi

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

LtHash default vs SIMD (ubuntu-latest)

CPU: AMD EPYC 9V74 80-Core Processor

This runner CPU lacks AVX-512F + VBMI2, so only the default backend ran.

goos: linux
goarch: amd64
pkg: github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv/lthash
cpu: AMD EPYC 9V74 80-Core Processor                
            │   default   │
            │   sec/op    │
Expand-4      3.657µ ± 3%
MixIn-4       459.6n ± 1%
MixOut-4      460.1n ± 0%
HashKV-4      4.107µ ± 1%
HashChunk-4   8.480m ± 1%
geomean       7.692µ

         │   default    │
         │     B/s      │
Expand-4   534.1Mi ± 6%

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

tmhash / merkle default vs SIMD (ubuntu-latest)

CPU: INTEL(R) XEON(R) PLATINUM 8573C

HashFromByteSlices is the end-to-end Merkle root; vs base is simd relative to default (SHA-NI).

goos: linux
goarch: amd64
pkg: github.com/sei-protocol/sei-chain/sei-tendermint/crypto/merkle
cpu: INTEL(R) XEON(R) PLATINUM 8573C
                                                │   default   │                simd                │
                                                │   sec/op    │   sec/op     vs base               │
HashFromByteSlices/leaves=1024/leaf=32-4          290.7µ ± 1%   152.6µ ± 1%  -47.51% (p=0.000 n=8)
HashFromByteSlices/leaves=1024/leaf=512-4         670.9µ ± 0%   443.1µ ± 1%  -33.95% (p=0.000 n=8)
HashFromByteSlices/leaves=100/leaf=32-4           28.21µ ± 1%   19.24µ ± 0%  -31.78% (p=0.000 n=8)
HashFromByteSlicesBatched/leaves=1024/leaf=32-4   268.8µ ± 0%   151.7µ ± 1%  -43.55% (p=0.000 n=8)
geomean                                           196.1µ        118.5µ       -39.55%

                                                │   default    │                 simd                 │
                                                │     B/s      │      B/s       vs base               │
HashFromByteSlices/leaves=1024/leaf=32-4          107.5Mi ± 0%    204.8Mi ± 5%  +90.52% (p=0.000 n=8)
HashFromByteSlices/leaves=1024/leaf=512-4         745.3Mi ± 1%   1128.5Mi ± 1%  +51.41% (p=0.000 n=8)
HashFromByteSlices/leaves=100/leaf=32-4           108.2Mi ± 1%    158.6Mi ± 0%  +46.58% (p=0.000 n=8)
HashFromByteSlicesBatched/leaves=1024/leaf=32-4   116.3Mi ± 1%    205.9Mi ± 1%  +77.13% (p=0.000 n=8)
geomean                                           178.2Mi         294.7Mi       +65.43%

pkg: github.com/sei-protocol/sei-chain/sei-tendermint/crypto/tmhash
                  │   default    │                simd                │
                  │    sec/op    │   sec/op     vs base               │
SumBatchInner-4     154.88µ ± 0%   82.53µ ± 1%  -46.71% (p=0.000 n=8)
SumBatchLeaf256-4    291.7µ ± 0%   191.1µ ± 0%  -34.47% (p=0.000 n=8)
SumBatchLeaf1K-4     829.2µ ± 0%   628.5µ ± 1%  -24.21% (p=0.000 n=8)
geomean              334.6µ        214.8µ       -35.80%

                  │   default    │                 simd                 │
                  │     B/s      │      B/s       vs base               │
SumBatchInner-4     409.8Mi ± 0%    769.1Mi ± 1%  +87.67% (p=0.000 n=8)
SumBatchLeaf256-4   860.4Mi ± 0%   1313.0Mi ± 0%  +52.61% (p=0.000 n=8)
SumBatchLeaf1K-4    1.179Gi ± 0%    1.555Gi ± 1%  +31.94% (p=0.000 n=8)
geomean             752.2Mi         1.144Gi       +55.76%

Base automatically changed from masih/1789147125-lthash-simd-backend to main September 14, 2026 16:49
revofusion pushed a commit to revofusion/sei-chain that referenced this pull request Sep 15, 2026
…otocol#4164)

Follow-up to sei-protocol#4151. The Go compiler emits no `VZEROUPPER` after
`archsimd` AVX-512 code, so the LtHash SIMD kernels returned to their
callers with dirty upper ZMM halves. Any legacy-SSE code that runs next
(`memmove`, SHA-NI, encoding helpers) then pays an upper-state merge
penalty until the registers are cleared; a throwaway benchmark of
`Expand` followed by `sha256.Sum256` over the 2 KiB serialisation goes
4.36 µs → 3.69 µs (-15%) on an Intel Xeon 8559C once the guard is in
place, while the package's own benchmarks are neutral (MixIn +~1 ns, the
cost of the instruction itself).

This adds a package-local `vzeroupper()` asm stub, the same shape as the
one in sei-protocol#4157, and calls it from thin wrappers registered in
`simdBackend()`, the one place every SIMD entry point passes through, so
a kernel added later cannot skip it. Hash output is unchanged and the
differential tests pass under both builds; the stub is only built under
`goexperiment.simd && amd64`.
@masih
masih marked this pull request as ready for review September 15, 2026 08:03
@cursor

cursor Bot commented Sep 15, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Changes how Merkle roots are computed on hot consensus paths (e.g. results hashes); a digest mismatch would break consensus, though fuzz and cross-backend tests aim to keep outputs byte-identical to crypto/sha256.

Overview
Introduces sei-tendermint/crypto/tmhash, a pluggable batch SHA-256 API (SumBatch) with a default scalar backend and an optional 16-lane AVX-512 backend under GOEXPERIMENT=simd, selectable via SEI_TMHASH_BACKEND. The SIMD path includes padding/prefix handling, a generated sha256x16 kernel, and VZEROUPPER before falling back to scalar so SHA-NI is not penalized.

merkle.HashFromByteSlices now routes to a level-batched builder when the active backend exposes multiple lanes and there are enough leaves, batching each tree level through tmhash.SumBatch while preserving RFC-6962 roots (differential tests vs the recursive implementation).

CI renames the workflow to SIMD hash backends, widens path triggers, and adds a bench-tmhash job that runs tests with/without the experiment, benchmarks default vs SIMD using separate builds (to avoid ZMM preemption skewing the baseline), and posts benchstat to the PR.

Reviewed by Cursor Bugbot for commit 38fbc9c. Bugbot is set up for automated code reviews on this repo. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 2a05ac8. Configure here.

Comment thread .github/workflows/lthash-bench.yml

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adds a runtime-selected 16-lane AVX-512 SHA-256 backend for batched Merkle hashing plus a level-batched HashFromByteSlices; the tree shape, padding, index bucketing and pooled-scratch reuse all check out, and the production default path is unchanged outside GOEXPERIMENT=simd builds. Two non-blocking items: the new benchmark job pools two different builds into one default baseline, and the mixed-size differential test never exercises the over-simdMaxBlocks fallback alongside filled SIMD lanes.

Findings: 0 blocking | 2 non-blocking | 2 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • None at the file/PR level.
  • 2 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread .github/workflows/lthash-bench.yml
Comment thread sei-tendermint/crypto/tmhash/backend_test.go Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant