Skip to content

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

Merged
masih merged 14 commits into
mainfrom
masih/1789163252-tmhash-simd-merkle
Sep 16, 2026
Merged

masih merged 14 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 16, 2026, 2:39 PM

@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      2.786µ ± 1%
MixIn-16       343.8n ± 0%
MixOut-16      344.1n ± 0%
HashKV-16      3.127µ ± 1%
HashChunk-16   6.404m ± 0%
geomean        5.806µ

          │   default    │
          │     B/s      │
Expand-16   701.0Mi ± 4%

@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.39%. Comparing base (33259ec) to head (3e42a4d).
⚠️ Report is 1 commits behind head on main.

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.69%   65.39%   -1.31%     
==========================================
  Files        2203     2082     -121     
  Lines      169886   157219   -12667     
==========================================
- Hits       113310   102815   -10495     
+ Misses      56435    54263    -2172     
  Partials      141      141              
Flag Coverage Δ
sei-chain-pr 76.61% <82.22%> (?)
sei-db 74.50% <ø> (ø)
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 213 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          258.9µ ± 1%
HashFromByteSlices/leaves=1024/leaf=512-16         575.3µ ± 0%
HashFromByteSlices/leaves=100/leaf=32-16           25.08µ ± 1%
HashFromByteSlicesBatched/leaves=1024/leaf=32-16   260.5µ ± 8%
geomean                                            176.6µ

                                                 │   default    │
                                                 │     B/s      │
HashFromByteSlices/leaves=1024/leaf=32-16          120.7Mi ± 1%
HashFromByteSlices/leaves=1024/leaf=512-16         869.1Mi ± 0%
HashFromByteSlices/leaves=100/leaf=32-16           121.7Mi ± 3%
HashFromByteSlicesBatched/leaves=1024/leaf=32-16   120.3Mi ± 8%
geomean                                            198.0Mi

pkg: github.com/sei-protocol/sei-chain/sei-tendermint/crypto/tmhash
                   │   default   │
                   │   sec/op    │
SumBatchInner-16     135.3µ ± 0%
SumBatchLeaf256-16   249.8µ ± 0%
SumBatchLeaf1K-16    701.6µ ± 0%
geomean              287.3µ

                   │    default    │
                   │      B/s      │
SumBatchInner-16      469.1Mi ± 0%
SumBatchLeaf256-16   1004.7Mi ± 0%
SumBatchLeaf1K-16     1.393Gi ± 0%
geomean               876.1Mi

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

LtHash default vs SIMD (ubuntu-latest)

CPU: AMD EPYC 7763 64-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 7763 64-Core Processor                
            │   default   │
            │   sec/op    │
Expand-4      3.115µ ± 0%
MixIn-4       384.2n ± 0%
MixOut-4      383.6n ± 0%
HashKV-4      3.509µ ± 0%
HashChunk-4   7.155m ± 0%
geomean       6.491µ

         │   default    │
         │     B/s      │
Expand-4   627.1Mi ± 1%

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

tmhash / merkle default vs SIMD (ubuntu-latest)

CPU: AMD EPYC 9V45 96-Core Processor

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: AMD EPYC 9V45 96-Core Processor                
                                                │   default   │                simd                │
                                                │   sec/op    │   sec/op     vs base               │
HashFromByteSlices/leaves=1024/leaf=32-4          196.9µ ± 2%   101.1µ ± 1%  -48.62% (p=0.000 n=8)
HashFromByteSlices/leaves=1024/leaf=512-4         468.2µ ± 1%   302.0µ ± 1%  -35.49% (p=0.000 n=8)
HashFromByteSlices/leaves=100/leaf=32-4           19.41µ ± 3%   12.50µ ± 2%  -35.60% (p=0.000 n=8)
HashFromByteSlicesBatched/leaves=1024/leaf=32-4   184.3µ ± 2%   100.7µ ± 2%  -45.34% (p=0.000 n=8)
geomean                                           134.8µ        78.75µ       -41.56%

                                                │   default    │                simd                 │
                                                │     B/s      │     B/s       vs base               │
HashFromByteSlices/leaves=1024/leaf=32-4          158.7Mi ± 2%   309.0Mi ± 4%  +94.65% (p=0.000 n=8)
HashFromByteSlices/leaves=1024/leaf=512-4         1.043Gi ± 3%   1.617Gi ± 2%  +55.03% (p=0.000 n=8)
HashFromByteSlices/leaves=100/leaf=32-4           157.2Mi ± 6%   244.2Mi ± 3%  +55.29% (p=0.000 n=8)
HashFromByteSlicesBatched/leaves=1024/leaf=32-4   169.5Mi ± 4%   310.2Mi ± 2%  +82.96% (p=0.000 n=8)
geomean                                           259.3Mi        443.6Mi       +71.11%

pkg: github.com/sei-protocol/sei-chain/sei-tendermint/crypto/tmhash
                  │   default    │                simd                │
                  │    sec/op    │   sec/op     vs base               │
SumBatchInner-4     105.38µ ± 1%   57.33µ ± 3%  -45.60% (p=0.000 n=8)
SumBatchLeaf256-4    206.0µ ± 5%   129.8µ ± 3%  -37.01% (p=0.000 n=8)
SumBatchLeaf1K-4     599.1µ ± 2%   424.3µ ± 1%  -29.17% (p=0.000 n=8)
geomean              235.2µ        146.7µ       -37.62%

                  │   default    │                 simd                 │
                  │     B/s      │      B/s       vs base               │
