Skip to content

Add runtime-selected LtHash backend with AVX-512 Blake3 XOF kernel - #4151

Merged
masih merged 4 commits into
mainfrom
masih/1789147125-lthash-simd-backend
Sep 14, 2026
Merged

masih merged 4 commits into
mainfrom
masih/1789147125-lthash-simd-backend

Conversation

@masih

@masih masih commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Giga's flatkv LtHash spends most of a block's hashing time in the Blake3 XOF that expands each serialized key/value into 2048 bytes, followed by the scalar MixIn/MixOut over 1024 uint16 limbs. Profiling put Blake3 compression at roughly 56% of hashChunk and the two mixes at another 31%. The 32 XOF output blocks of one hash are independent compressions of the same chaining value with different counters, which maps directly onto a 16-lane AVX-512 kernel without any cross-mutation batching, and the limb arithmetic is a plain wrapping add/sub over 32 lanes of uint16.

This change moves the expand/add/sub steps behind a small backend struct selected once at init. The default backend is the existing pooled zeebo/blake3 XOF plus scalar mixing and always builds. A second backend, compiled only under goexperiment.simd && amd64 and enabled at runtime only when archsimd.X86.AVX512() and AVX512VBMI2() report support, runs a generated, fully unrolled 16-lane Blake3 compression using simd/archsimd (VPSHRDD for the rotates, pre-broadcast input rows loaded as vectors to avoid the legacy-SSE cost of Broadcast*) and vectorised limb mixing. Inputs longer than one Blake3 chunk fall back to the default expand. Output is byte-identical to the reference; SEI_LTHASH_BACKEND=default pins the portable path. A new workflow builds and tests the package both with and without the experiment, benchmarks every backend the runner CPU can execute, and writes a benchstat -col /backend comparison to the step summary.

Locally on a Xeon 8559C (AVX-512 + VBMI2), benchstat over 4 runs: Expand 2.61 µs → 1.04 µs, MixIn 213 ns → 18 ns, HashKV 3.13 µs → 1.08 µs, hashChunk (1000 mutations) 5.40 ms → 2.37 ms. Differential tests compare every backend against zeebo/blake3 across block and chunk boundaries (1..5000 bytes), existing lthash tests pass unchanged under both builds with -race, and golangci-lint run is clean with and without GOEXPERIMENT=simd.

@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 11, 2026, 8:49 PM

@codecov

codecov Bot commented Sep 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.61538% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.48%. Comparing base (ac460ac) to head (356d8e3).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
sei-db/state_db/sc/flatkv/lthash/backend.go 50.00% 8 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #4151      +/-   ##
==========================================
- Coverage   66.60%   65.48%   -1.13%     
==========================================
  Files        2196     2090     -106     
  Lines      169188   157791   -11397     
==========================================
- Hits       112692   103325    -9367     
+ Misses      56355    54325    -2030     
  Partials      141      141              
Flag Coverage Δ
sei-db 74.50% <ø> (ø)
sei-db-state-db ?
sei-db-state-db-pr 88.86% <84.61%> (?)

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

Files with missing lines Coverage Δ
...ei-db/state_db/sc/flatkv/lthash/backend_default.go 100.00% <100.00%> (ø)
sei-db/state_db/sc/flatkv/lthash/backend_nosimd.go 100.00% <100.00%> (ø)
sei-db/state_db/sc/flatkv/lthash/lthash.go 85.07% <100.00%> (-4.51%) ⬇️
sei-db/state_db/sc/flatkv/lthash/backend.go 50.00% <50.00%> (ø)

... and 156 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

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.778µ ± 0%
MixIn-16       344.4n ± 0%
MixOut-16      344.8n ± 0%
HashKV-16      3.133µ ± 1%
HashChunk-16   6.384m ± 0%
geomean        5.806µ

          │   default    │
          │     B/s      │
Expand-16   703.2Mi ± 0%

@github-actions

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      3524.0n ± 1%   720.4n ± 0%  -79.56% (p=0.000 n=8)
MixIn-4       208.85n ± 1%   11.59n ± 3%  -94.45% (p=0.000 n=8)
MixOut-4      209.90n ± 2%   11.87n ± 3%  -94.35% (p=0.000 n=8)
HashKV-4      3726.0n ± 0%   742.4n ± 1%  -80.08% (p=0.000 n=8)
HashChunk-4    7.575m ± 0%   1.576m ± 1%  -79.20% (p=0.000 n=8)
geomean        5.344µ        649.9n       -87.84%

         │   default    │                 simd                  │
         │     B/s      │      B/s       vs base                │
