From 756e3205200fa5a77c94905771e168cf4d32e575 Mon Sep 17 00:00:00 2001 From: masih Date: Fri, 11 Sep 2026 17:25:30 +0000 Subject: [PATCH 1/3] Add runtime-selected LtHash backend with AVX-512 Blake3 XOF kernel --- .github/workflows/lthash-bench.yml | 88 +++ sei-db/state_db/sc/flatkv/lthash/backend.go | 62 ++ .../sc/flatkv/lthash/backend_default.go | 73 +++ .../sc/flatkv/lthash/backend_nosimd.go | 8 + .../sc/flatkv/lthash/backend_simd_amd64.go | 164 ++++++ .../state_db/sc/flatkv/lthash/backend_test.go | 177 ++++++ .../sc/flatkv/lthash/blake3_xof16_amd64.go | 555 ++++++++++++++++++ .../sc/flatkv/lthash/gen_blake3_xof16.go | 118 ++++ sei-db/state_db/sc/flatkv/lthash/lthash.go | 51 +- 9 files changed, 1248 insertions(+), 48 deletions(-) create mode 100644 .github/workflows/lthash-bench.yml create mode 100644 sei-db/state_db/sc/flatkv/lthash/backend.go create mode 100644 sei-db/state_db/sc/flatkv/lthash/backend_default.go create mode 100644 sei-db/state_db/sc/flatkv/lthash/backend_nosimd.go create mode 100644 sei-db/state_db/sc/flatkv/lthash/backend_simd_amd64.go create mode 100644 sei-db/state_db/sc/flatkv/lthash/backend_test.go create mode 100644 sei-db/state_db/sc/flatkv/lthash/blake3_xof16_amd64.go create mode 100644 sei-db/state_db/sc/flatkv/lthash/gen_blake3_xof16.go diff --git a/.github/workflows/lthash-bench.yml b/.github/workflows/lthash-bench.yml new file mode 100644 index 0000000000..aadeade7aa --- /dev/null +++ b/.github/workflows/lthash-bench.yml @@ -0,0 +1,88 @@ +name: LtHash backends +on: + workflow_dispatch: + pull_request: + paths: + - 'sei-db/state_db/sc/flatkv/lthash/**' + - '.github/workflows/lthash-bench.yml' + push: + branches: + - main + paths: + - 'sei-db/state_db/sc/flatkv/lthash/**' + +concurrency: + cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'push' && github.sha || github.ref }} + +env: + GO_VERSION: '1.27.1' + LTHASH_PKG: ./sei-db/state_db/sc/flatkv/lthash + BENCH_COUNT: 8 + +jobs: + bench: + name: Default vs SIMD + runs-on: uci-default + steps: + # See: https://github.com/actions/checkout/releases/tag/v7.0.0 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + with: + fetch-depth: 1 + + - uses: actions/setup-go@v7 + with: + go-version: ${{ env.GO_VERSION }} + cache: false + + - name: Download modules + run: go mod download + + - name: CPU + run: | + { + echo '## CPU' + echo '```' + lscpu | grep -E 'Model name|^Flags' | sed -E 's/^Flags:\s+/Flags: /' | fold -w 120 + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + + # Both builds must pass: the default build has no SIMD backend compiled + # in, the experiment build adds it and the differential tests compare + # every available backend against the Blake3 reference. + - name: Test without GOEXPERIMENT=simd + run: go test -count=1 -race ${{ env.LTHASH_PKG }} + + - name: Test with GOEXPERIMENT=simd + env: + GOEXPERIMENT: simd + run: go test -count=1 -race ${{ env.LTHASH_PKG }} + + - name: Benchmark every available backend + env: + GOEXPERIMENT: simd + run: | + go test \ + -run '^$' \ + -bench . \ + -count ${{ env.BENCH_COUNT }} \ + ${{ env.LTHASH_PKG }} | tee bench.txt + + - name: Compare backends + run: | + go install golang.org/x/perf/cmd/benchstat@v0.0.0-20250813145418-2f7363a06fe1 + { + echo '## LtHash backends (`benchstat -col /backend`)' + echo + echo 'Only backends the runner CPU can execute appear as columns; the SIMD' + echo 'backend needs AVX-512F + VBMI2. `hashChunk` is the end-to-end per-block path.' + echo + echo '```' + benchstat -col /backend bench.txt + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + + - uses: actions/upload-artifact@v4 + with: + name: lthash-bench + path: bench.txt diff --git a/sei-db/state_db/sc/flatkv/lthash/backend.go b/sei-db/state_db/sc/flatkv/lthash/backend.go new file mode 100644 index 0000000000..58c4819893 --- /dev/null +++ b/sei-db/state_db/sc/flatkv/lthash/backend.go @@ -0,0 +1,62 @@ +package lthash + +import ( + "os" + "sort" +) + +// BackendEnv is the environment variable that pins the hashing backend by +// name. An unknown or empty value leaves the selection automatic. +const BackendEnv = "SEI_LTHASH_BACKEND" + +// backend is one implementation of the LtHash primitives. Every backend must +// produce bit-identical results; they differ only in how fast they get there. +type backend struct { + name string + // expand fills dst with the 2048-byte Blake3 XOF of data, one + // little-endian uint16 per limb. data is never empty. + expand func(data []byte, dst *LtHash) + // add and sub are element-wise mod 2^16 on the limb vectors. + add func(dst, src *LtHash) + sub func(dst, src *LtHash) +} + +var active = selectBackend(os.Getenv(BackendEnv)) + +// ActiveBackend returns the name of the hashing backend in use. +func ActiveBackend() string { + return active.name +} + +// availableBackends returns every backend this binary can run on this CPU, +// keyed by name. +func availableBackends() map[string]backend { + m := map[string]backend{defaultBackend.name: defaultBackend} + if b, ok := simdBackend(); ok { + m[b.name] = b + } + return m +} + +// availableBackendNames returns the names from availableBackends, sorted. +func availableBackendNames() []string { + m := availableBackends() + names := make([]string, 0, len(m)) + for name := range m { + names = append(names, name) + } + sort.Strings(names) + return names +} + +// selectBackend picks the backend named by pin, or the fastest available one +// when pin is empty or unknown. +func selectBackend(pin string) backend { + if b, ok := availableBackends()[pin]; ok { + return b + } + if b, ok := simdBackend(); ok { + return b + } + return defaultBackend +} diff --git a/sei-db/state_db/sc/flatkv/lthash/backend_default.go b/sei-db/state_db/sc/flatkv/lthash/backend_default.go new file mode 100644 index 0000000000..20ff55d757 --- /dev/null +++ b/sei-db/state_db/sc/flatkv/lthash/backend_default.go @@ -0,0 +1,73 @@ +package lthash + +import ( + "encoding/binary" + "sync" + + "github.com/zeebo/blake3" +) + +// defaultBackend is the portable implementation: zeebo/blake3 for the XOF and +// plain Go loops for the limb arithmetic. It is always compiled in. +var defaultBackend = backend{ + name: "default", + expand: expandBlake3, + add: addScalar, + sub: subScalar, +} + +func expandBlake3(data []byte, dst *LtHash) { + hasher := blake3HasherPool.Get().(*blake3.Hasher) + hasher.Reset() + _, _ = hasher.Write(data) + digest := hasher.Digest() + + bufPtr := xofBufferPool.Get().(*[]byte) + output := *bufPtr + _, _ = digest.Read(output) // Blake3 XOF never errors and always fills buffer + blake3HasherPool.Put(hasher) + + for i := 0; i < LtHashSize; i++ { + dst.limbs[i] = binary.LittleEndian.Uint16(output[i*2 : (i+1)*2]) + } + xofBufferPool.Put(bufPtr) +} + +func addScalar(dst, src *LtHash) { + for i := 0; i < LtHashSize; i += 8 { + dst.limbs[i] += src.limbs[i] + dst.limbs[i+1] += src.limbs[i+1] + dst.limbs[i+2] += src.limbs[i+2] + dst.limbs[i+3] += src.limbs[i+3] + dst.limbs[i+4] += src.limbs[i+4] + dst.limbs[i+5] += src.limbs[i+5] + dst.limbs[i+6] += src.limbs[i+6] + dst.limbs[i+7] += src.limbs[i+7] + } +} + +func subScalar(dst, src *LtHash) { + for i := 0; i < LtHashSize; i += 8 { + dst.limbs[i] -= src.limbs[i] + dst.limbs[i+1] -= src.limbs[i+1] + dst.limbs[i+2] -= src.limbs[i+2] + dst.limbs[i+3] -= src.limbs[i+3] + dst.limbs[i+4] -= src.limbs[i+4] + dst.limbs[i+5] -= src.limbs[i+5] + dst.limbs[i+6] -= src.limbs[i+6] + dst.limbs[i+7] -= src.limbs[i+7] + } +} + +var xofBufferPool = sync.Pool{ + New: func() interface{} { + buf := make([]byte, LtHashBytes) + return &buf + }, +} + +var blake3HasherPool = sync.Pool{ + New: func() interface{} { + return blake3.New() + }, +} diff --git a/sei-db/state_db/sc/flatkv/lthash/backend_nosimd.go b/sei-db/state_db/sc/flatkv/lthash/backend_nosimd.go new file mode 100644 index 0000000000..8d71dce513 --- /dev/null +++ b/sei-db/state_db/sc/flatkv/lthash/backend_nosimd.go @@ -0,0 +1,8 @@ +//go:build !(goexperiment.simd && amd64) + +package lthash + +// simdBackend reports that no SIMD backend is compiled into this binary. +func simdBackend() (backend, bool) { + return backend{}, false +} diff --git a/sei-db/state_db/sc/flatkv/lthash/backend_simd_amd64.go b/sei-db/state_db/sc/flatkv/lthash/backend_simd_amd64.go new file mode 100644 index 0000000000..8c689b5caf --- /dev/null +++ b/sei-db/state_db/sc/flatkv/lthash/backend_simd_amd64.go @@ -0,0 +1,164 @@ +//go:build goexperiment.simd && amd64 + +package lthash + +import ( + "encoding/binary" + "math/bits" + "unsafe" + + "simd/archsimd" +) + +//go:generate go run gen_blake3_xof16.go + +// simdBackendName is the name reported by ActiveBackend for the AVX-512 path. +const simdBackendName = "simd" + +// simdBackend returns the AVX-512 backend when the CPU can run it. The XOF +// kernel needs AVX-512F (Uint32x16) and VBMI2 (VPSHRDD rotates); the limb +// arithmetic needs AVX-512BW (Uint16x32). +func simdBackend() (backend, bool) { + if !archsimd.X86.AVX512() || !archsimd.X86.AVX512VBMI2() { + return backend{}, false + } + return backend{ + name: simdBackendName, + expand: expandSIMD, + add: addSIMD, + sub: subSIMD, + }, true +} + +const ( + blake3BlockLen = 64 + blake3ChunkLen = 1024 + + blake3ChunkStart = 1 << 0 + blake3ChunkEnd = 1 << 1 + blake3Root = 1 << 3 +) + +var blake3IV = [8]uint32{ + 0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, + 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19, +} + +// expandSIMD computes the 2048-byte Blake3 XOF of data as two 16-lane root +// compressions. Inputs longer than one chunk need the Blake3 tree, which the +// default backend already implements. +func expandSIMD(data []byte, dst *LtHash) { + if len(data) > blake3ChunkLen { + expandBlake3(data, dst) + return + } + var in xof16Inputs + singleChunkRoot(data, &in) + out := (*[2][16][16]uint32)(unsafe.Pointer(&dst.limbs)) //nolint:gosec // G103: same size, little-endian limb layout + xof16(&in, &out[0]) + for lane := range in[12] { + in[12][lane] = 16 + } + xof16(&in, &out[1]) +} + +// singleChunkRoot compresses all but the last block of a one-chunk message +// into a chaining value and broadcasts the root compression inputs into in. +func singleChunkRoot(data []byte, in *xof16Inputs) { + cv := blake3IV + flags := uint32(blake3ChunkStart) + var block [16]uint32 + for len(data) > blake3BlockLen { + loadBlock(&block, data[:blake3BlockLen]) + out := blake3Compress(&cv, &block, 0, blake3BlockLen, flags) + copy(cv[:], out[:8]) + flags = 0 + data = data[blake3BlockLen:] + } + var last [blake3BlockLen]byte + copy(last[:], data) + loadBlock(&block, last[:]) + flags |= blake3ChunkEnd | blake3Root + + for lane := 0; lane < 16; lane++ { + for i := 0; i < 8; i++ { + in[i][lane] = cv[i] + in[8+i][lane] = blake3IV[i] + } + in[12][lane] = 0 + in[13][lane] = 0 + in[14][lane] = uint32(len(data)) //nolint:gosec // G115: len(data) <= blake3BlockLen + in[15][lane] = flags + for i := 0; i < 16; i++ { + in[16+i][lane] = block[i] + } + } +} + +func loadBlock(block *[16]uint32, b []byte) { + for i := range block { + block[i] = binary.LittleEndian.Uint32(b[4*i:]) + } +} + +// blake3Compress is the scalar Blake3 compression function, returning the +// full 16-word state (only the first 8 words are the chaining value). +func blake3Compress(cv *[8]uint32, block *[16]uint32, counter uint64, blockLen, flags uint32) [16]uint32 { + s := [16]uint32{ + cv[0], cv[1], cv[2], cv[3], cv[4], cv[5], cv[6], cv[7], + blake3IV[0], blake3IV[1], blake3IV[2], blake3IV[3], + uint32(counter), uint32(counter >> 32), blockLen, flags, //nolint:gosec // G115: counter is split into its two 32-bit halves + } + m := *block + for r := 0; r < 7; r++ { + blake3G(&s, 0, 4, 8, 12, m[0], m[1]) + blake3G(&s, 1, 5, 9, 13, m[2], m[3]) + blake3G(&s, 2, 6, 10, 14, m[4], m[5]) + blake3G(&s, 3, 7, 11, 15, m[6], m[7]) + blake3G(&s, 0, 5, 10, 15, m[8], m[9]) + blake3G(&s, 1, 6, 11, 12, m[10], m[11]) + blake3G(&s, 2, 7, 8, 13, m[12], m[13]) + blake3G(&s, 3, 4, 9, 14, m[14], m[15]) + m = [16]uint32{ + m[2], m[6], m[3], m[10], m[7], m[0], m[4], m[13], + m[1], m[11], m[12], m[5], m[9], m[14], m[15], m[8], + } + } + for i := 0; i < 8; i++ { + s[i] ^= s[i+8] + s[i+8] ^= cv[i] + } + return s +} + +func blake3G(s *[16]uint32, a, b, c, d int, mx, my uint32) { + s[a] += s[b] + mx + s[d] = bits.RotateLeft32(s[d]^s[a], -16) + s[c] += s[d] + s[b] = bits.RotateLeft32(s[b]^s[c], -12) + s[a] += s[b] + my + s[d] = bits.RotateLeft32(s[d]^s[a], -8) + s[c] += s[d] + s[b] = bits.RotateLeft32(s[b]^s[c], -7) +} + +const simdLimbVectors = LtHashSize / 32 + +// limbVectors views the limbs as 32-lane vectors; the sizes are identical. +func limbVectors(l *LtHash) *[simdLimbVectors][32]uint16 { + return (*[simdLimbVectors][32]uint16)(unsafe.Pointer(&l.limbs)) //nolint:gosec // G103 +} + +func addSIMD(dst, src *LtHash) { + a, b := limbVectors(dst), limbVectors(src) + for i := range a { + archsimd.LoadUint16x32Array(&a[i]).Add(archsimd.LoadUint16x32Array(&b[i])).StoreArray(&a[i]) + } +} + +func subSIMD(dst, src *LtHash) { + a, b := limbVectors(dst), limbVectors(src) + for i := range a { + archsimd.LoadUint16x32Array(&a[i]).Sub(archsimd.LoadUint16x32Array(&b[i])).StoreArray(&a[i]) + } +} diff --git a/sei-db/state_db/sc/flatkv/lthash/backend_test.go b/sei-db/state_db/sc/flatkv/lthash/backend_test.go new file mode 100644 index 0000000000..4a7a81e20c --- /dev/null +++ b/sei-db/state_db/sc/flatkv/lthash/backend_test.go @@ -0,0 +1,177 @@ +package lthash + +import ( + "encoding/binary" + "fmt" + "math/rand" + "testing" + + "github.com/zeebo/blake3" +) + +// referenceExpand is the specification every backend must match: the first +// 2048 bytes of the Blake3 XOF, read as little-endian uint16 limbs. +func referenceExpand(data []byte) *LtHash { + var out [LtHashBytes]byte + h := blake3.New() + _, _ = h.Write(data) + _, _ = h.Digest().Read(out[:]) + lth := New() + for i := range lth.limbs { + lth.limbs[i] = binary.LittleEndian.Uint16(out[2*i:]) + } + return lth +} + +// expandSizes covers block and chunk boundaries of Blake3, including the +// multi-chunk tree path that the SIMD backend delegates. +var expandSizes = []int{1, 8, 63, 64, 65, 124, 127, 128, 129, 500, 1023, 1024, 1025, 2048, 4096, 5000} + +func TestBackendsAgreeWithReference(t *testing.T) { + rng := rand.New(rand.NewSource(1)) + for name, b := range availableBackends() { + t.Run(name, func(t *testing.T) { + for _, n := range expandSizes { + for iter := 0; iter < 8; iter++ { + data := make([]byte, n) + rng.Read(data) + got := New() + b.expand(data, got) + if want := referenceExpand(data); !got.Equal(want) { + t.Fatalf("expand(%d bytes) differs from the Blake3 reference", n) + } + } + } + }) + } +} + +func TestBackendsAgreeOnMix(t *testing.T) { + rng := rand.New(rand.NewSource(2)) + x, y := New(), New() + for i := range x.limbs { + x.limbs[i] = uint16(rng.Uint32()) + y.limbs[i] = uint16(rng.Uint32()) + } + wantAdd, wantSub := x.Clone(), x.Clone() + addScalar(wantAdd, y) + subScalar(wantSub, y) + for name, b := range availableBackends() { + t.Run(name, func(t *testing.T) { + gotAdd, gotSub := x.Clone(), x.Clone() + b.add(gotAdd, y) + b.sub(gotSub, y) + if !gotAdd.Equal(wantAdd) { + t.Fatal("add differs from scalar") + } + if !gotSub.Equal(wantSub) { + t.Fatal("sub differs from scalar") + } + }) + } +} + +func TestSelectBackend(t *testing.T) { + if got := selectBackend("default").name; got != "default" { + t.Fatalf("pinning default selected %q", got) + } + want := "default" + if simd, ok := simdBackend(); ok { + want = simd.name + } + if got := selectBackend("").name; got != want { + t.Fatalf("automatic selection picked %q, want %q", got, want) + } + if got := selectBackend("no-such-backend").name; got != selectBackend("").name { + t.Fatalf("unknown pin %q should fall back to automatic selection", got) + } + if _, ok := availableBackends()[ActiveBackend()]; !ok { + t.Fatalf("active backend %q is not available", ActiveBackend()) + } +} + +// Benchmarks are keyed by backend so `benchstat -col /backend` places the +// implementations side by side. + +func benchmarkKV() []byte { + rng := rand.New(rand.NewSource(3)) + key := make([]byte, 40) + value := make([]byte, 76) + rng.Read(key) + rng.Read(value) + return serializeKV(key, value) +} + +func forEachBackend(b *testing.B, fn func(b *testing.B, be backend)) { + all := availableBackends() + for _, name := range availableBackendNames() { + be := all[name] + b.Run(fmt.Sprintf("backend=%s", name), func(b *testing.B) { fn(b, be) }) + } +} + +func BenchmarkExpand(b *testing.B) { + data := benchmarkKV() + forEachBackend(b, func(b *testing.B, be backend) { + dst := New() + b.SetBytes(LtHashBytes) + for i := 0; i < b.N; i++ { + be.expand(data, dst) + } + }) +} + +func BenchmarkMixIn(b *testing.B) { + forEachBackend(b, func(b *testing.B, be backend) { + x, y := New(), New() + for i := 0; i < b.N; i++ { + be.add(x, y) + } + }) +} + +func BenchmarkMixOut(b *testing.B) { + forEachBackend(b, func(b *testing.B, be backend) { + x, y := New(), New() + for i := 0; i < b.N; i++ { + be.sub(x, y) + } + }) +} + +// BenchmarkHashKV is one leaf update as hashChunk performs it: expand the +// serialized pair and fold it into an accumulator. +func BenchmarkHashKV(b *testing.B) { + data := benchmarkKV() + forEachBackend(b, func(b *testing.B, be backend) { + acc, h := New(), New() + for i := 0; i < b.N; i++ { + be.expand(data, h) + be.add(acc, h) + } + }) +} + +// BenchmarkHashChunk runs the full mutation pipeline through the active +// backend, switching the active backend for each sub-benchmark. +func BenchmarkHashChunk(b *testing.B) { + rng := rand.New(rand.NewSource(4)) + mutations := make([]KeyMutation, 1000) + for i := range mutations { + key := make([]byte, 40) + last := make([]byte, 76) + value := make([]byte, 76) + rng.Read(key) + rng.Read(last) + rng.Read(value) + mutations[i] = KeyMutation{Key: key, LastValue: last, Value: value} + } + saved := active + defer func() { active = saved }() + forEachBackend(b, func(b *testing.B, be backend) { + active = be + for i := 0; i < b.N; i++ { + hashChunk(mutations) + } + }) +} diff --git a/sei-db/state_db/sc/flatkv/lthash/blake3_xof16_amd64.go b/sei-db/state_db/sc/flatkv/lthash/blake3_xof16_amd64.go new file mode 100644 index 0000000000..487a138138 --- /dev/null +++ b/sei-db/state_db/sc/flatkv/lthash/blake3_xof16_amd64.go @@ -0,0 +1,555 @@ +// Code generated by gen_blake3_xof16.go; DO NOT EDIT. + +//go:build goexperiment.simd && amd64 + +package lthash + +import "simd/archsimd" + +// xof16Inputs holds the 16-lane broadcast of every compression input. +// Rows 0-7 are the chaining value, 8-11 the IV, 12 the block counter base, +// 13 zero (counter high word), 14 the block length, 15 the flags and 16-31 the +// message words. Loading a pre-broadcast row is used instead of +// archsimd.BroadcastUint32x16, which compiles to a legacy-SSE sequence that +// costs an SSE/AVX transition on every call. +type xof16Inputs [32][16]uint32 + +var xof16Lanes = [16]uint32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} + +// rotr32 rotates every lane right by n using VPSHRDD; RotateAllRight is +// emulated with three instructions. +func rotr32(x archsimd.Uint32x16, n uint64) archsimd.Uint32x16 { + return x.ShiftAllRightConcatMod32(x, n) +} + +// xof16 writes output blocks base..base+15 of the root XOF to out, +// out[lane][word]. +func xof16(in *xof16Inputs, out *[16][16]uint32) { + s0 := archsimd.LoadUint32x16Array(&in[0]) + s1 := archsimd.LoadUint32x16Array(&in[1]) + s2 := archsimd.LoadUint32x16Array(&in[2]) + s3 := archsimd.LoadUint32x16Array(&in[3]) + s4 := archsimd.LoadUint32x16Array(&in[4]) + s5 := archsimd.LoadUint32x16Array(&in[5]) + s6 := archsimd.LoadUint32x16Array(&in[6]) + s7 := archsimd.LoadUint32x16Array(&in[7]) + s8 := archsimd.LoadUint32x16Array(&in[8]) + s9 := archsimd.LoadUint32x16Array(&in[9]) + s10 := archsimd.LoadUint32x16Array(&in[10]) + s11 := archsimd.LoadUint32x16Array(&in[11]) + s12 := archsimd.LoadUint32x16Array(&xof16Lanes).Add(archsimd.LoadUint32x16Array(&in[12])) + s13 := archsimd.LoadUint32x16Array(&in[13]) + s14 := archsimd.LoadUint32x16Array(&in[14]) + s15 := archsimd.LoadUint32x16Array(&in[15]) + m0 := archsimd.LoadUint32x16Array(&in[16]) + m1 := archsimd.LoadUint32x16Array(&in[17]) + m2 := archsimd.LoadUint32x16Array(&in[18]) + m3 := archsimd.LoadUint32x16Array(&in[19]) + m4 := archsimd.LoadUint32x16Array(&in[20]) + m5 := archsimd.LoadUint32x16Array(&in[21]) + m6 := archsimd.LoadUint32x16Array(&in[22]) + m7 := archsimd.LoadUint32x16Array(&in[23]) + m8 := archsimd.LoadUint32x16Array(&in[24]) + m9 := archsimd.LoadUint32x16Array(&in[25]) + m10 := archsimd.LoadUint32x16Array(&in[26]) + m11 := archsimd.LoadUint32x16Array(&in[27]) + m12 := archsimd.LoadUint32x16Array(&in[28]) + m13 := archsimd.LoadUint32x16Array(&in[29]) + m14 := archsimd.LoadUint32x16Array(&in[30]) + m15 := archsimd.LoadUint32x16Array(&in[31]) + // round 0 + s0 = s0.Add(s4).Add(m0) + s12 = rotr32(s12.Xor(s0), 16) + s8 = s8.Add(s12) + s4 = rotr32(s4.Xor(s8), 12) + s0 = s0.Add(s4).Add(m1) + s12 = rotr32(s12.Xor(s0), 8) + s8 = s8.Add(s12) + s4 = rotr32(s4.Xor(s8), 7) + s1 = s1.Add(s5).Add(m2) + s13 = rotr32(s13.Xor(s1), 16) + s9 = s9.Add(s13) + s5 = rotr32(s5.Xor(s9), 12) + s1 = s1.Add(s5).Add(m3) + s13 = rotr32(s13.Xor(s1), 8) + s9 = s9.Add(s13) + s5 = rotr32(s5.Xor(s9), 7) + s2 = s2.Add(s6).Add(m4) + s14 = rotr32(s14.Xor(s2), 16) + s10 = s10.Add(s14) + s6 = rotr32(s6.Xor(s10), 12) + s2 = s2.Add(s6).Add(m5) + s14 = rotr32(s14.Xor(s2), 8) + s10 = s10.Add(s14) + s6 = rotr32(s6.Xor(s10), 7) + s3 = s3.Add(s7).Add(m6) + s15 = rotr32(s15.Xor(s3), 16) + s11 = s11.Add(s15) + s7 = rotr32(s7.Xor(s11), 12) + s3 = s3.Add(s7).Add(m7) + s15 = rotr32(s15.Xor(s3), 8) + s11 = s11.Add(s15) + s7 = rotr32(s7.Xor(s11), 7) + s0 = s0.Add(s5).Add(m8) + s15 = rotr32(s15.Xor(s0), 16) + s10 = s10.Add(s15) + s5 = rotr32(s5.Xor(s10), 12) + s0 = s0.Add(s5).Add(m9) + s15 = rotr32(s15.Xor(s0), 8) + s10 = s10.Add(s15) + s5 = rotr32(s5.Xor(s10), 7) + s1 = s1.Add(s6).Add(m10) + s12 = rotr32(s12.Xor(s1), 16) + s11 = s11.Add(s12) + s6 = rotr32(s6.Xor(s11), 12) + s1 = s1.Add(s6).Add(m11) + s12 = rotr32(s12.Xor(s1), 8) + s11 = s11.Add(s12) + s6 = rotr32(s6.Xor(s11), 7) + s2 = s2.Add(s7).Add(m12) + s13 = rotr32(s13.Xor(s2), 16) + s8 = s8.Add(s13) + s7 = rotr32(s7.Xor(s8), 12) + s2 = s2.Add(s7).Add(m13) + s13 = rotr32(s13.Xor(s2), 8) + s8 = s8.Add(s13) + s7 = rotr32(s7.Xor(s8), 7) + s3 = s3.Add(s4).Add(m14) + s14 = rotr32(s14.Xor(s3), 16) + s9 = s9.Add(s14) + s4 = rotr32(s4.Xor(s9), 12) + s3 = s3.Add(s4).Add(m15) + s14 = rotr32(s14.Xor(s3), 8) + s9 = s9.Add(s14) + s4 = rotr32(s4.Xor(s9), 7) + // round 1 + s0 = s0.Add(s4).Add(m2) + s12 = rotr32(s12.Xor(s0), 16) + s8 = s8.Add(s12) + s4 = rotr32(s4.Xor(s8), 12) + s0 = s0.Add(s4).Add(m6) + s12 = rotr32(s12.Xor(s0), 8) + s8 = s8.Add(s12) + s4 = rotr32(s4.Xor(s8), 7) + s1 = s1.Add(s5).Add(m3) + s13 = rotr32(s13.Xor(s1), 16) + s9 = s9.Add(s13) + s5 = rotr32(s5.Xor(s9), 12) + s1 = s1.Add(s5).Add(m10) + s13 = rotr32(s13.Xor(s1), 8) + s9 = s9.Add(s13) + s5 = rotr32(s5.Xor(s9), 7) + s2 = s2.Add(s6).Add(m7) + s14 = rotr32(s14.Xor(s2), 16) + s10 = s10.Add(s14) + s6 = rotr32(s6.Xor(s10), 12) + s2 = s2.Add(s6).Add(m0) + s14 = rotr32(s14.Xor(s2), 8) + s10 = s10.Add(s14) + s6 = rotr32(s6.Xor(s10), 7) + s3 = s3.Add(s7).Add(m4) + s15 = rotr32(s15.Xor(s3), 16) + s11 = s11.Add(s15) + s7 = rotr32(s7.Xor(s11), 12) + s3 = s3.Add(s7).Add(m13) + s15 = rotr32(s15.Xor(s3), 8) + s11 = s11.Add(s15) + s7 = rotr32(s7.Xor(s11), 7) + s0 = s0.Add(s5).Add(m1) + s15 = rotr32(s15.Xor(s0), 16) + s10 = s10.Add(s15) + s5 = rotr32(s5.Xor(s10), 12) + s0 = s0.Add(s5).Add(m11) + s15 = rotr32(s15.Xor(s0), 8) + s10 = s10.Add(s15) + s5 = rotr32(s5.Xor(s10), 7) + s1 = s1.Add(s6).Add(m12) + s12 = rotr32(s12.Xor(s1), 16) + s11 = s11.Add(s12) + s6 = rotr32(s6.Xor(s11), 12) + s1 = s1.Add(s6).Add(m5) + s12 = rotr32(s12.Xor(s1), 8) + s11 = s11.Add(s12) + s6 = rotr32(s6.Xor(s11), 7) + s2 = s2.Add(s7).Add(m9) + s13 = rotr32(s13.Xor(s2), 16) + s8 = s8.Add(s13) + s7 = rotr32(s7.Xor(s8), 12) + s2 = s2.Add(s7).Add(m14) + s13 = rotr32(s13.Xor(s2), 8) + s8 = s8.Add(s13) + s7 = rotr32(s7.Xor(s8), 7) + s3 = s3.Add(s4).Add(m15) + s14 = rotr32(s14.Xor(s3), 16) + s9 = s9.Add(s14) + s4 = rotr32(s4.Xor(s9), 12) + s3 = s3.Add(s4).Add(m8) + s14 = rotr32(s14.Xor(s3), 8) + s9 = s9.Add(s14) + s4 = rotr32(s4.Xor(s9), 7) + // round 2 + s0 = s0.Add(s4).Add(m3) + s12 = rotr32(s12.Xor(s0), 16) + s8 = s8.Add(s12) + s4 = rotr32(s4.Xor(s8), 12) + s0 = s0.Add(s4).Add(m4) + s12 = rotr32(s12.Xor(s0), 8) + s8 = s8.Add(s12) + s4 = rotr32(s4.Xor(s8), 7) + s1 = s1.Add(s5).Add(m10) + s13 = rotr32(s13.Xor(s1), 16) + s9 = s9.Add(s13) + s5 = rotr32(s5.Xor(s9), 12) + s1 = s1.Add(s5).Add(m12) + s13 = rotr32(s13.Xor(s1), 8) + s9 = s9.Add(s13) + s5 = rotr32(s5.Xor(s9), 7) + s2 = s2.Add(s6).Add(m13) + s14 = rotr32(s14.Xor(s2), 16) + s10 = s10.Add(s14) + s6 = rotr32(s6.Xor(s10), 12) + s2 = s2.Add(s6).Add(m2) + s14 = rotr32(s14.Xor(s2), 8) + s10 = s10.Add(s14) + s6 = rotr32(s6.Xor(s10), 7) + s3 = s3.Add(s7).Add(m7) + s15 = rotr32(s15.Xor(s3), 16) + s11 = s11.Add(s15) + s7 = rotr32(s7.Xor(s11), 12) + s3 = s3.Add(s7).Add(m14) + s15 = rotr32(s15.Xor(s3), 8) + s11 = s11.Add(s15) + s7 = rotr32(s7.Xor(s11), 7) + s0 = s0.Add(s5).Add(m6) + s15 = rotr32(s15.Xor(s0), 16) + s10 = s10.Add(s15) + s5 = rotr32(s5.Xor(s10), 12) + s0 = s0.Add(s5).Add(m5) + s15 = rotr32(s15.Xor(s0), 8) + s10 = s10.Add(s15) + s5 = rotr32(s5.Xor(s10), 7) + s1 = s1.Add(s6).Add(m9) + s12 = rotr32(s12.Xor(s1), 16) + s11 = s11.Add(s12) + s6 = rotr32(s6.Xor(s11), 12) + s1 = s1.Add(s6).Add(m0) + s12 = rotr32(s12.Xor(s1), 8) + s11 = s11.Add(s12) + s6 = rotr32(s6.Xor(s11), 7) + s2 = s2.Add(s7).Add(m11) + s13 = rotr32(s13.Xor(s2), 16) + s8 = s8.Add(s13) + s7 = rotr32(s7.Xor(s8), 12) + s2 = s2.Add(s7).Add(m15) + s13 = rotr32(s13.Xor(s2), 8) + s8 = s8.Add(s13) + s7 = rotr32(s7.Xor(s8), 7) + s3 = s3.Add(s4).Add(m8) + s14 = rotr32(s14.Xor(s3), 16) + s9 = s9.Add(s14) + s4 = rotr32(s4.Xor(s9), 12) + s3 = s3.Add(s4).Add(m1) + s14 = rotr32(s14.Xor(s3), 8) + s9 = s9.Add(s14) + s4 = rotr32(s4.Xor(s9), 7) + // round 3 + s0 = s0.Add(s4).Add(m10) + s12 = rotr32(s12.Xor(s0), 16) + s8 = s8.Add(s12) + s4 = rotr32(s4.Xor(s8), 12) + s0 = s0.Add(s4).Add(m7) + s12 = rotr32(s12.Xor(s0), 8) + s8 = s8.Add(s12) + s4 = rotr32(s4.Xor(s8), 7) + s1 = s1.Add(s5).Add(m12) + s13 = rotr32(s13.Xor(s1), 16) + s9 = s9.Add(s13) + s5 = rotr32(s5.Xor(s9), 12) + s1 = s1.Add(s5).Add(m9) + s13 = rotr32(s13.Xor(s1), 8) + s9 = s9.Add(s13) + s5 = rotr32(s5.Xor(s9), 7) + s2 = s2.Add(s6).Add(m14) + s14 = rotr32(s14.Xor(s2), 16) + s10 = s10.Add(s14) + s6 = rotr32(s6.Xor(s10), 12) + s2 = s2.Add(s6).Add(m3) + s14 = rotr32(s14.Xor(s2), 8) + s10 = s10.Add(s14) + s6 = rotr32(s6.Xor(s10), 7) + s3 = s3.Add(s7).Add(m13) + s15 = rotr32(s15.Xor(s3), 16) + s11 = s11.Add(s15) + s7 = rotr32(s7.Xor(s11), 12) + s3 = s3.Add(s7).Add(m15) + s15 = rotr32(s15.Xor(s3), 8) + s11 = s11.Add(s15) + s7 = rotr32(s7.Xor(s11), 7) + s0 = s0.Add(s5).Add(m4) + s15 = rotr32(s15.Xor(s0), 16) + s10 = s10.Add(s15) + s5 = rotr32(s5.Xor(s10), 12) + s0 = s0.Add(s5).Add(m0) + s15 = rotr32(s15.Xor(s0), 8) + s10 = s10.Add(s15) + s5 = rotr32(s5.Xor(s10), 7) + s1 = s1.Add(s6).Add(m11) + s12 = rotr32(s12.Xor(s1), 16) + s11 = s11.Add(s12) + s6 = rotr32(s6.Xor(s11), 12) + s1 = s1.Add(s6).Add(m2) + s12 = rotr32(s12.Xor(s1), 8) + s11 = s11.Add(s12) + s6 = rotr32(s6.Xor(s11), 7) + s2 = s2.Add(s7).Add(m5) + s13 = rotr32(s13.Xor(s2), 16) + s8 = s8.Add(s13) + s7 = rotr32(s7.Xor(s8), 12) + s2 = s2.Add(s7).Add(m8) + s13 = rotr32(s13.Xor(s2), 8) + s8 = s8.Add(s13) + s7 = rotr32(s7.Xor(s8), 7) + s3 = s3.Add(s4).Add(m1) + s14 = rotr32(s14.Xor(s3), 16) + s9 = s9.Add(s14) + s4 = rotr32(s4.Xor(s9), 12) + s3 = s3.Add(s4).Add(m6) + s14 = rotr32(s14.Xor(s3), 8) + s9 = s9.Add(s14) + s4 = rotr32(s4.Xor(s9), 7) + // round 4 + s0 = s0.Add(s4).Add(m12) + s12 = rotr32(s12.Xor(s0), 16) + s8 = s8.Add(s12) + s4 = rotr32(s4.Xor(s8), 12) + s0 = s0.Add(s4).Add(m13) + s12 = rotr32(s12.Xor(s0), 8) + s8 = s8.Add(s12) + s4 = rotr32(s4.Xor(s8), 7) + s1 = s1.Add(s5).Add(m9) + s13 = rotr32(s13.Xor(s1), 16) + s9 = s9.Add(s13) + s5 = rotr32(s5.Xor(s9), 12) + s1 = s1.Add(s5).Add(m11) + s13 = rotr32(s13.Xor(s1), 8) + s9 = s9.Add(s13) + s5 = rotr32(s5.Xor(s9), 7) + s2 = s2.Add(s6).Add(m15) + s14 = rotr32(s14.Xor(s2), 16) + s10 = s10.Add(s14) + s6 = rotr32(s6.Xor(s10), 12) + s2 = s2.Add(s6).Add(m10) + s14 = rotr32(s14.Xor(s2), 8) + s10 = s10.Add(s14) + s6 = rotr32(s6.Xor(s10), 7) + s3 = s3.Add(s7).Add(m14) + s15 = rotr32(s15.Xor(s3), 16) + s11 = s11.Add(s15) + s7 = rotr32(s7.Xor(s11), 12) + s3 = s3.Add(s7).Add(m8) + s15 = rotr32(s15.Xor(s3), 8) + s11 = s11.Add(s15) + s7 = rotr32(s7.Xor(s11), 7) + s0 = s0.Add(s5).Add(m7) + s15 = rotr32(s15.Xor(s0), 16) + s10 = s10.Add(s15) + s5 = rotr32(s5.Xor(s10), 12) + s0 = s0.Add(s5).Add(m2) + s15 = rotr32(s15.Xor(s0), 8) + s10 = s10.Add(s15) + s5 = rotr32(s5.Xor(s10), 7) + s1 = s1.Add(s6).Add(m5) + s12 = rotr32(s12.Xor(s1), 16) + s11 = s11.Add(s12) + s6 = rotr32(s6.Xor(s11), 12) + s1 = s1.Add(s6).Add(m3) + s12 = rotr32(s12.Xor(s1), 8) + s11 = s11.Add(s12) + s6 = rotr32(s6.Xor(s11), 7) + s2 = s2.Add(s7).Add(m0) + s13 = rotr32(s13.Xor(s2), 16) + s8 = s8.Add(s13) + s7 = rotr32(s7.Xor(s8), 12) + s2 = s2.Add(s7).Add(m1) + s13 = rotr32(s13.Xor(s2), 8) + s8 = s8.Add(s13) + s7 = rotr32(s7.Xor(s8), 7) + s3 = s3.Add(s4).Add(m6) + s14 = rotr32(s14.Xor(s3), 16) + s9 = s9.Add(s14) + s4 = rotr32(s4.Xor(s9), 12) + s3 = s3.Add(s4).Add(m4) + s14 = rotr32(s14.Xor(s3), 8) + s9 = s9.Add(s14) + s4 = rotr32(s4.Xor(s9), 7) + // round 5 + s0 = s0.Add(s4).Add(m9) + s12 = rotr32(s12.Xor(s0), 16) + s8 = s8.Add(s12) + s4 = rotr32(s4.Xor(s8), 12) + s0 = s0.Add(s4).Add(m14) + s12 = rotr32(s12.Xor(s0), 8) + s8 = s8.Add(s12) + s4 = rotr32(s4.Xor(s8), 7) + s1 = s1.Add(s5).Add(m11) + s13 = rotr32(s13.Xor(s1), 16) + s9 = s9.Add(s13) + s5 = rotr32(s5.Xor(s9), 12) + s1 = s1.Add(s5).Add(m5) + s13 = rotr32(s13.Xor(s1), 8) + s9 = s9.Add(s13) + s5 = rotr32(s5.Xor(s9), 7) + s2 = s2.Add(s6).Add(m8) + s14 = rotr32(s14.Xor(s2), 16) + s10 = s10.Add(s14) + s6 = rotr32(s6.Xor(s10), 12) + s2 = s2.Add(s6).Add(m12) + s14 = rotr32(s14.Xor(s2), 8) + s10 = s10.Add(s14) + s6 = rotr32(s6.Xor(s10), 7) + s3 = s3.Add(s7).Add(m15) + s15 = rotr32(s15.Xor(s3), 16) + s11 = s11.Add(s15) + s7 = rotr32(s7.Xor(s11), 12) + s3 = s3.Add(s7).Add(m1) + s15 = rotr32(s15.Xor(s3), 8) + s11 = s11.Add(s15) + s7 = rotr32(s7.Xor(s11), 7) + s0 = s0.Add(s5).Add(m13) + s15 = rotr32(s15.Xor(s0), 16) + s10 = s10.Add(s15) + s5 = rotr32(s5.Xor(s10), 12) + s0 = s0.Add(s5).Add(m3) + s15 = rotr32(s15.Xor(s0), 8) + s10 = s10.Add(s15) + s5 = rotr32(s5.Xor(s10), 7) + s1 = s1.Add(s6).Add(m0) + s12 = rotr32(s12.Xor(s1), 16) + s11 = s11.Add(s12) + s6 = rotr32(s6.Xor(s11), 12) + s1 = s1.Add(s6).Add(m10) + s12 = rotr32(s12.Xor(s1), 8) + s11 = s11.Add(s12) + s6 = rotr32(s6.Xor(s11), 7) + s2 = s2.Add(s7).Add(m2) + s13 = rotr32(s13.Xor(s2), 16) + s8 = s8.Add(s13) + s7 = rotr32(s7.Xor(s8), 12) + s2 = s2.Add(s7).Add(m6) + s13 = rotr32(s13.Xor(s2), 8) + s8 = s8.Add(s13) + s7 = rotr32(s7.Xor(s8), 7) + s3 = s3.Add(s4).Add(m4) + s14 = rotr32(s14.Xor(s3), 16) + s9 = s9.Add(s14) + s4 = rotr32(s4.Xor(s9), 12) + s3 = s3.Add(s4).Add(m7) + s14 = rotr32(s14.Xor(s3), 8) + s9 = s9.Add(s14) + s4 = rotr32(s4.Xor(s9), 7) + // round 6 + s0 = s0.Add(s4).Add(m11) + s12 = rotr32(s12.Xor(s0), 16) + s8 = s8.Add(s12) + s4 = rotr32(s4.Xor(s8), 12) + s0 = s0.Add(s4).Add(m15) + s12 = rotr32(s12.Xor(s0), 8) + s8 = s8.Add(s12) + s4 = rotr32(s4.Xor(s8), 7) + s1 = s1.Add(s5).Add(m5) + s13 = rotr32(s13.Xor(s1), 16) + s9 = s9.Add(s13) + s5 = rotr32(s5.Xor(s9), 12) + s1 = s1.Add(s5).Add(m0) + s13 = rotr32(s13.Xor(s1), 8) + s9 = s9.Add(s13) + s5 = rotr32(s5.Xor(s9), 7) + s2 = s2.Add(s6).Add(m1) + s14 = rotr32(s14.Xor(s2), 16) + s10 = s10.Add(s14) + s6 = rotr32(s6.Xor(s10), 12) + s2 = s2.Add(s6).Add(m9) + s14 = rotr32(s14.Xor(s2), 8) + s10 = s10.Add(s14) + s6 = rotr32(s6.Xor(s10), 7) + s3 = s3.Add(s7).Add(m8) + s15 = rotr32(s15.Xor(s3), 16) + s11 = s11.Add(s15) + s7 = rotr32(s7.Xor(s11), 12) + s3 = s3.Add(s7).Add(m6) + s15 = rotr32(s15.Xor(s3), 8) + s11 = s11.Add(s15) + s7 = rotr32(s7.Xor(s11), 7) + s0 = s0.Add(s5).Add(m14) + s15 = rotr32(s15.Xor(s0), 16) + s10 = s10.Add(s15) + s5 = rotr32(s5.Xor(s10), 12) + s0 = s0.Add(s5).Add(m10) + s15 = rotr32(s15.Xor(s0), 8) + s10 = s10.Add(s15) + s5 = rotr32(s5.Xor(s10), 7) + s1 = s1.Add(s6).Add(m2) + s12 = rotr32(s12.Xor(s1), 16) + s11 = s11.Add(s12) + s6 = rotr32(s6.Xor(s11), 12) + s1 = s1.Add(s6).Add(m12) + s12 = rotr32(s12.Xor(s1), 8) + s11 = s11.Add(s12) + s6 = rotr32(s6.Xor(s11), 7) + s2 = s2.Add(s7).Add(m3) + s13 = rotr32(s13.Xor(s2), 16) + s8 = s8.Add(s13) + s7 = rotr32(s7.Xor(s8), 12) + s2 = s2.Add(s7).Add(m4) + s13 = rotr32(s13.Xor(s2), 8) + s8 = s8.Add(s13) + s7 = rotr32(s7.Xor(s8), 7) + s3 = s3.Add(s4).Add(m7) + s14 = rotr32(s14.Xor(s3), 16) + s9 = s9.Add(s14) + s4 = rotr32(s4.Xor(s9), 12) + s3 = s3.Add(s4).Add(m13) + s14 = rotr32(s14.Xor(s3), 8) + s9 = s9.Add(s14) + s4 = rotr32(s4.Xor(s9), 7) + // finalize: full 16-word output for the root XOF + s0 = s0.Xor(s8) + s8 = s8.Xor(archsimd.LoadUint32x16Array(&in[0])) + s1 = s1.Xor(s9) + s9 = s9.Xor(archsimd.LoadUint32x16Array(&in[1])) + s2 = s2.Xor(s10) + s10 = s10.Xor(archsimd.LoadUint32x16Array(&in[2])) + s3 = s3.Xor(s11) + s11 = s11.Xor(archsimd.LoadUint32x16Array(&in[3])) + s4 = s4.Xor(s12) + s12 = s12.Xor(archsimd.LoadUint32x16Array(&in[4])) + s5 = s5.Xor(s13) + s13 = s13.Xor(archsimd.LoadUint32x16Array(&in[5])) + s6 = s6.Xor(s14) + s14 = s14.Xor(archsimd.LoadUint32x16Array(&in[6])) + s7 = s7.Xor(s15) + s15 = s15.Xor(archsimd.LoadUint32x16Array(&in[7])) + // s_j holds word j of every lane; transpose so out[lane] is one block. + var t [16][16]uint32 + s0.StoreArray(&t[0]) + s1.StoreArray(&t[1]) + s2.StoreArray(&t[2]) + s3.StoreArray(&t[3]) + s4.StoreArray(&t[4]) + s5.StoreArray(&t[5]) + s6.StoreArray(&t[6]) + s7.StoreArray(&t[7]) + s8.StoreArray(&t[8]) + s9.StoreArray(&t[9]) + s10.StoreArray(&t[10]) + s11.StoreArray(&t[11]) + s12.StoreArray(&t[12]) + s13.StoreArray(&t[13]) + s14.StoreArray(&t[14]) + s15.StoreArray(&t[15]) + for lane := 0; lane < 16; lane++ { + for word := 0; word < 16; word++ { + out[lane][word] = t[word][lane] + } + } +} diff --git a/sei-db/state_db/sc/flatkv/lthash/gen_blake3_xof16.go b/sei-db/state_db/sc/flatkv/lthash/gen_blake3_xof16.go new file mode 100644 index 0000000000..ba95f11dd0 --- /dev/null +++ b/sei-db/state_db/sc/flatkv/lthash/gen_blake3_xof16.go @@ -0,0 +1,118 @@ +//go:build ignore + +// gen_blake3_xof16 writes blake3_xof16_amd64.go: a fully unrolled 16-lane +// Blake3 root compression over simd/archsimd Uint32x16 vectors. Each lane is one +// output block of the XOF (counter base+lane), so a single call produces +// 16 x 64 bytes of XOF output; two calls produce the 2048 bytes an LtHash needs. +// +// Usage: go run gen_blake3_xof16.go +package main + +import ( + "bytes" + "fmt" + "go/format" + "os" +) + +// msgPerm is the Blake3 message word permutation applied between rounds. +var msgPerm = [16]int{2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8} + +func main() { + var b bytes.Buffer + p := func(format string, args ...any) { fmt.Fprintf(&b, format+"\n", args...) } + + p("// Code generated by gen_blake3_xof16.go; DO NOT EDIT.") + p("") + p("//go:build goexperiment.simd && amd64") + p("") + p("package lthash") + p("") + p(`import "simd/archsimd"`) + p("") + p("// xof16Inputs holds the 16-lane broadcast of every compression input.") + p("// Rows 0-7 are the chaining value, 8-11 the IV, 12 the block counter base,") + p("// 13 zero (counter high word), 14 the block length, 15 the flags and 16-31 the") + p("// message words. Loading a pre-broadcast row is used instead of") + p("// archsimd.BroadcastUint32x16, which compiles to a legacy-SSE sequence that") + p("// costs an SSE/AVX transition on every call.") + p("type xof16Inputs [32][16]uint32") + p("") + p("var xof16Lanes = [16]uint32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}") + p("") + p("// rotr32 rotates every lane right by n using VPSHRDD; RotateAllRight is") + p("// emulated with three instructions.") + p("func rotr32(x archsimd.Uint32x16, n uint64) archsimd.Uint32x16 {") + p("\treturn x.ShiftAllRightConcatMod32(x, n)") + p("}") + p("") + p("// xof16 writes output blocks base..base+15 of the root XOF to out,") + p("// out[lane][word].") + p("func xof16(in *xof16Inputs, out *[16][16]uint32) {") + for i := 0; i < 12; i++ { + p("\ts%d := archsimd.LoadUint32x16Array(&in[%d])", i, i) + } + p("\ts12 := archsimd.LoadUint32x16Array(&xof16Lanes).Add(archsimd.LoadUint32x16Array(&in[12]))") + for i := 13; i < 16; i++ { + p("\ts%d := archsimd.LoadUint32x16Array(&in[%d])", i, i) + } + for i := 0; i < 16; i++ { + p("\tm%d := archsimd.LoadUint32x16Array(&in[%d])", i, 16+i) + } + g := func(a, b, c, d, mx, my int) { + p("\ts%d = s%d.Add(s%d).Add(m%d)", a, a, b, mx) + p("\ts%d = rotr32(s%d.Xor(s%d), 16)", d, d, a) + p("\ts%d = s%d.Add(s%d)", c, c, d) + p("\ts%d = rotr32(s%d.Xor(s%d), 12)", b, b, c) + p("\ts%d = s%d.Add(s%d).Add(m%d)", a, a, b, my) + p("\ts%d = rotr32(s%d.Xor(s%d), 8)", d, d, a) + p("\ts%d = s%d.Add(s%d)", c, c, d) + p("\ts%d = rotr32(s%d.Xor(s%d), 7)", b, b, c) + } + m := [16]int{} + for i := range m { + m[i] = i + } + for r := 0; r < 7; r++ { + p("\t// round %d", r) + g(0, 4, 8, 12, m[0], m[1]) + g(1, 5, 9, 13, m[2], m[3]) + g(2, 6, 10, 14, m[4], m[5]) + g(3, 7, 11, 15, m[6], m[7]) + g(0, 5, 10, 15, m[8], m[9]) + g(1, 6, 11, 12, m[10], m[11]) + g(2, 7, 8, 13, m[12], m[13]) + g(3, 4, 9, 14, m[14], m[15]) + var next [16]int + for i := range next { + next[i] = m[msgPerm[i]] + } + m = next + } + p("\t// finalize: full 16-word output for the root XOF") + for i := 0; i < 8; i++ { + p("\ts%d = s%d.Xor(s%d)", i, i, i+8) + p("\ts%d = s%d.Xor(archsimd.LoadUint32x16Array(&in[%d]))", i+8, i+8, i) + } + p("\t// s_j holds word j of every lane; transpose so out[lane] is one block.") + p("\tvar t [16][16]uint32") + for j := 0; j < 16; j++ { + p("\ts%d.StoreArray(&t[%d])", j, j) + } + p("\tfor lane := 0; lane < 16; lane++ {") + p("\t\tfor word := 0; word < 16; word++ {") + p("\t\t\tout[lane][word] = t[word][lane]") + p("\t\t}") + p("\t}") + p("}") + + src, err := format.Source(b.Bytes()) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + if err := os.WriteFile("blake3_xof16_amd64.go", src, 0o644); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/sei-db/state_db/sc/flatkv/lthash/lthash.go b/sei-db/state_db/sc/flatkv/lthash/lthash.go index 47348ee1ae..fcf4131708 100644 --- a/sei-db/state_db/sc/flatkv/lthash/lthash.go +++ b/sei-db/state_db/sc/flatkv/lthash/lthash.go @@ -47,16 +47,7 @@ func (l *LtHash) MixIn(other *LtHash) { if other == nil { return } - for i := 0; i < LtHashSize; i += 8 { - l.limbs[i] += other.limbs[i] - l.limbs[i+1] += other.limbs[i+1] - l.limbs[i+2] += other.limbs[i+2] - l.limbs[i+3] += other.limbs[i+3] - l.limbs[i+4] += other.limbs[i+4] - l.limbs[i+5] += other.limbs[i+5] - l.limbs[i+6] += other.limbs[i+6] - l.limbs[i+7] += other.limbs[i+7] - } + active.add(l, other) } // MixOut subtracts other from this LtHash (element-wise mod 2^16). Nil is a no-op. @@ -64,16 +55,7 @@ func (l *LtHash) MixOut(other *LtHash) { if other == nil { return } - for i := 0; i < LtHashSize; i += 8 { - l.limbs[i] -= other.limbs[i] - l.limbs[i+1] -= other.limbs[i+1] - l.limbs[i+2] -= other.limbs[i+2] - l.limbs[i+3] -= other.limbs[i+3] - l.limbs[i+4] -= other.limbs[i+4] - l.limbs[i+5] -= other.limbs[i+5] - l.limbs[i+6] -= other.limbs[i+6] - l.limbs[i+7] -= other.limbs[i+7] - } + active.sub(l, other) } // Equal returns true if both LtHash vectors are identical. @@ -137,22 +119,8 @@ func hash(data []byte) *LtHash { if len(data) == 0 { return New() } - - hasher := blake3HasherPool.Get().(*blake3.Hasher) - hasher.Reset() - _, _ = hasher.Write(data) - digest := hasher.Digest() - - bufPtr := xofBufferPool.Get().(*[]byte) - output := *bufPtr - _, _ = digest.Read(output) // Blake3 XOF never errors and always fills buffer - blake3HasherPool.Put(hasher) - lth := ltHashPool.Get().(*LtHash) - for i := 0; i < LtHashSize; i++ { - lth.limbs[i] = binary.LittleEndian.Uint16(output[i*2 : (i+1)*2]) - } - xofBufferPool.Put(bufPtr) + active.expand(data, lth) return lth } @@ -183,19 +151,6 @@ func serializeKV(key, value []byte) []byte { // --- internal pools --- -var xofBufferPool = sync.Pool{ - New: func() interface{} { - buf := make([]byte, LtHashBytes) - return &buf - }, -} - -var blake3HasherPool = sync.Pool{ - New: func() interface{} { - return blake3.New() - }, -} - var checksumBufferPool = sync.Pool{ New: func() interface{} { buf := make([]byte, LtHashBytes) From cb9829ed6d6acc4b4fd3fec210de250fa32cfa98 Mon Sep 17 00:00:00 2001 From: masih Date: Fri, 11 Sep 2026 17:33:52 +0000 Subject: [PATCH 2/3] Run LtHash benchmark job on several runner pools and flag missing AVX-512 --- .github/workflows/lthash-bench.yml | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/.github/workflows/lthash-bench.yml b/.github/workflows/lthash-bench.yml index aadeade7aa..2eb9469874 100644 --- a/.github/workflows/lthash-bench.yml +++ b/.github/workflows/lthash-bench.yml @@ -22,8 +22,14 @@ env: jobs: bench: - name: Default vs SIMD - runs-on: uci-default + name: Default vs SIMD (${{ matrix.runner }}) + # Not every runner pool has AVX-512; several are tried so at least one is + # likely to execute the SIMD backend. + strategy: + fail-fast: false + matrix: + runner: [uci-default, ubuntu-latest] + runs-on: ${{ matrix.runner }} steps: # See: https://github.com/actions/checkout/releases/tag/v7.0.0 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 @@ -41,7 +47,7 @@ jobs: - name: CPU run: | { - echo '## CPU' + echo '## CPU (${{ matrix.runner }})' echo '```' lscpu | grep -E 'Model name|^Flags' | sed -E 's/^Flags:\s+/Flags: /' | fold -w 120 echo '```' @@ -74,15 +80,21 @@ jobs: { echo '## LtHash backends (`benchstat -col /backend`)' echo - echo 'Only backends the runner CPU can execute appear as columns; the SIMD' - echo 'backend needs AVX-512F + VBMI2. `hashChunk` is the end-to-end per-block path.' + if grep -q 'backend=simd' bench.txt; then + echo '`hashChunk` is the end-to-end per-block path.' + else + echo '**This runner CPU lacks AVX-512F + VBMI2, so only the default backend ran.**' + fi echo echo '```' benchstat -col /backend bench.txt echo '```' } >> "$GITHUB_STEP_SUMMARY" + if ! grep -q 'backend=simd' bench.txt; then + echo '::warning::SIMD backend unavailable on this runner CPU; only the default backend was benchmarked' + fi - uses: actions/upload-artifact@v4 with: - name: lthash-bench + name: lthash-bench-${{ matrix.runner }} path: bench.txt From 356d8e31802c6ba5ec09177e00206bd0dc2b546d Mon Sep 17 00:00:00 2001 From: masih Date: Fri, 11 Sep 2026 20:47:52 +0000 Subject: [PATCH 3/3] Summarise LtHash benchmarks with benchstat in log, job summary and PR comment --- .github/workflows/lthash-bench.yml | 49 +++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/.github/workflows/lthash-bench.yml b/.github/workflows/lthash-bench.yml index 2eb9469874..df1bc278bd 100644 --- a/.github/workflows/lthash-bench.yml +++ b/.github/workflows/lthash-bench.yml @@ -15,6 +15,10 @@ concurrency: cancel-in-progress: true group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'push' && github.sha || github.ref }} +permissions: + contents: read + pull-requests: write + env: GO_VERSION: '1.27.1' LTHASH_PKG: ./sei-db/state_db/sc/flatkv/lthash @@ -46,12 +50,7 @@ jobs: - name: CPU run: | - { - echo '## CPU (${{ matrix.runner }})' - echo '```' - lscpu | grep -E 'Model name|^Flags' | sed -E 's/^Flags:\s+/Flags: /' | fold -w 120 - echo '```' - } >> "$GITHUB_STEP_SUMMARY" + lscpu | grep -E 'Model name|^Flags' | sed -E 's/^Flags:\s+/Flags: /' | fold -w 120 # Both builds must pass: the default build has no SIMD backend compiled # in, the experiment build adds it and the differential tests compare @@ -74,14 +73,19 @@ jobs: -count ${{ env.BENCH_COUNT }} \ ${{ env.LTHASH_PKG }} | tee bench.txt - - name: Compare backends + # benchstat groups the samples by the `backend=` sub-benchmark name, so the + # simd column reads as a delta against default. The report is printed to + # the log, added to the job summary and upserted as a PR comment. + - name: Summarise with benchstat run: | go install golang.org/x/perf/cmd/benchstat@v0.0.0-20250813145418-2f7363a06fe1 { - echo '## LtHash backends (`benchstat -col /backend`)' + echo '### LtHash default vs SIMD (`${{ matrix.runner }}`)' + echo + echo "CPU: $(lscpu | sed -nE 's/^Model name:\s+//p')" echo if grep -q 'backend=simd' bench.txt; then - echo '`hashChunk` is the end-to-end per-block path.' + echo '`HashChunk` is the end-to-end per-block path; `vs base` is simd relative to default.' else echo '**This runner CPU lacks AVX-512F + VBMI2, so only the default backend ran.**' fi @@ -89,12 +93,35 @@ jobs: echo '```' benchstat -col /backend bench.txt echo '```' - } >> "$GITHUB_STEP_SUMMARY" + } | tee benchstat.md >> "$GITHUB_STEP_SUMMARY" if ! grep -q 'backend=simd' bench.txt; then echo '::warning::SIMD backend unavailable on this runner CPU; only the default backend was benchmarked' fi + - name: Post benchstat report on the PR + if: github.event_name == 'pull_request' + continue-on-error: true + uses: actions/github-script@v8 + env: + MARKER: '' + with: + script: | + const fs = require('fs'); + const marker = process.env.MARKER; + const body = `${marker}\n${fs.readFileSync('benchstat.md', 'utf8')}`; + const { owner, repo } = context.repo; + const issue_number = context.issue.number; + const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number, per_page: 100 }); + const existing = comments.find(c => c.body && c.body.startsWith(marker)); + if (existing) { + await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }); + } else { + await github.rest.issues.createComment({ owner, repo, issue_number, body }); + } + - uses: actions/upload-artifact@v4 with: name: lthash-bench-${{ matrix.runner }} - path: bench.txt + path: | + bench.txt + benchstat.md