Skip to content

Minor/optimize dero miner - #71

Open
secretnamebasis wants to merge 25 commits into
DEROFDN:community-devfrom
secretnamebasis:minor/optimize-dero-miner
Open

Minor/optimize dero miner#71
secretnamebasis wants to merge 25 commits into
DEROFDN:community-devfrom
secretnamebasis:minor/optimize-dero-miner

Conversation

@secretnamebasis

Copy link
Copy Markdown

Description

Replaces AstroBWTv3's suffix-array construction in two steps:

  1. A pure-Go port of libdivsufsort, replacing the previous SA-IS implementation as the production path (text_32_0alloc). ~19-22% faster in isolation, byte-identical output proven via oracle testing against the retired SA-IS implementation (kept as a permanent comparison oracle, not deleted).
  2. A structure-aware "template-descriptor" SA path, ported from Dirtybird-Go-Miner (internal/astrobwt/sa_v114*.go, MIT license) and integrated as this package's new default. AstroBWTv3's wolf loop applies RC4 whitening on only ~25% of iterations — a real property of the protocol itself, not specific to any one implementation — leaving long correlated runs in the hash's data buffer that a generic sort algorithm can't exploit but this path does directly. Any decline falls back to the divsufsort path from step 1, not SAIS.

Both algorithms produce byte-identical output by construction (a suffix array is unique for a given input), verified via a 1,000,000-hash differential gate across all cores, 64 real captured fixtures round-tripped two independent ways, a 5,000-trial + 12-length differential suite, and the package's own pre-existing golden-hash tests passing unmodified throughout.

No consensus-level change — AstroBWTv3's output is unchanged, only the internal algorithm computing it. This is an implementation-level performance improvement, not a protocol change.

Note: this template says PRs should target dev — that branch doesn't exist on this fork; I've based this off community-dev instead. Flagging in case the base needs adjusting before merge.

Fixes # (no linked issue — proactive performance work, not a bug report)

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update
  • This will require a HardFork to be enabled

Which part is impacted?

  • Wallet
  • Daemon
  • Miner
  • Explorer
  • Simulator
  • Misc (documentation, comments, text...)

Checklist:

  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • My changes generate no new warnings

License

I am contributing & releasing the code under DERO Research License (which can be found here).

secretnamebasis and others added 25 commits August 3, 2026 21:06
…t port

AstroBWTv3's proof-of-work spends ~82% of its per-hash time in suffix-array
construction, previously a Go port of stdlib SA-IS (sais_8_32). This swaps
the production entry point (text_32_0alloc, called from both the miner and
block validation via miniblock.go) to a faithful pure-Go port of
libdivsufsort instead, which runs ~19-22% faster in isolation and ~11%
faster on the real dero-miner binary end to end.

The old SA-IS implementation is kept as text_32_0alloc_sais rather than
deleted, serving as a permanent comparison oracle: since a suffix array is
unique for a given input, two correct algorithms must produce byte-identical
output, and TestDivSufSortMatchesProductionSAIS asserts exactly that on
every test run. Correctness is additionally backed by the package's
pre-existing golden-hash tests (TestAstroBWTv3, TestAstroBWTv3repeattest),
which pass unchanged, and 379+ dual-compute trials against real captured
AstroBWTv3 fixtures.