Expand-4   554.3Mi ± 2%   2711.4Mi ± 0%  +389.18% (p=0.000 n=8)

@masih
masih marked this pull request as ready for review September 14, 2026 15:41
@cursor

cursor Bot commented Sep 14, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes state-db LtHash digest computation with a new fast path; correctness is heavily tested but any SIMD/backend bug could alter block hashes on AVX-512 hosts unless pinned to default.

Overview
Introduces pluggable LtHash backends so Blake3 XOF expansion and limb MixIn/MixOut can be swapped without changing call sites. lthash.go now routes hash, MixIn, and MixOut through an active backend chosen at init (optional pin via SEI_LTHASH_BACKEND).

The default backend keeps the prior pooled zeebo/blake3 XOF plus scalar add/sub. An optional simd path (GOEXPERIMENT=simd, amd64, AVX-512F + VBMI2) adds a generated 16-lane Blake3 root XOF kernel and AVX-512 uint16 mixing; inputs over one Blake3 chunk still use the default expand. Differential tests assert every compiled backend matches the Blake3 reference across size boundaries.

A new GitHub Actions workflow runs -race tests with and without the simd experiment, benchmarks each available backend, and posts a benchstat default-vs-simd report on PRs.

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

@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.

The backend indirection is clean and the generated 16-lane BLAKE3 root-XOF kernel traces correctly against the spec (flags, chunk-vs-output counter, message permutation, finalize, and the little-endian limb aliasing all check out), with a solid differential test against zeebo/blake3 across block and chunk boundaries. No blocking defects; the notes below are about how the new consensus-critical second implementation is validated in CI and whether it reaches production builds at all.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • [suggestion] No production build path enables the SIMD backend: no Makefile target, Dockerfile, or release workflow in the tree sets GOEXPERIMENT=simd, so shipped seid binaries compile backend_nosimd.go and always run the default path. The speedups in the description are not realized by any current build. Worth stating explicitly in the PR description (or a follow-up) so the change isn't assumed to be live on validators.
  • [suggestion] Nothing logs ActiveBackend() — it is exported but referenced only from backend_test.go. Since two kernels now feed a consensus-relevant hash, a node should record which one it selected at startup (e.g. where the flatkv hash engine is constructed); otherwise, if a divergence ever did occur, there would be no way to tell from logs which nodes ran which kernel. SEI_LTHASH_BACKEND is also undocumented outside the const's godoc.
  • 2 suggestion(s)/nit(s) flagged inline on specific lines.

- name: Test without GOEXPERIMENT=simd
run: go test -count=1 -race ${{ env.LTHASH_PKG }}

- name: Test with GOEXPERIMENT=simd

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] This step passes vacuously when the runner CPU lacks AVX-512F/VBMI2. simdBackend() returns false, so availableBackends() yields only default, and TestBackendsAgreeWithReference / TestBackendsAgreeOnMix degrade to comparing zeebo/blake3 against itself — green, with no signal that the SIMD kernel was never exercised. The ::warning:: at line 98 only covers the benchmark step, and go test here runs without -v, so the missing subtest isn't visible in the log either.

Since the main go-test.yml never sets GOEXPERIMENT=simd, this workflow is the only place the kernel is validated at all, and a consensus-critical hash implementation can reach main untested if neither matrix leg has AVX-512. Consider asserting coverage — e.g. run this step with -v and grep -q 'TestBackendsAgreeWithReference/simd', or emit the same ::warning:: (or fail a dedicated job) when no matrix leg saw simd.


// selectBackend picks the backend named by pin, or the fastest available one
// when pin is empty or unknown.
func selectBackend(pin string) backend {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] An unrecognized SEI_LTHASH_BACKEND value silently falls through to automatic selection. The knob's main operational use is pinning the portable path during a suspected divergence, and a typo (defualt) — or pinning simd on a host without AVX-512 — produces exactly the opposite of what the operator asked for, with no log line and no error to notice it by. Emitting a warning when pin != "" and the name isn't in availableBackends() would make the failure self-reporting.

@masih
masih added this pull request to the merge queue Sep 14, 2026
Merged via the queue into main with commit 33fa177 Sep 14, 2026
92 checks passed
@masih
masih deleted the masih/1789147125-lthash-simd-backend branch 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`.
revofusion pushed a commit to revofusion/sei-chain that referenced this pull request Sep 16, 2026
…protocol#4157)

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
(sei-protocol#4151) and had to be measured rather than assumed.

This adds `sei-tendermint/crypto/tmhash` with the same runtime-selected
backend pattern as sei-protocol#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.
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.

2 participants