SumBatchInner-4     602.3Mi ± 2%   1107.2Mi ± 3%  +83.82% (p=0.000 n=8)
SumBatchLeaf256-4   1.190Gi ± 6%    1.888Gi ± 5%  +58.74% (p=0.000 n=8)
SumBatchLeaf1K-4    1.632Gi ± 2%    2.304Gi ± 2%  +41.19% (p=0.000 n=8)
geomean             1.045Gi         1.676Gi       +60.31%

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

Medium Risk
Touches consensus-relevant Merkle root computation, but digests are gated behind extensive differential tests and the default build keeps single-lane hashing unless SIMD is enabled.

Overview
Introduces sei-tendermint/crypto/tmhash, a pluggable batched SHA-256 layer (SumBatch, SEI_TMHASH_BACKEND, optional 16-lane AVX-512 under GOEXPERIMENT=simd) with scalar fallback and VZEROUPPER before legacy SHA-NI.

merkle.HashFromByteSlices now uses a level-by-level batched RFC-6962 tree when the active backend exposes more than one lane and there are at least that many leaves; otherwise behavior stays on the recursive crypto/sha256 path. Equivalence tests, fuzz, and benchmarks cover backends and the batched tree.

CI replaces lthash-bench.yml with simd-hash-bench.yml, keeping LtHash benches and adding a bench-tmhash job that tests tmhash/merkle with and without GOEXPERIMENT=simd and posts benchstat (plain build baseline vs SIMD build to avoid ZMM preemption skew).

Reviewed by Cursor Bugbot for commit 3e42a4d. 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/simd-hash-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/simd-hash-bench.yml
Comment thread sei-tendermint/crypto/tmhash/backend_test.go Outdated
@masih
masih enabled auto-merge September 16, 2026 14:14
Comment thread .github/workflows/simd-hash-bench.yml
Comment thread .github/workflows/simd-hash-bench.yml
Comment thread sei-tendermint/crypto/tmhash/backend_simd_amd64.go Outdated
@github-actions

github-actions Bot commented Sep 16, 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      2.792µ ±  2%
MixIn-16       390.2n ± 12%
MixOut-16      389.1n ± 12%
HashKV-16      3.198µ ±  4%
HashChunk-16   6.461m ±  1%
geomean        6.145µ

          │   default    │
          │     B/s      │
Expand-16   699.6Mi ± 2%

@github-actions

github-actions Bot commented Sep 16, 2026

Copy link
Copy Markdown

LtHash default vs SIMD (ubuntu-latest)

CPU: AMD EPYC 9V45 96-Core Processor

HashChunk is the end-to-end per-block path; vs base is simd relative to default.

goos: linux
goarch: amd64
pkg: github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv/lthash
cpu: AMD EPYC 9V45 96-Core Processor                
            │   default    │                simd                │
            │    sec/op    │   sec/op     vs base               │
Expand-4      3750.0n ± 0%   793.7n ± 0%  -78.83% (p=0.000 n=8)
MixIn-4       232.45n ± 0%   16.05n ± 1%  -93.10% (p=0.000 n=8)
MixOut-4      220.95n ± 0%   16.13n ± 1%  -92.70% (p=0.000 n=8)
HashKV-4      3967.5n ± 0%   818.9n ± 1%  -79.36% (p=0.000 n=8)
HashChunk-4    8.047m ± 0%   1.724m ± 1%  -78.58% (p=0.000 n=8)
geomean        5.725µ        780.7n       -86.36%

         │   default    │                 simd                  │
         │     B/s      │      B/s       vs base                │
Expand-4   520.9Mi ± 2%   2460.8Mi ± 1%  +372.45% (p=0.000 n=8)

@devin-ai-integration

Copy link
Copy Markdown
Contributor

A note on byte formats, since this PR touches the hashing path of consensus data.

Wire/hash format is unchanged. Every backend computes plain FIPS 180-4 SHA-256 over exactly the same bytes as before: a leaf hashes 0x00 || leaf, an inner node hashes 0x01 || left || right, and HashFromByteSlices returns the same 32-byte big-endian digest for any input. The batched tree is just a different evaluation order of the same RFC 6962 shape, so it pairs the same nodes the recursive getSplitPoint implementation pairs. Nothing serialized, stored, signed or gossiped changes, which is what TestBatchedTreeMatchesRecursive (totals 1–130, leaf sizes 0/2/32/128) and the tmhash differential tests against crypto/sha256 pin down.

In-register layout (SIMD backend only, never leaves the process). The kernel does not change the SHA-256 message format either; it only changes how sixteen messages sit in registers while being hashed:

  • Each lane's message is assembled block by block as a 64-byte row: prefix || msg, the 0x80 terminator, zero fill, and in the final block the 64-bit big-endian bit length in bytes 56–63 — the standard SHA-256 padding, just built with vector ops (prefixShift for the prefix splice, pad80 for the terminator, lens for the length).
  • The sixteen rows are byte-swapped into big-endian 32-bit words (bswapRows) and transposed (transposeBlock) so the kernel sees blk[word][lane]: register w holds message word w of all sixteen lanes. That is what lets one Uint32x16 instruction stream run sixteen independent compressions.
  • After the last block the eight state words are read back per lane and written big-endian into out[i], producing the identical [32]byte digest the scalar path produces.

Messages are grouped by padded block count so each kernel call sees equal-length lanes; anything that does not fill a group of sixteen, or exceeds simdMaxBlocks, goes to the scalar crypto/sha256 path after vzeroupper().

@masih
masih added this pull request to the merge queue Sep 16, 2026
Merged via the queue into main with commit ca358aa Sep 16, 2026
80 of 82 checks passed
@masih
masih deleted the masih/1789163252-tmhash-simd-merkle branch September 16, 2026 15:20
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.

3 participants