The power limit and 810 MHz memory lock were applied at boot as a host
setting, so they outlived the miner: a host switched back to LLM
inference kept the lock and decoded at a sixth of its measured speed,
with nothing reporting as throttled.
- quantus-miner.service applies the profile in ExecStartPre and resets
to driver defaults in ExecStopPost (nvidia-power-limit.sh, installed
by lair/quantus). ExecStopPost runs on stop, crash and failed start.
- deploy: on a host with neuron.service enabled, ship the files but do
not enable or start the miner; validate passes there without checking
hashing. neuron conflicts with the miner, so starting it would stop
inference behind the operator's back.
- bench-on-host.sh: stopping the unit now resets the clocks, so apply
the miner profile for the measurement window (and read the asserted
power limit after it), restore the prior profile afterwards, and
refuse to measure on a host where neuron is active.
- infra-setup.sh: sudoers for the two script invocations the bench
needs. Re-run it on the mining hosts before the next deploy; the
preflight will name the missing path until then.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XViS3RVfGTRaRSopLN68MA
MiningUniforms carries the twelve diagonal constants (416 bytes now) and
the loop kernel reads them there (LAIR_LOOP_PARAM_DIAG=1) instead of from
the __constant__ table. Static: still 13 loads per internal round (ptxas
reloads either way) but spill 88 -> 32 bytes. Measured, three interleaved
rounds, loop kernel only: beast 2135 -> 2162 MH/s (+1.3%), benjy 725 ->
730 (+0.7%); parity 300/300 and 200/200. The unrolled kernel is byte-
identical (21,120 instructions, 110 registers).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ue5ZZm1Hiv5zPnucykKKuF
Constant hoisting in C and 32-bit-halves delivery of the diagonal both
compile to byte-identical SASS and are removed. The remaining 40
instructions per round over the quanpool kernel are itemised in the
kernel comment.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ue5ZZm1Hiv5zPnucykKKuF
A second entry point in the same fatbin, sharing every arithmetic
primitive with mining_main, with the round loops rolled and the round
constants fetched from constant memory by index (quantus/miner#27). The
per-nonce body is one template used by both entries, so the deployed
kernel is byte-identical (sm_120 21,120 instructions, 110 registers).
Selected at run time: MINER_CUDA_KERNEL=loop loads mining_loop, reports
kernel id cuda-loop on the per-device metrics, and defaults the block
size to 512 (LAIR_LOOP_TPB); unset or unrolled keeps mining_main and the
cuda kernel id the deploy validate asserts.
Measured on beast, three interleaved rounds, parity 500/500 on both:
unrolled 2206, loop 2130 MH/s (-3.4%). The loop kernel is 11,840 static
instructions at 128 registers (88 bytes of spill) on sm_120; 88 registers
with no spill on sm_86/89. Shapes tried for it: 192 registers 1976 (one
block per SM, card underfilled), 80 registers 2005 (424 B spill), 64
registers 373-532 (716-872 B spill), two internal rounds per iteration
1976 against one at 2105, 256/128-thread blocks below 512.
Not the win #27 hoped for yet: the loop structure alone does not lower
this arithmetic's register footprint the way the quanpool kernel's is,
and its internal round is 254 instructions to their 199. Kept as an
opt-in path so the next steps (the round arithmetic as one carry chain)
can be measured against the deployed kernel without touching it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ue5ZZm1Hiv5zPnucykKKuF
Three scheduling changes measured against the #23 kernel, three interleaved
rounds each, parity clean on every host:
- Batches are rounded up to whole grids, two per batch by default
(MINER_CUDA_BATCH_ALIGN, MINER_CUDA_BATCH_WAVES). The CLI's 1M-nonce
batch left a half-empty tail wave and paid the launch gap every 1M
hashes. beast +1.6% for one grid, +0.4% more for two; benjy +0.5% for
the second grid; quadbrat neutral.
- LAIR_EARLY_REJECT: the second permutation stops before its last linear
layer, computes output 0 alone (the hash's top 64 bits) and rejects on
it; only the rare survivors pay for the other eleven outputs. +0.6%.
- LAIR_NONCE_DIR: the host precomputes the state after the first linear
layer for the batch's first nonce (first_layer_after_absorb, unit test
against the matrix column), and the kernel derives each nonce's state
as that plus a scalar times column 7 of the external matrix; nonces
past a carry out of the low limb take the old path. +0.25% on beast,
+0.6% on the 4090 and 3060.
Also tried and rejected: 512-thread blocks (-0.4%), a smaller grid via
MINER_CUDA_THREADS_PER_SM=16384 (-0.6%), a larger one (65536, within
noise of two waves), and the whole S-box as one PTX block, which
compiles to byte-identical SASS. The threads-per-block knob
(MINER_CUDA_THREADS_PER_BLOCK with LAIR_TPB) stays for experiments.
beast 2x5090: 2146 -> ~2206 MH/s (+2.8%); benjy 4090: 711 -> ~718;
quadbrat 3060: 140.2 -> 141.2.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ue5ZZm1Hiv5zPnucykKKuF
The closed-source qpow-cuda kernel runs 2x our hashrate on the same
silicon with the same 1472 multiplies per hash. Its PTX shows why: every
field operation is one self-contained inline-PTX block, the 128-bit
product is reduced with mad.lo.cc/madc.hi.cc (one IMAD.WIDE with a
carry-out) plus three ALU ops, the internal layer's row sum rides in the
mad.wide partial products for free, and accumulators are 32-bit limb
chains that ptxas turns into IADD/IADD.X with merged carries. Ours paid
a compare-and-select reduction (~14 ops), a 64-bit add-with-carry per
diagonal element, and five instructions per accumulator add on sm_120.
This ports those primitives behind LAIR_FUSED_MUL (default 1): gf_mul,
gf_sqr (three-product squaring), gf_mul_add with the addend folded into
the partial products, acc_add/acc_add2 as limb chains, and a four-op
acc_reduce. The round constant now joins the unreduced sum in the
internal layer.
LAIR_EXACT_REDUCE (default 0) keeps the reduction bit-exact for three
more ops per multiply. With 0 the final borrow is dropped, as in their
kernel: wrong only when the product's bits 64..95 are zero and its low
64 bits are below its top 32 bits, about 2^-64 per multiply (emulated
against a reference: 0/3M random mismatches, 2^48*2^48 fails as
predicted). Exactness measured at -15% on beast, so it is off.
Measured, three interleaved rounds, parity 40/40 on each host:
beast 2x5090 1356 -> 2146 MH/s (+58%)
benjy 4090 460 -> 711 MH/s (+54%)
quadbrat 3060 84 -> 140 MH/s (+66%)
Static sm_120 31,280 -> 20,888 instructions, sm_86/89 39,208 -> 22,576,
no spills. ncu: 22,913 instructions per nonce against their 22,233.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ue5ZZm1Hiv5zPnucykKKuF
An `asm` block is opaque to nvcc's optimiser, so carry arithmetic written
as inline PTX blocks common-subexpression elimination and strength
reduction across neighbouring field operations. On Blackwell that costs a
quarter of the kernel. On Ada and Ampere the hand-written sequence still
wins, by a similar margin in the other direction, so the default is now
chosen per device pass.
Whole-kernel instructions (ptxas + cuobjdump, CUDA 13.0) and measured
hashrate, three interleaved rounds each:
sm_120 sm_86/89
PTX_CARRY=1 37,432 1127.1 MH/s 37,027 benjy 461.6 quadbrat 85.5
PTX_CARRY=0 27,837 1358.0 MH/s 41,196 benjy 401.5 quadbrat 76.3
sm_86/89 keep the exact configuration they had, so those cards are
unchanged by construction -- identical macro values, identical 37,027
instructions. Parity 60/60 against the CPU on beast.
LAIR_PTX_ACC is now 1 everywhere rather than following LAIR_PTX_CARRY. In
the Acc accumulators the add-with-carry pair is the whole operation, so
there is nothing around it for the optimiser to fold; on sm_120 with the
carry path off, Acc PTX on measured 1333.3 against 1285.8 with it off.
PR #17 measured the PTX carry path as a win on every card. That was true
of the kernel it was written for, which spilled 104 bytes per thread;
once the spills went the trade reversed on Blackwell only.
Found by profiling rather than reading: ncu puts us at 43,700 executed
instructions per nonce against the rival binary's 22,229 on the same
card, a 1.97x ratio that matches the 2.05x throughput gap, with 82% of
the excess on the ALU pipe rather than the multiply pipe. This lands
35,785. The remaining gap is still ALU: 26,586 against their 14,413,
while the multiply pipe is now close to parity at 8,405 against 7,030.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ytCkttecH9SNTUEw8VT9D
The three-multiply squaring is a real algebraic saving and a measured
loss. Implemented, verified bit-exact against __int128 over 40M random
values plus limb and power-of-two boundaries, then benchmarked on beast:
1072.9 vs 1137.7 MH/s over three interleaved rounds, 5.7% slower.
It trades 6.7% of the kernel's widening multiplies for 11% more
instructions -- 5 IMAD.WIDE in 56 instructions against the general
product's 6 in 48 -- and four formulations (condition-code carry, compare
carry, two mad.wide.u32 variants) all compiled to the same 5/56, so that
is the floor rather than a tuning failure.
Taken with the LAIR_INT_UNROLL result, the picture is consistent: this
kernel is bound by instruction issue and by the cards' power envelope,
not by multiply throughput or occupancy. Re-expressing the same work
cannot win; only removing work can. Left as a comment so the next person
does not spend the afternoon rediscovering it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ytCkttecH9SNTUEw8VT9D
The kernel copied midstate, target and the nonce base into per-thread
register arrays and held all 56 registers live across the whole nonce
loop, which is exactly what the permutation's own working set needed.
ptxas spilled 104 bytes per thread on sm_120 and 44 on sm_86/89; the SASS
carried 26 LDL + 26 STL inside the hot loop.
They are launch-uniform, so they now travel as a by-value MiningUniforms
parameter. Parameters live in constant memory -- broadcast and cached, no
register residency -- and arrive with the launch, so three memcpy_htod
calls per batch are gone with them. Only the low eight nonce limbs are
materialised now; on a hit the high half is written from the parameter
bank. 128 -> 80/80/106 registers on sm_86/89/120, zero spill everywhere.
Both sides assert the 224-byte layout, so a mismatch is a build error
rather than a silent mining bug.
Measured A/B on the fleet, same session, same silicon, parity verified
against the CPU (25/25 on quadbrat and benjy, 40/40 on beast):
quadbrat 3060 83.45 -> 84.60 MH/s +1.38%
benjy 4090 457.87 -> 461.77 MH/s +0.85%
beast 2x5090 1133.97 -> 1138.79 MH/s +0.43%
fleet 1675.29 -> 1685.16 MH/s +0.59%
Not taken: a per-arch LAIR_INT_UNROLL of 1 on sm_120. It is better by
every static measure -- 78 registers instead of 106, three resident
blocks per SM instead of two, no spill either way -- and measured 0.92%
slower on beast over three interleaved rounds. These cards are
power-bound, not latency-bound: the extra warps buy power draw and the
card clocks down to pay for it. Benjy shows the same thing from the other
side, delivering +0.85% at a lower clock (2085 vs 2160 MHz), +4.4% per
MHz. Occupancy is not the lever here; work per hash is.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ytCkttecH9SNTUEw8VT9D
Each linear layer now folds the constant of the round that follows it into
its final deferred-carry reduction (or, for the internal rounds, into the
element-0 multiply-add), removing the per-element gf_add before every S-box.
Parity verified. On benjy the effect is within run-to-run noise: three
interleaved rounds gave medians of 418.5 vs 409.7 MH/s against the previous
default, with SM clock swinging 1935-2025 MHz across rounds. Kept on as
LAIR_FOLD_RC=1 for the instruction count; not claimed as a measured gain.
bench.yaml: crates/engine-cuda/** now triggers the harness on PRs. The
previous PR's kernel changes did not run it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBgs2nSi4H2mdh8kD8vMX5
quantus/miner#3 step 5, measured on benjy (RTX 4090, 250 W, miner paused,
interleaved rounds, parity verified on every variant):
variant MH/s
straight port (PR #16) 297-305
+ grid cap 32768 threads/SM 304 (1 nonce per thread up to 4M)
+ deferred-carry linear layers 344 (+13%)
+ carry-flag gf_add / gf_reduce (PTX) 385 (+12%)
+ carry-flag accumulators 414 (+7%)
414 MH/s is 2.86x origin's wgpu build (144.4) on the same card and limit.
Fewer instructions per hash also let the power-capped card clock higher
(1920 -> 1995 MHz), which is part of the gain.
Rejected by measurement: __noinline__ permute (-13%: code size was not the
limit, the call convention was), -maxrregcount 80/64 (flat), internal-round
unroll 1 or 22 (flat, within noise of 2).
The batch sweep found batch size irrelevant once the grid keeps one nonce
per thread (304-306 MH/s from 1M to 16M); batch stays at 1M for the
smallest stale cost. All knobs remain as -D macros and MINER_NVCC_FLAGS
so the harness can keep testing variants.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBgs2nSi4H2mdh8kD8vMX5
quantus/miner#3. New crate, nothing in engine-gpu touched.
- kernels/mining.cu: the same host contract and sponge schedule as
mining_u64.wgsl (host midstate, low-256-bit nonce increments, lazy second
squeeze), bit-exact with pow_core. The field multiply is __umul64hi plus a
plain multiply and one reduction, instead of four 32-bit partials with
carry reconstruction.
- build.rs: generates poseidon2_constants.cuh from qp-poseidon-constants
(a hash change at origin is a dependency bump, never a hand-copied table)
and, when nvcc is present, compiles one fat binary with cubins for
sm_86/89/120 plus compute_120 PTX. Without nvcc it writes an empty image,
the crate still builds, and CudaEngine::try_new fails with a clear message
so the miner falls back to wgpu.
- lib.rs: CudaEngine with the same batch loop, cancellation, thread-local
device assignment, metrics (kernel label "cuda") and stale accounting as
engine-gpu. cudarc 0.19 with dynamic loading; no link-time CUDA dependency.
- miner-service: resolve_gpu_configuration tries CUDA first unless
--gpu-engine wgpu; --gpu-engine cuda makes its absence an error.
miner-cli gains the flag on serve and benchmark (MINER_GPU_ENGINE).
- bench-harness: --engine auto|cuda|wgpu.
- deploy.yaml: matrix rows carry the kernel validate expects; a wgpu
fallback on a CUDA host now fails the deploy instead of passing at a
fraction of the hashrate.
Measured on quadbrat (RTX 3060, 130 W, miner paused, 2 x 10 s windows,
parity 5/5 verified against CPU): wgpu 37.3 MH/s, CUDA 59.5 MH/s (1.59x).
Grid sizing 8192 threads/SM or more is flat; 2048 loses 10%.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBgs2nSi4H2mdh8kD8vMX5
miner_stale_hashes_total was recorded in the QUIC loop for every result whose
job id no longer matched, which is every cancelled search: 74% of all hashes
on benjy flagged as stale on the first hour of data. That is the loop's
notion of a stale *result*, not wasted work; the hashes were done while the
job was current.
The wasted work is the batch that completes after the job was superseded.
Record that in the engine at the cancellation check, per device and kernel,
and drop the loop-level accounting. Expect one batch per job switch.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBgs2nSi4H2mdh8kD8vMX5
quantus/miner#9. Additive metrics in a lair-owned module of the metrics
crate; origin's metrics are untouched so the fleet dashboard keeps working
across origin merges.
Identity:
- miner_build_info{version, commit}: the join key for everything below.
- miner_config_info{engine, gpu_batch_size, gpu_devices, cpu_workers,
gpu_throttle_ms}: two deploys of one commit with different flags are
different experiments.
Per device (handles resolved once at engine init, no label lookups in the
batch path):
- miner_device_hashes_total{device, kernel}, miner_device_solutions_total,
miner_device_lost_total.
- miner_gpu_batch_seconds{device, kernel, phase=gpu|host}: submit-to-mapped
on the device versus everything else in the batch. This is the bubble
#4, #5 and #6 attack, measured directly.
Jobs and results:
- miner_jobs_received_total, miner_stale_hashes_total{engine},
miner_job_pickup_seconds{engine} (issued to picked up; the cancel latency
of a busy worker), miner_results_submitted_total,
miner_results_send_failed_total, miner_seal_latency_seconds (found to
sent), miner_job_idle_seconds_total (sent to next job, node-attributable).
Connection: miner_connects_total, miner_connect_failures_total,
miner_disconnects_total, miner_connected, miner_disconnected_seconds_total.
Origin-owned code touched, each block marked lair: engine-gpu gains a
metrics dependency, a DeviceMetrics handle on GpuContext and timing points
in run_single_batch; miner-service gains found_at on WorkerResult,
created_at on MiningJob and the recording calls; miner-cli sets build info
at startup. Deploy validate now asserts miner_build_info carries the
deployed commit.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBgs2nSi4H2mdh8kD8vMX5
Gitea's concurrency group did not serialise two runs on benjy: the second
run's scp failed with ETXTBSY on the binary the first was executing (safe,
it happened before the miner was touched, but the run was lost). The pause,
lock, measure and resume sequence now lives in bench-on-host.sh, executed
over one ssh call under flock on the host, with per-run binary and record
names so staging never collides.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBgs2nSi4H2mdh8kD8vMX5
Two threads on one card overlap one thread's readback with the other's
dispatch, which is the bubble quantus/miner#4 and #5 target. Measuring it in
the harness first means #4 lands with a number instead of a hypothesis.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBgs2nSi4H2mdh8kD8vMX5
quantus/miner#2. The measurement gate every kernel and submission change
passes before merge.
crates/bench-harness (lair-owned, never touched by origin merges):
- drives GpuEngine through MinerEngine, one worker thread per card, with
fixed timed windows after a warm-up; reports per-worker median MH/s and
spread and refuses (exit 5) when spread exceeds 2%
- captures nvidia-smi state (driver, power limit, draw, clock, temperature)
before and after, and refuses to run (exit 3) when the enforced power limit
is not the expected one, since that is the largest confound on these cards
- GPU/CPU parity on random jobs, same shape as engine-gpu's gpu_cpu_parity
example, exit 4 on any mismatch
- one JSON record per run (schema 1) plus a markdown summary on stdout
- build.rs embeds the commit like miner-cli's
.gitea/workflows/bench.yaml:
- runner containers on the GPU hosts have no device passthrough, so the
binary is built on cuda-13.0 (Fedora 43, matching the hosts; the rust
image is Fedora 44 and its binaries need a newer glibc) and executed on
the host over ssh as gitea_ci
- stops quantus-miner.service for the window and starts it again under a
trap; refuses to measure a card that is busy with the miner stopped
- benjy (4090, dedicated) by default; host, duration, runs, batch size and
workers are dispatch inputs; PRs touching engine or service crates trigger
it and get the summary as a comment
- one measurement per host at a time (concurrency group)
The stop/start sudoers grants were added to lair/quantus infra-setup.sh
(lair/quantus#5) and applied to benjy and quadbrat.
Origin coupling: one line in the workspace members list, marked.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBgs2nSi4H2mdh8kD8vMX5
First lair-only change on top of origin v4.0.2 (quantus/miner#8, #1).
- Delete .github/ (workflows and the disk action). Gitea Actions reads
.github/workflows too, so origin's GitHub-hosted jobs would queue forever
for runners that do not exist here. Standing resolution on every
origin-main merge: `git rm -r .github/workflows`.
- Add .gitea/workflows/ci.yml on the `rust` runner: fmt, clippy with
-D warnings (same invocation as clippy.sh), build, test, doc, and a check
that the built binary reports a commit in --version.
- crates/miner-cli/build.rs embeds MINER_BUILD_SHA (env override, else
git rev-parse with a -dirty marker, else "unknown") and --version prints
"<semver> (<sha>)". A deploy that follows main needs to assert the running
binary is the commit it shipped; the semver alone cannot do that.
Origin coupling: .gitea/ and build.rs are new files; main.rs gains one const
and one attribute argument behind a `// lair:` marker.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBgs2nSi4H2mdh8kD8vMX5
* Select Apple Metal 4.0.1 kernel; keep 4.0.0 u64 elsewhere
NVIDIA was ~37% slower on the 4.0.1 Metal-tuned kernel. Split
Poseidon2 kernels and pick by backend:
- Metal + SHADER_INT64: v4.0.1 u64
- other SHADER_INT64: v4.0.0 u64
- no SHADER_INT64: 32-bit fallback (same as 4.0.0)
CI uploads the Linux release binary as an artifact so we can
bench NVIDIA without building on the GPU box.
* Drop version numbers from GPU kernel labels
Log native-u64 vs native-u64 Apple Metal without tying
the strings to release tags.
Apple M5 Pro, CLI benchmark: 24.0 -> 43.2 MH/s. Kernel output is
unchanged and bit-exact against the CPU reference.
Field arithmetic:
- Deferred-carry additions (Acc): the external layer and the internal
row sum accumulate unreduced with a carry counter and fold once per
output instead of two epsilon corrections per add.
- gf64_mul_add folds the row sum into the 128-bit product before a
single plain reduction.
- mul_wide / gf64_sqr assemble the 128-bit product from 32-bit-valued
partial sums with no carry compares.
Kernel structure (code size dominates on Apple's compiler; every
unrolled variant measured slower):
- One loop drives all 30 rounds so each layer is emitted once, with
round constants added by the linear layer preceding each S-box
(RC_EXT table, RC_INTERNAL padded with a trailing zero).
- One S-box site with a runtime lane count (12 external, 1 internal).
- mining_main runs pad / first squeeze / second squeeze through a
single inlined permute64 call in a phase loop.
Measured and rejected: uniform buffers and unhoisted loads, workgroup
sizes 64/128/512, 8-add MDS, one-compare reduce, fully unrolled
layers, two nonces per thread.
* Send miner auth token in Ready handshake.
Require --auth-token or --auth-token-file when connecting to a node so the
miner matches the node's shared-secret QUIC auth.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Pin the node's miner TLS certificate by SHA-256 fingerprint.
Require --tls-cert-sha256 or --tls-cert-sha256-file so miners reject
MITM certs instead of accepting any self-signed server certificate.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fmt
* Docs: prefer reading miner auth token from file, not node logs.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Version the miner ALPN as quantus-miner/2 to match the node.
A node/miner protocol mismatch now fails cleanly at the TLS handshake with
"no application protocol" instead of an opaque auth error.
Co-authored-by: Cursor <cursoragent@cursor.com>
* point at git
* Depend on published quantus-miner-api 0.3.0 from crates.io.
The temporary git branch pin is no longer needed now that 0.3.0 is published.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fail closed on permanent auth/TLS misconfig instead of reconnect looping.
Validate the fingerprint and Ready frame size at miner/pool startup before
metrics, workers, or HTTP come up. Treat node auth rejection and pin mismatch
as PermanentConnectError (no retry), and only reset reconnect backoff after
the first NewJob proves auth succeeded.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Point miner auth docs and examples at miner-auth-token / TLS pin files.
The node never logs the auth token; CLI/pool help and every serve/Docker
example now require the chain config files (or env vars) instead of implying
credentials can be copied from logs.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fmt
* Remove the miner Dockerfile and Docker packaging docs/CI.
Container GPU access is a poor fit for this miner, and the Docker examples
were unusable with SocketAddr (hostnames rejected). Run the native binary
instead.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Shut down the pool on permanent upstream auth failure; keep reconnecting otherwise.
Supervise run_node_client alongside HTTP so a bad token/pin ends the process
instead of leaving a live API on a dead upstream. Node restarts still reconnect
with backoff; the current job is cleared while disconnected so captchas are not
issued against a stale header.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Replace obsolete EXTERNAL_MINER_PROTOCOL.md with a pointer to MINING.md.
The local copy still described the pre-auth protocol (no Ready token, no pin,
old ALPN, 16 MB frames). Canonical docs live in quantus-miner-api and the
node's MINING.md.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Delete EXTERNAL_MINER_PROTOCOL.md; README already points at MINING.md.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Investigating reported seal submission failures. Root cause analysis:
the miner's job-id staleness chain (worker check, quic-loop filter,
node-side job_id compare) cannot mislabel a candidate — internal and
node job ids are updated atomically together. The remaining benign race
is node-side: a block-template rebuild (new pre_hash, same parent) does
not bump the node's job counter, so an in-flight seal mined on the old
template passes the job-id check and fails seal verification, logging
"Failed to submit seal". Fixing that belongs in the chain repo (bump the
job counter on template rebuild).
Miner-side hardening in this commit:
- gpu_cpu_parity is now a two-phase fuzzer. Phase 1 dumps FULL 512-bit
hashes for randomized (header, nonce) pairs via the real
midstate-resume datapath and verifies each against the canonical CPU
implementation, multi-threaded (defaults to 1M hashes, ~5s; 10M runs
in ~15s). Phase 2 fuzzes whole mining jobs with five profiles: random,
carry-edge starts (saturated low limbs), 2^256-boundary crossings,
impossible difficulty with exact hash-count assertion, and CPU-known
solutions to catch false negatives. Seeded and reproducible; also
asserts the submitted work bytes match the nonce.
Verified clean: 10M bulk hashes + ~1,500 seals across 6 seeds.
- miner-service re-verifies every candidate on the CPU (one hash) before
sending JobResult; an engine-produced invalid seal is never submitted
and is logged loudly instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The benchmark fed each worker a fixed range (10k CPU / 1M GPU nonces),
capping GPU dispatches at 1M regardless of --gpu-batch-size, and
re-searched the same range on every iteration. Worker ranges now step
through the nonce space and are sized from the batch flags, floored at
the old widths so tiny flags keep engine-internal cancellation batching
instead of turning per-call harness overhead into the measured quantity.
Batch-size flags are validated non-zero at parse time (serve and
benchmark) and echoed in the benchmark banner.
Exploring a fully native Metal port of the mining kernel showed it runs
~16% faster than the wgpu path at equal batch size. Decomposing that gap:
~9% is naga's injected runtime bounds checks and forced loop bounding,
~1.5% is mulhi()-based multiplication (inexpressible in WGSL), <1% is
the constant address space; the rest is binding/codegen residue.
The dominant term is recoverable inside wgpu on every backend:
the engine now builds its pipelines with create_shader_module_trusted +
ShaderRuntimeChecks::unchecked(). This is sound for our shaders: sources
are compiled into the binary, every buffer access is a constant-bounded
loop index into fixed-size bindings the engine allocates itself, all
loops have static bounds, and both variants remain covered by the
dual-shader component suite and the CPU parity example.
Apple M4: 11.39 -> 12.34 MH/s at the default 1M batch (+8.3%), 11.74 ->
12.73 MH/s at 8M (+8.4%). Cumulative vs main: 4.4x.
New examples/trusted_hashrate.rs measures checked vs trusted on the same
shader and CPU-verifies results.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Evaluated all ideas from #80 against the native-u64 kernel. Its three
main wins (lazy field arithmetic, precomputed sponge state, conditional
second squeeze) were already present in stronger form; two micro-opts
carried over and measured positive at large batches (+1.3% at 16M,
noise-level at the 1M default):
- Hoist midstate/target/start-nonce storage reads out of the nonce loop.
- Produce byte-swapped hash words on demand during the target compare,
so the reject path skips building the swapped hash entirely.
Forcing nonces_per_thread=2 was also tested and regressed; one thread
per nonce stays.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three stacked optimizations, each verified bit-exact against the CPU
reference (qp-poseidon-core) by the dual-shader component suite and a
new GPU/CPU parity example:
- mining_u64.wgsl: native 64-bit Goldilocks arithmetic with plonky2-style
lazy reduction, used automatically when the adapter supports
SHADER_INT64 (all Apple Silicon, NVIDIA, modern AMD). mining.wgsl
remains the 32-bit fallback. (+50%)
- Midstate precompute: the first two of five sponge permutations absorb
the header and high nonce half, both constant per batch; they are now
computed once on the CPU (pow_core::mining_midstate) and resumed on
the GPU. Batches are clamped so nonces never carry past 2^256. (+77%)
- Lazy second squeeze: the first squeeze yields the most significant 256
bits of the hash, which decide hash-vs-target unless exactly equal to
the target's high half, so the common reject path skips the final
permutation. (+56%)
Apple M4 throughput: 2.81 -> 11.4 MH/s at the default 1M batch
(criterion large_range_1m/gpu: 355.8ms -> 86.3ms, -75.7%).
Also:
- Fix criterion GPU bench crash: thread-local worker resources are now
tagged with an engine id so multiple GpuEngines per process never mix
devices.
- Fix stale end-to-end test harness (6-binding layout) and run the full
component suite against both shader variants.
- New examples: hashrate, gpu_cpu_parity (25 jobs incl. 2^256-boundary
crossing), gpu_features.
- Add Apple M5 family GPU tiers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Add captcha share pool service and browser WASM solver
pool-service connects to a node over the external-miner QUIC protocol and
turns browser captcha solves into real mining shares: sessions with disjoint
nonce ranges over the current header, single-use share tokens, a
reCAPTCHA-shaped /siteverify endpoint, and upstream block submission when a
share meets full network difficulty. solver-wasm is a raw C-ABI wasm32 build
of the Poseidon2 nonce grinder (no wasm-bindgen) for the embeddable widget.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fmt
* Re-queue block solution when upstream write fails
A network-difficulty share taken off the solution channel was permanently
lost if the JobResult write to the node failed: the connection loop broke to
reconnect but the block was no longer anywhere. Park the block in a pending
slot that survives reconnection and is retried before any other work.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fmt
* Address PR review: capacity limits, loud block drops, shared QUIC transport, constant-time secret compare
- Cap live sessions and tokens (configurable via --max-sessions/--max-tokens,
default 100k each); /api/session returns 503 at_capacity so a request flood
can no longer grow the maps without bound.
- A block solution that fails to queue upstream is now logged at error level,
and blocks_found/block_found only report solutions actually queued.
- Extract the node QUIC connect + insecure-verifier pattern into a shared
quic-transport crate used by both miner-service and pool-service; derive
the network target via pow_core::JobContext instead of re-deriving it.
- Compare the site secret in constant time (constant_time_eq).
- Note single-tenant token semantics on site_secret for the host-registration
follow-up.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Add per-IP rate limiting on /api/session issuance
The global session cap stops unbounded map growth, but a single client could
still churn entries at the TTL boundary. Limit each IP to 60 issuances per
minute (configurable via --sessions-per-ip-per-min); excess requests get 429
rate_limited. Stale IP windows are GC'd alongside sessions/tokens.
Co-authored-by: Cursor <cursoragent@cursor.com>
* clippy
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* handle lost device correctly
* Update lib.rs
* allow-integrated flag otherwise exclude
* DeviceLost is a status
* Integrated GPU skipped when discrete init fails
* nits
On Windows wgpu enumerates each physical GPU once per backend (Vulkan +
DX12) plus a CPU-emulated fallback ("Microsoft Basic Render Driver").
Building a mining context for every entry causes VRAM contention and
OOMs the process during benchmark/serve startup (#61).
Instead of deduplicating by (vendor, device) PCI IDs - which would
collapse rigs with multiple identical cards into a single context - drop
CPU-emulated adapters and keep all adapters from the highest-ranked
backend present (Vulkan/Metal, then DX12). Within a single backend each
physical GPU appears exactly once, so identical cards are preserved by
construction and no physical-ID matching is needed.
Selected adapters are ordered discrete-first so `--gpu-devices 1` picks
the discrete card on hybrid laptops. Skipped adapters are logged at info
level; if nothing usable remains, init fails with an explicit error.
Selection logic is a pure index-based function unit-tested against the
exact enumeration reported in #61, identical multi-GPU rigs, DX12-only
machines, and software-only environments.
Co-authored-by: illuzen <illuzen@users.noreply.github.com>
* support AMD GPUs on windows
* add support for more GPUs
* remove shader dump from logs
* handle gpu initialization failure better
* fmt
* re-order matching to avoid mobile / desktop false positives
* harden gpu matching
* add tests to engine-gpu module
* handle dead device and failed mapping correctly
* handle timeout properly with tokio
* nits
* fix benchmarks
* fmt
* improve logging
* don't let the queue spill, just drain it
* move cancellation out of the shader, since it doesn't work
* simplify batch size mechanism, improve logs
* fmt
* address review items
* single hash, new poseidon
* fix build
* new poseidon api
* fmt
* fix race condition with stale jobs being reported
* fmt
* fix deps
* format toml
remove stale comments
---------
Co-authored-by: Nikolaus Heger <nheger@gmail.com>
* implement quic
* simpler names
* simplify worker logic
* simplify gpu configuration logic
* miner initiates and picks random start nonce
* remove mining service
* clean up logs and remove unused engines
* clean up metrics
* metrics fix
* shared worker pool, cpu driven cancellation
just 10x the gpu thru-put, no more batch-size on gpu
* fmt
* non-local git
* taplo derp
* fix the benches
* handle stale results correctly in gpu
* more accurate gpu hashrate
* fmt
* remove unused
* handle duplicate solutions, cli controls gpu cancel frequency
* put plonky2 back
* demote noisy log