Adds the divsufsort port itself (divsufsort_go.go, sssort_go.go,
trsort_go.go), its correctness-oracle test suite, a C-vs-Go cross-check
harness (divsufsort_bench/), and fixture-capture infrastructure used to
build realistic benchmark/test corpora from live AstroBWTv3 runs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
divsufsort treats AstroBWTv3's suffix-array input as opaque text, same as
SA-IS did. It isn't: the wolf loop applies RC4 whitening on only ~25% of
iterations (the trigger is AstroBWTv3's own protocol, `step_3[pos1]-
step_3[pos2] <= 0x40`, identical in any implementation of this hash), so
runs of 256-byte chunks between whitening events stay highly correlated.
This adds a suffix-array construction that exploits that structure directly
instead of sorting it as generic text.

Ported from Dirtybird-Go-Miner's internal/astrobwt/sa_v114*.go (MIT),
itself derived from a Zig/C++ reference. The core algorithm
(sa_template_emit.go, sa_template_merge.go) is close to a verbatim port;
what's new here is the integration into this package's own ScratchData/Pool
structure, buffer sizing re-derived from this package's own MAX_LENGTH and
protocol bounds rather than the reference's, and the fallback choice: any
decline falls back to this package's own divsufsort path
(text_32_0alloc), not SAIS, since divsufsort is already faster.

Wolf-loop instrumentation (pow.go) records template markers unconditionally
during the whitening branch -- pure bookkeeping, no effect on the hash
regardless of whether anything reads it. The template path itself is
opt-in only (ScratchData.useTemplateSA, off by default): AstroBWTv3's
behavior is unchanged, proven by the existing golden-hash tests passing
without modification.

Correctness, checked independently of the ported reference's own test
suite: a hand-built end-to-end run against this package's divsufsort oracle,
Stage 4/5 isolated against synthetic buffers and hand-built descriptor
records, 5,000-trial and 12-length differential runs against production
(divsufsort, since the template path isn't yet the default), and a
marker-aware fixture corpus (testdata/safixtures_template, regenerate with
`go test -tags astrobwt_capture -run TestGenerateSATemplateFixtures`)
round-tripped two independent ways. A million-hash differential gate
(TEMPLATE_SA_GATE_HASHES=1000000) is available for a larger confirmation
pass, off by default.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The template path (previous commit) is proven byte-identical to divsufsort
across a 1,000,000-hash differential gate, 64 real captured fixtures, and
substantially faster in isolation and end to end. Pool.New now sets
ScratchData.useTemplateSA = true for every pooled scratch, so AstroBWTv3
runs it by default. Any decline still falls back to text_32_0alloc
(divsufsort) at the pow.go dispatch site, unchanged from the previous
commit -- this adds no new correctness risk beyond what divsufsort itself
already carries.

Retargets every differential test that previously compared the template
path against AstroBWTv3's own output as the "production" reference: once
the template path is what AstroBWTv3 runs, that comparison is circular.
Each test now forces scratch.useTemplateSA = false explicitly to get the
independent divsufsort reference, so the suite keeps checking the two
algorithms against each other rather than a path against itself.

Adds BenchmarkHashDivSufSortOnly, forcing the divsufsort path explicitly
through the same whole-hash benchmark shape as BenchmarkHashTemplateSA, for
a permanent, fair end-to-end comparison against the new default.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Single-stream SHA-NI is latency-bound: each SHA256RNDS2 depends on the
previous, leaving pipeline bubbles even on hardware that has the
dedicated instructions. Interleaving two independent messages'
instruction streams lets an out-of-order core fill those bubbles,
recovering throughput a single stream can't reach on its own.

Ported from Dirtybird-Go-Miner's internal/astrobwt/sha256mb_amd64.s
(MIT), verbatim -- transcription risk is closed by an exact byte diff
against the source, since every instruction's semantics are already
ISA-guaranteed by Intel/AMD, not something to re-derive by hand.

pairHashAvailable gates on real cpuid SHA support (plus LittleEndian);
every other host transparently falls back to two ordinary hashes
through sha256-simd, so this is safe to build into any binary
regardless of what it ends up running on. Unwired from the rest of the
package for now -- nothing calls sha256Sum256Pair yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Splits astroBWTv3's body into astroBWTv3Stream (wolf loop through SA
construction) and the existing SHA-256 finish, so the finish step can
run through the 2-way SHA-NI kernel for two nonces at once instead of
one at a time. astroBWTv3 itself calls Stream then does exactly what
it did before -- no behavioral change to any existing caller.

astroBWTv3Stream carries its own panic recover (astroBWTv3's original
recover stays put, guarding the single-hash path unchanged) since it's
now called from two places. data_len == 0 is an unambiguous panic
signal: the wolf loop's own minimum of 261 tries guarantees
data_len >= 65792 on any real run, so 0 can't occur otherwise.

AstroBWTv3Pair's fallback and mid-stream-panic branches both call
AstroBWTv3 directly -- the same function every single-hash caller
uses -- rather than a second, separately maintained slow path. That
makes AstroBWTv3Pair(a,b) == (AstroBWTv3(a), AstroBWTv3(b)) true by
construction on any host, not just one a test happens to exercise.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
TestSha256Sum256PairDifferential pins the kernel itself against
sha256-simd's own hashing, over a length matrix deliberately dense
around block boundaries (56/64-byte padding edges) plus unequal-length
pairs -- an end-to-end gate that only ever sees the miner's fixed
input sizes could let a wrong two-stream kernel slip through
undetected. It logs pairHashAvailable() so a green run on hardware
without SHA-NI (or big-endian) isn't mistaken for proof the assembly
ran; only a log showing pairHashAvailable=true means the kernel itself
was exercised.

TestAstroBWTv3PairMatchesSequential and the 0-alloc test extend this
package's existing KAT/alloc conventions to the pair path.
BenchmarkAstroBWTv3Pair mirrors the existing single-hash benchmark's
shape for a same-basis throughput comparison.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Pulls astroBWTv3Stream's 256-case per-position transform switch out
into its own function, byte-for-byte unchanged. This is the ground
truth every later fast path in this area gets derived from and
verified against, not a separately-maintained copy: correctness of
anything built on top reduces to "does it match applyBranchOp",
checkable directly against DEROHE's own reference rather than any
external source.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Classifies each of the 256 branch-loop opcodes by exhaustively testing
applyBranchOp against every (input byte, step_3[pos2] byte) pair --
65536 evaluations per op, not a sample -- into: always-zero-output,
output-independent-of-step_3[pos2] (safe to precompute as a 256-entry
table), or genuinely needs step_3[pos2] at each position. Derived
entirely from applyBranchOp itself, not ported from any external
source; cross-checked against Dirtybird-C-Miner's own (syntactic, more
conservative) classification during development and found 6 ops
provably always-zero where source inspection alone would call them
op-count-dependent -- see the algebraic proof in the commit that wires
this in.

Unwired from astroBWTv3Stream for now; nothing calls opClass/opLUT yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Routes each branch-loop op through opClass's classification: a plain
loop-clear for the always-zero ops, a single table lookup per byte for
the step_3[pos2]-independent ops, and applyBranchOp unchanged for
everything else. forceScalarBranchOp gives tests a way to force every
op through applyBranchOp for a direct differential comparison against
the fast paths, independent of the hardware they happen to run on.

Caught a real bug before this ever landed: ops 254/255 carry a
mandatory RC4 re-key that isn't part of their step_3 transform, so
their transform alone tests as step_3[pos2]-independent -- but a fast
path bypassing applyBranchOp would silently skip the re-key. The
generator now forces those two (and 253, which already fails the
independence test on its own transform) to the scalar path
unconditionally, regardless of what the transform-only scan finds.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
For the ops that don't decompose into a static lookup table (their
transform reads step_3[pos2], so no fixed input->output mapping
exists), derives which 4-step sequence over a 16-symbol vocabulary
(add-self, xor-pos2, variable shift/rotate, popcount, etc. -- the same
primitives AVX2 can vectorize across a 32-byte window) reproduces
applyBranchOp's output. Brute forces all 16^4 candidates per op,
screens against sampled inputs, then verifies every surviving
candidate exhaustively across all 256x256 (input, step_3[pos2]) pairs
before accepting -- derived from DEROHE's own switch, not ported from
Dirtybird's CodeLUT (spot checks against it agreed exactly, but the
sequences here are independently re-derived, not copied).

Found a decomposition for all 99 eligible ops (every opClassScalar op
except 0/253/254/255, which have side effects or a cross-iteration
dependency outside this per-byte-transform model entirely). Unwired:
nothing calls opSubSeq yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Four building-block AVX2 kernels (bit-reverse, popcount, per-byte
self-multiply, per-byte variable left-shift), each independently
differential-tested against a scalar Go reference before being trusted
to compose into anything larger. The variable shift uses the standard
multiply-by-2^count-via-VPSHUFB-lookup trick, split into even/odd byte
lanes so 16-bit VPMULLW doesn't let one byte's product bleed into its
neighbor.

Right-shift and rotate aren't separate primitives: srl(x,k) =
reverse8(shl(reverse8(x),k)) (right-shift-by-k equals reverse, then
left-shift-by-k, then reverse again -- verified against this package's
own composition, not assumed), and rotate composes shl|srl. Fewer
independent pieces to get right in hand-written assembly, at the cost
of the extra reverse8 calls -- correctness took priority over shaving
instructions at this stage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Vectorizes the remaining ops' per-position transform across a 32-byte
window using the derived opSubSeq sequences over the verified
primitives, gated on cpuid AVX2 support and a pos1+32<=len(step_3)
bounds check (a 32-byte SIMD load/store near the end of the 256-byte
buffer would otherwise read past it -- costs roughly 12.5% of eligible
iterations, falling back to applyBranchOp for those). Op 0's swap and
ops 253/254/255's side effects are excluded from opSubSeq entirely
(see the earlier derivation commit), so they always take the scalar
path regardless of AVX2 availability.

Fully unrolled -- no CALL/RET anywhere in the per-op dispatch, every
primitive inlined at each of the 4 sequence positions -- after an
earlier CALL-based version (subroutine per primitive, shared across
all 4 positions) measured statistically indistinguishable from no-AVX2
at all (~0.45% on a 20x50-rep benchstat, inside the ~1% CI). The
hypothesis was call overhead through the nested
dispatch->rol->srl->reverse chain (up to 6 nested calls for a single
rotate-based sub-op); removing it entirely changed nothing
(2.437ms -> 2.426ms, same ~0.45%). Kept as verified-correct,
zero-risk-to-merge infrastructure: every fast path here falls back to
the untouched applyBranchOp by construction, so it can't be wrong even
though it doesn't measurably help on this hardware. Real speedup, if
any exists, needs different hardware or a different lever entirely --
not another rewrite of this same kernel shape.

Differential-verified against applySubOpSeq (200k arbitrary-sequence
trials) and applyBranchOp directly on real derived per-op sequences
(~198k trials across all 99 eligible ops).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
mineblock() now hashes two nonces per iteration through AstroBWTv3Pair
instead of one through AstroBWTv3, for blocks past MAJOR_HF2_HEIGHT (the
pre-HF2 branch, using the older astrobwt_fast.POW_optimized, is untouched).
A second work buffer (workB) is synced from the job template alongside the
existing one; each iteration advances the nonce counter twice, hashes both
buffers together, and checks/submits each result independently against the
current difficulty.

The underlying kernel (AstroBWTv3Pair, sha256mb_amd64.s) was already
merged and differential-tested; this was the one remaining piece -- actually
calling it from the real mining loop -- and had been sitting as an
uncommitted local diff, live-verified once already on real SHA-NI hardware
(core, AMD Ryzen 9800X3D: kernel-level ~1.5x, live miner A/B against
mainnet infra +6-7%) but never committed. On hardware without SHA-NI,
AstroBWTv3Pair falls back to two ordinary hashes internally, so this is
correct either way with no capability check needed at the call site.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
sort_indices (sa_fast.go) is an alternate SA-construction path that was
already measured slower than production and never wired into any real
hash path -- its only caller is BenchmarkSortIndicesFastPath_Realistic,
informational only. But its two backing arrays (indices/tmp_indices,
768KB combined) and the stage1_result/stage1_result_bytes pointers
aliasing into them were fields on ScratchData, the struct every pooled
mining-thread scratch object carries -- live, dead weight on every real
hash, confirmed by grepping for any production reference (there is none).

Moved indices/tmp_indices out to caller-supplied parameters instead
(sort_indices now takes them directly), with the one live test caller
allocating its own local buffers. ScratchData shrinks from ~1249KB to
~481KB per worker. Full package test suite green; no behavior change to
any production code path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
templateSAScratch's runs/radixTmp/groupPos/mergePos/runLens/nextLens
buffers were all sized at MAX_LENGTH (98,303) to cover their theoretical
worst case, but that worst case is far from what real AstroBWTv3 output
produces. Measured across 200,064 hashes (200,000 random MINIBLOCK-sized
inputs plus every real captured fixture, TestMeasureTemplateSAUsage, not
committed -- throwaway measurement tooling, kept only as the numbers
below):

  - runs/radixTmp:  observed max 28,358 (p99.99 27,097) vs MAX_LENGTH 98,303
  - groupPos/mergePos/runLens/nextLens: observed max 924 vs MAX_LENGTH 98,303

New capacities: stage5RunsCapacity = MAX_LENGTH/2 = 49,151 (~1.7x margin
over observed max), stage5MergeCapacity = 4,096 (~4.4x margin over
observed max). arena is left untouched -- its real utilization (66,119
observed vs 98,303 worst case, ~67%) is close enough to its bound that
shrinking it isn't worth the added decline risk for a modest gain.

templateSAScratch drops from 3.38MB to 1.19MB per worker (combined with
the previous commit's ScratchData cleanup: ~4.63MB -> ~1.67MB per worker
overall, a 2.77x reduction).

Correctness: runs/groupPos/runLens/nextLens are append-based, so Go's
slice growth already makes exceeding the new smaller capacity safe by
construction (a rare real allocation, not a bug). radixTmp and mergePos
are indexed directly (not via append), so writeFusedRunsToSA gained two
explicit decline guards before each is used -- same "clean fallback to
divsufsort, never a wrong answer" contract as every other decline point
in this file (arena overflow, stage4MaxGroupRun). Also fixed
TestTemplateSAZeroAllocsAfterWarmup, which used synthetic uniform-random
data (no structural correlation, unlike real wolf-loop output) shaped
into artificial templates -- decorrelated enough to trip the new guard
even though real traffic doesn't. Switched it to the same real-fixture
data BenchmarkTemplateSAStageOnly_Realistic already uses. Added
templateSAKWayMergeHits (mirrors the existing templateSAFallbacks
counter) for observability into how often the k-way-merge path fires.

Full package test suite green, including the 1M-hash-scale differential
gates against divsufsort. Real dero-miner --bench on the dev machine
(i9-9900K) showed no measurable throughput change from either this or the
previous commit (~7415 H/s before, ~7415-7481 H/s after, within run-to-run
noise) -- this is a memory-footprint reduction, not a compute reduction,
and this machine's L3 doesn't appear to be the bottleneck it was
hypothesized to be. Kept regardless: real, safe, fully verified reduction
in per-worker memory footprint, independent of whether it moves this
CPU's hashrate number.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant