Author SHA1 Message Date
grenade 4a6ed42d6b Merge pull request 'readme: 4090 at 727 MH/s on the loop kernel, how to select it' (#30) from docs/readme-lair2 into main
ci / fmt (push) Successful in 20s
release / build (push) Successful in 1m35s
ci / clippy (push) Successful in 1m43s
ci / doc (push) Successful in 1m49s
ci / test (push) Successful in 6m35s
2026-09-14 16:39:23 +00:00
grenadeandClaude Fable 5.1 bbbaf815ac readme: 4090 at 727 MH/s on the loop kernel, how to select it
ci / fmt (pull_request) Successful in 19s
ci / clippy (pull_request) Successful in 1m48s
ci / doc (pull_request) Successful in 2m3s
ci / test (pull_request) Successful in 6m49s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ue5ZZm1Hiv5zPnucykKKuF
2026-09-14 19:30:43 +03:00
grenade ad77d99dfa Merge pull request 'engine-cuda: loop-structured mining_loop kernel, opt-in via MINER_CUDA_KERNEL=loop' (#29) from perf/loop-kernel into main
ci / fmt (push) Successful in 21s
deploy / build (push) Successful in 1m32s
ci / clippy (push) Successful in 1m37s
deploy / deploy (unrolled, 1, quadbrat.hanzalova.internal, cuda, bob.hanzalova.internal) (push) Successful in 1m10s
deploy / deploy (unrolled, 2, beast.hanzalova.internal, cuda, bob.hanzalova.internal) (push) Successful in 1m15s
deploy / deploy (loop, 1, benjy.hanzalova.internal, cuda-loop, bob.hanzalova.internal) (push) Successful in 1m18s
ci / doc (push) Successful in 2m1s
ci / test (push) Successful in 6m50s
2026-09-14 13:44:55 +00:00
grenadeandClaude Fable 5.1 12bd36aee2 deploy: per-host CUDA kernel choice; benjy runs the loop kernel
ci / fmt (pull_request) Successful in 21s
bench / build (pull_request) Successful in 1m0s
ci / clippy (pull_request) Successful in 2m42s
ci / doc (pull_request) Successful in 2m46s
bench / measure (pull_request) Successful in 2m56s
ci / test (pull_request) Successful in 7m54s
The matrix gains `cuda_kernel` (rendered into miner.env as
MINER_CUDA_KERNEL) next to `kernel`, the id validate asserts. benjy's
4090 measured +1.7% on the loop kernel (727-736 vs 716-718 MH/s, parity
200/200), so it runs loop / cuda-loop; quadbrat (-1.8%) and beast
(-3.4%) stay on unrolled / cuda. A rollback to a binary that predates
the switch is safe: it ignores the variable.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ue5ZZm1Hiv5zPnucykKKuF
2026-09-14 16:36:02 +03:00
grenadeandClaude Fable 5.1 816b1efb61 engine-cuda: loop-structured mining_loop kernel behind MINER_CUDA_KERNEL
ci / fmt (pull_request) Successful in 19s
bench / build (pull_request) Successful in 1m13s
bench / measure (pull_request) Failing after 14s
ci / clippy (pull_request) Successful in 1m49s
ci / doc (pull_request) Successful in 2m25s
ci / test (pull_request) Successful in 8m23s
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
2026-09-14 16:30:15 +03:00
grenade 8b01387e97 Merge pull request 'release: tag-triggered binaries and a README that states the measured rates' (#28) from release/binaries-and-readme into main
ci / fmt (push) Successful in 19s
release / build (push) Successful in 1m21s
deploy / build (push) Successful in 1m31s
ci / clippy (push) Successful in 1m51s
deploy / deploy (1, benjy.hanzalova.internal, cuda, bob.hanzalova.internal) (push) Successful in 36s
deploy / deploy (1, quadbrat.hanzalova.internal, cuda, bob.hanzalova.internal) (push) Successful in 36s
deploy / deploy (2, beast.hanzalova.internal, cuda, bob.hanzalova.internal) (push) Successful in 36s
ci / doc (push) Successful in 2m0s
ci / test (push) Successful in 6m56s
2026-09-13 22:24:05 +00:00
grenadeandClaude Fable 5.1 7831323362 release: tag-triggered binaries, README section with measured rates
ci / fmt (pull_request) Successful in 20s
bench / build (pull_request) Successful in 1m15s
ci / clippy (pull_request) Successful in 1m43s
ci / doc (pull_request) Successful in 2m0s
bench / measure (pull_request) Successful in 2m52s
ci / test (pull_request) Successful in 6m55s
For independent miners (quantus/miner#27): a `v<version>-lair.<n>` tag
builds quantus-miner and quantus-bench on the cuda-13.0 runner with the
tag's commit in --version, and publishes a Gitea release with the
tarball, its sha256 and a build report. The README now leads with what
this fork is, the measured rates per card against upstream and the two
closed pool binaries, where the binaries are, how to reproduce the
numbers with quantus-bench, and that there is no fee, licence server or
telemetry. release.yaml is excluded from deploy triggers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ue5ZZm1Hiv5zPnucykKKuF
2026-09-14 01:13:49 +03:00
6 changed files with 314 additions and 19 deletions
+11 -1
View File
@@ -20,6 +20,7 @@ on:
- "**.md"
- .gitea/workflows/ci.yml
- .gitea/workflows/bench.yaml
- .gitea/workflows/release.yaml
workflow_dispatch:
inputs:
mode:
@@ -87,17 +88,24 @@ jobs:
# `kernel` is the kernel id validate expects on the per-device metric:
# cuda (engine-cuda, #3) on every NVIDIA host once the build carries
# the fat binary; u64 would mean the miner silently fell back to wgpu.
# `cuda_kernel` selects the kernel entry (MINER_CUDA_KERNEL, #29):
# unrolled is the default; loop measured +1.7% on the 4090 and
# -1.8% / -3.4% on the 3060 / 5090 (2026-09-14), so only benjy runs
# it. `kernel` must be the id that choice reports: cuda or cuda-loop.
- host: benjy.hanzalova.internal
node: bob.hanzalova.internal
gpu_devices: "1" # 1x RTX 4090 (sm_89); reference card for #2
kernel: cuda
cuda_kernel: loop
kernel: cuda-loop
- host: quadbrat.hanzalova.internal
node: bob.hanzalova.internal
gpu_devices: "1" # 1x RTX 3060 (sm_86)
cuda_kernel: unrolled
kernel: cuda
- host: beast.hanzalova.internal
node: bob.hanzalova.internal
gpu_devices: "2" # 2x RTX 5090 (sm_120)
cuda_kernel: unrolled
kernel: cuda
steps:
- uses: actions/checkout@v4
@@ -145,6 +153,7 @@ jobs:
if: ${{ github.event.inputs.mode != 'validate' }}
env:
GPU_DEVICES: ${{ matrix.gpu_devices }}
CUDA_KERNEL: ${{ matrix.cuda_kernel }}
run: |
set -euo pipefail
SSHOPTS="-i $HOME/.ssh/id_gitea_ci -o StrictHostKeyChecking=accept-new"
@@ -209,6 +218,7 @@ jobs:
t = pathlib.Path("deploy/miner.env.tmpl").read_text()
t = t.replace("{{QUANTUS_NODE_ADDR}}", os.environ["NODE_ADDR"])
t = t.replace("{{QUANTUS_GPU_DEVICES}}", os.environ["GPU_DEVICES"])
t = t.replace("{{MINER_CUDA_KERNEL}}", os.environ["CUDA_KERNEL"])
pathlib.Path("miner.env").write_text(t)
PY
push --chown=root:quantus-miner --chmod=F0640 \
+78
View File
@@ -0,0 +1,78 @@
---
# Release binaries for independent miners (quantus/miner#27).
#
# Pushing a tag `v<upstream version>-lair.<n>` builds the miner and the bench
# harness on the cuda-13.0 runner (Fedora 43 like the mining hosts, and the
# only runner with nvcc, so the binary carries the sm_86/sm_89/sm_120 cubins
# and PTX), records what was built, and publishes a Gitea release with the
# binaries, their checksums and the build report attached. The tag's commit
# is embedded in `--version` so a downloaded binary identifies itself.
#
# Deploying to the fleet is deploy.yaml's job and follows main; a release is
# only a public snapshot and changes nothing on the hosts.
name: release
on:
push:
tags: ["v*-lair.*"]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: cuda-13.0
steps:
- uses: actions/checkout@v4
- name: build quantus-miner and quantus-bench
env:
MINER_BUILD_SHA: ${{ github.sha }}
MINER_CUDA_REQUIRE: "1"
run: |
set -euo pipefail
cargo build --release --locked -p miner-cli -p bench-harness
./target/release/quantus-miner --version
./target/release/quantus-bench --version || true
- name: package
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME}"
dir="quantus-miner-${tag}-linux-x86_64"
mkdir -p "dist/${dir}"
cp target/release/quantus-miner target/release/quantus-bench "dist/${dir}/"
cp LICENSE "dist/${dir}/"
{
echo "quantus-miner ${tag}"
echo "commit: ${GITHUB_SHA}"
echo "built: $(date -u +%Y-%m-%dT%H:%M:%SZ) on $(source /etc/os-release && echo "$PRETTY_NAME"), $(ldd --version | head -1)"
echo "cuda: $(nvcc --version | grep -oE 'release [0-9.]+' | head -1)"
echo "cubins: sm_86 sm_89 sm_120 + compute_120 PTX"
echo "version string: $(./target/release/quantus-miner --version)"
} > "dist/${dir}/BUILD.txt"
(cd dist && tar -czf "${dir}.tar.gz" "${dir}" && sha256sum "${dir}.tar.gz" > "${dir}.tar.gz.sha256")
cat "dist/${dir}/BUILD.txt" "dist/${dir}.tar.gz.sha256"
- name: publish release
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME}"
dir="quantus-miner-${tag}-linux-x86_64"
api="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}"
auth="Authorization: token ${{ secrets.GITHUB_TOKEN }}"
body=$(python3 - "$tag" "dist/${dir}/BUILD.txt" "dist/${dir}.tar.gz.sha256" <<'PY'
import json, sys, pathlib
tag, build, sha = sys.argv[1], pathlib.Path(sys.argv[2]).read_text(), pathlib.Path(sys.argv[3]).read_text().strip()
text = (
f"quantus-miner {tag}: Linux x86_64, NVIDIA RTX 30/40/50 (sm_86, sm_89, sm_120, plus PTX), "
"glibc 2.42 or newer, driver with CUDA 13.0 support.\n\n"
"No dev fee, no licence server, no telemetry. Measured rates and how to reproduce them are in the README.\n\n"
f"```\n{build}```\n\nsha256: `{sha}`\n"
)
print(json.dumps({"tag_name": tag, "name": f"quantus-miner {tag}", "body": text, "draft": False, "prerelease": False}))
PY
)
id=$(curl -fsS -X POST -H "$auth" -H "Content-Type: application/json" "${api}/releases" -d "$body" | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')
echo "release id ${id}"
for f in "dist/${dir}.tar.gz" "dist/${dir}.tar.gz.sha256" "dist/${dir}/BUILD.txt"; do
curl -fsS -X POST -H "$auth" "${api}/releases/${id}/assets?name=$(basename "$f")" -F "attachment=@${f}" > /dev/null
echo "attached $(basename "$f")"
done
+52
View File
@@ -2,6 +2,58 @@
High-performance external mining service for Quantus Network with support for CPU, GPU, and hybrid CPU+GPU mining.
## This fork
Like the official miner, but fast: a native CUDA engine for NVIDIA cards, kept
bit-exact with the reference hash and measured against every other miner we
can get hold of. No dev fee, no licence server, no telemetry, solo or pool.
Apache-2.0, like upstream.
**Measured hashrate**, three interleaved 30-second rounds per card at the
enforced power cap, memory clock locked at 810 MHz, CUDA 13.0 driver 580:
| card | power cap | this fork | upstream `--cuda-gpu` | quantusminer.com qpow-cuda 1.0.7 | quanpool-miner 6.2.4 (5% fee) |
|---|---|---|---|---|---|
| RTX 5090 | 400 W | **1104 MH/s** | 944 | 1171 | 1234 (1172 after fee) |
| RTX 4090 | 250 W | **727 MH/s** (loop kernel) | | | |
| RTX 3060 | 130 W | **141 MH/s** | | | |
The 5090 column was measured on the same card in the same session
(2026-09-13); the closed pool binaries were run in benchmark mode only. How
the pool kernels get their remaining edge, and what has been tried, is
written up in [issue #27](https://git.lair.cafe/quantus/miner/issues/27).
Since v4.0.2-lair.2 the binary carries two kernels. The default (`unrolled`)
is fastest on the RTX 5090 and 3060; RTX 40-series cards measure 1.7% better
on the rolled-loop one, selected with `MINER_CUDA_KERNEL=loop` in the
environment (it reports as kernel `cuda-loop` on the metrics). Both are
bit-exact with the CPU reference; try each with `quantus-bench` on your card.
**Binaries** are on the [releases page](https://git.lair.cafe/quantus/miner/releases):
one Linux x86_64 `quantus-miner` carrying cubins for sm_86 (RTX 30), sm_89
(RTX 40) and sm_120 (RTX 50) plus PTX for anything newer, built on Fedora 43
(glibc 2.42 or newer), and `quantus-bench`, the measurement harness. Nothing
is compiled at run time; the driver needs CUDA 13.0 support (580.x).
**Reproduce the numbers.** `quantus-bench` pins the power limit it expects,
warms up, reports the median of timed windows with their spread, and checks
GPU hashes against the CPU reference for random jobs:
```bash
# hashrate: median of 5 x 30 s windows, one worker thread per card
./quantus-bench --duration-secs 30 --runs 5 --workers 1 --expect-power-limit 400
# parity: 2000 random jobs, every found hash recomputed on the CPU
./quantus-bench --duration-secs 3 --runs 1 --parity-jobs 2000
```
Compare builds with interleaved rounds (A, B, A, B, ...) rather than one
block of runs each: on a power-capped card, clock drift between blocks looks
like a 0.5% code change.
**Solo mining** against your own node is the `serve` command below with the
node's auth token and certificate fingerprint; nothing else is needed.
## Building
```bash
+118 -11
View File
@@ -333,6 +333,32 @@ __device__ __forceinline__ u64 gf_canon(u64 a) {
#else
#define LAIR_LAUNCH_BOUNDS __launch_bounds__(LAIR_TPB)
#endif
// lair: the loop-structured kernel (`mining_loop`, quantus/miner#27). Same
// arithmetic and same host contract as `mining_main`, but the round loops
// are rolled with the constants fetched from constant memory by index, the
// way the fastest closed kernel is built, so the register footprint can drop
// and more warps fit per SM. Selected at run time (MINER_CUDA_KERNEL=loop);
// the unrolled kernel stays the default until this one measures better.
// 512 measured best for this kernel on beast (2131 MH/s against 2095 at
// 256 and 2099 at 128 x 4 blocks); the host picks the same block size when
// the loop kernel is selected.
#ifndef LAIR_LOOP_TPB
#define LAIR_LOOP_TPB 512
#endif
#ifndef LAIR_LOOP_MINBLOCKS
#define LAIR_LOOP_MINBLOCKS 0
#endif
#if LAIR_LOOP_MINBLOCKS
#define LAIR_LOOP_LAUNCH_BOUNDS __launch_bounds__(LAIR_LOOP_TPB, LAIR_LOOP_MINBLOCKS)
#else
#define LAIR_LOOP_LAUNCH_BOUNDS __launch_bounds__(LAIR_LOOP_TPB)
#endif
// Internal rounds per loop iteration in the loop kernel (1 or 2). One
// measured 2105 MH/s against 1976 for two on beast: the pair doubles the
// live state across the iteration for no scheduling gain.
#ifndef LAIR_LOOP_INT_PAIR
#define LAIR_LOOP_INT_PAIR 1
#endif
// Decide most nonces on element 0 of the final state before computing the
// rest of the last linear layer: at mainnet difficulty the top ~47 bits of
// the hash must be zero, and they live in that element.
@@ -708,6 +734,65 @@ __device__ __forceinline__ void permute(u64 st[12], bool skip_last_ext = false,
#endif
}
// lair: rolled-loop twin of permute for the loop kernel. Round-constant
// rows are addressed by the loop variable, so they are constant-memory loads
// with a uniform offset instead of immediates; the per-element loops inside
// each round stay unrolled so the state lives in registers.
#if LAIR_LAZY_ADD && LAIR_FOLD_RC
static_assert(N_INTERNAL % 2 == 0, "the paired internal loop assumes an even round count");
__device__ __forceinline__ void permute_loop(u64 st[12], bool skip_last_ext, bool skip_first_ext) {
if (!skip_first_ext) ext_layer_rc(st, RC_INITIAL[0]);
#pragma unroll 1
for (int r = 0; r + 1 < N_EXTERNAL_HALF; r++) {
#pragma unroll
for (int i = 0; i < 12; i++) st[i] = gf_sbox(st[i]);
ext_layer_rc(st, RC_INITIAL[r + 1]);
}
#pragma unroll
for (int i = 0; i < 12; i++) st[i] = gf_sbox(st[i]);
ext_layer(st);
st[0] = gf_add(st[0], RC_INTERNAL[0]);
#if LAIR_LOOP_INT_PAIR == 2
#pragma unroll 1
for (int r = 0; r + 2 < N_INTERNAL; r += 2) {
st[0] = gf_sbox(st[0]);
int_layer_rc(st, RC_INTERNAL[r + 1], true, nullptr);
st[0] = gf_sbox(st[0]);
int_layer_rc(st, RC_INTERNAL[r + 2], true, nullptr);
}
st[0] = gf_sbox(st[0]);
int_layer_rc(st, RC_INTERNAL[N_INTERNAL - 1], true, nullptr);
#else
#pragma unroll 1
for (int r = 0; r + 1 < N_INTERNAL; r++) {
st[0] = gf_sbox(st[0]);
int_layer_rc(st, RC_INTERNAL[r + 1], true, nullptr);
}
#endif
st[0] = gf_sbox(st[0]);
int_layer_rc(st, 0ull, false, RC_TERMINAL[0]);
#pragma unroll 1
for (int r = 0; r + 1 < N_EXTERNAL_HALF; r++) {
#pragma unroll
for (int i = 0; i < 12; i++) st[i] = gf_sbox(st[i]);
ext_layer_rc(st, RC_TERMINAL[r + 1]);
}
#pragma unroll
for (int i = 0; i < 12; i++) st[i] = gf_sbox(st[i]);
if (!skip_last_ext) ext_layer(st);
}
#else
__device__ __forceinline__ void permute_loop(u64 st[12], bool skip_last_ext, bool skip_first_ext) {
permute(st, skip_last_ext, skip_first_ext);
}
#endif
template <bool LOOP>
__device__ __forceinline__ void run_permute(u64 st[12], bool skip_last_ext = false, bool skip_first_ext = false) {
if (LOOP) permute_loop(st, skip_last_ext, skip_first_ext);
else permute(st, skip_last_ext, skip_first_ext);
}
__device__ __forceinline__ u32 bswap32(u32 v) {
return __byte_perm(v, 0, 0x0123);
}
@@ -742,12 +827,14 @@ static_assert(sizeof(MiningUniforms) == 320, "MiningUniforms must match the host
__device__ __constant__ u64 LAIR_DIR7[12] = {1, 1, 3, 2, 2, 2, 6, 4, 1, 1, 3, 2};
#endif
extern "C" __global__ void LAIR_LAUNCH_BOUNDS
mining_main(u32* __restrict__ results,
const MiningUniforms uni,
u32 total_threads,
u32 nonces_per_thread,
u32 total_nonces)
// The per-nonce search, shared by both kernel entries; LOOP picks the
// permutation implementation at compile time.
template <bool LOOP>
__device__ __forceinline__ void mine_nonces(u32* __restrict__ results,
const MiningUniforms& uni,
u32 total_threads,
u32 nonces_per_thread,
u32 total_nonces)
{
if (*((volatile u32*)results) != 0u) return;
u32 tid = blockIdx.x * blockDim.x + threadIdx.x;
@@ -796,13 +883,13 @@ mining_main(u32* __restrict__ results,
for (int i = 0; i < 8; i++) st[i] = gf_add(st[i], (u64)bswap32(current_nonce[7 - i]));
ext_layer_rc(st, RC_INITIAL[0]);
}
permute(st, false, true);
run_permute<LOOP>(st, false, true);
#else
#pragma unroll
for (int i = 0; i < 12; i++) st[i] = uni.midstate[i];
#pragma unroll
for (int i = 0; i < 8; i++) st[i] = gf_add(st[i], (u64)bswap32(current_nonce[7 - i]));
permute(st);
run_permute<LOOP>(st);
#endif
st[0] = gf_add(st[0], 1ull);
st[1] = gf_add(st[1], 1ull);
@@ -810,7 +897,7 @@ mining_main(u32* __restrict__ results,
// Element 0 of the final state is the hash's top 64 bits. Compute it
// alone, and only nonces that do not already exceed the target there
// pay for the other eleven outputs of the last linear layer.
permute(st, true);
run_permute<LOOP>(st, true);
{
u64 c0 = gf_canon(ext_layer_out0(st));
u32 h0 = bswap32((u32)(c0 & EPS));
@@ -820,7 +907,7 @@ mining_main(u32* __restrict__ results,
}
ext_layer(st);
#else
permute(st);
run_permute<LOOP>(st);
#endif
// First squeeze: most significant 256 bits of the hash.
@@ -843,7 +930,7 @@ mining_main(u32* __restrict__ results,
u32 hash_le[16];
#pragma unroll
for (int i = 0; i < 8; i++) hash_le[15 - i] = bswap32(first[i]);
permute(st);
run_permute<LOOP>(st);
#pragma unroll
for (int i = 0; i < 4; i++) {
u64 c = gf_canon(st[i]);
@@ -874,3 +961,23 @@ mining_main(u32* __restrict__ results,
}
}
}
extern "C" __global__ void LAIR_LAUNCH_BOUNDS
mining_main(u32* __restrict__ results,
const MiningUniforms uni,
u32 total_threads,
u32 nonces_per_thread,
u32 total_nonces)
{
mine_nonces<false>(results, uni, total_threads, nonces_per_thread, total_nonces);
}
extern "C" __global__ void LAIR_LOOP_LAUNCH_BOUNDS
mining_loop(u32* __restrict__ results,
const MiningUniforms uni,
u32 total_threads,
u32 nonces_per_thread,
u32 total_nonces)
{
mine_nonces<true>(results, uni, total_threads, nonces_per_thread, total_nonces);
}
+53 -7
View File
@@ -26,18 +26,60 @@ use std::time::Instant;
const FATBIN: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/mining.fatbin"));
/// SM list the fat binary carries, for `--version` and the config metric.
pub const CUDA_ARCHS: &str = env!("MINER_CUDA_ARCHS");
const KERNEL_ID: &str = "cuda";
/// lair: which kernel entry the module runs (quantus/miner#27). `unrolled`
/// is `mining_main`, the deployed kernel; `loop` is `mining_loop`, the
/// rolled-round twin under evaluation. Selected with MINER_CUDA_KERNEL so a
/// host can be switched without a rebuild, and reported as the kernel id on
/// the per-device metrics so the deploy validate and the dashboards can tell
/// them apart.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum KernelChoice {
Unrolled,
Loop,
}
impl KernelChoice {
fn from_env() -> Self {
match std::env::var("MINER_CUDA_KERNEL").as_deref() {
Ok("loop") => KernelChoice::Loop,
Ok("unrolled") | Ok("") | Err(_) => KernelChoice::Unrolled,
Ok(other) => {
log::warn!(target: "cuda_engine", "MINER_CUDA_KERNEL={other:?} is not unrolled|loop; using unrolled");
KernelChoice::Unrolled
}
}
}
fn function(self) -> &'static str {
match self {
KernelChoice::Unrolled => "mining_main",
KernelChoice::Loop => "mining_loop",
}
}
/// Kernel id on metrics: `cuda` for the deployed kernel (what the deploy
/// validate asserts), `cuda-loop` for the candidate.
fn id(self) -> &'static str {
match self {
KernelChoice::Unrolled => "cuda",
KernelChoice::Loop => "cuda-loop",
}
}
}
/// Threads per block. Must match the kernel's `__launch_bounds__` (LAIR_TPB,
/// default 256); override for experiments with MINER_CUDA_THREADS_PER_BLOCK
/// on a kernel compiled with the same value.
const THREADS_PER_BLOCK_DEFAULT: u32 = 256;
/// The loop kernel's `LAIR_LOOP_TPB`.
const LOOP_THREADS_PER_BLOCK_DEFAULT: u32 = 512;
fn threads_per_block() -> u32 {
fn threads_per_block(kernel: KernelChoice) -> u32 {
std::env::var("MINER_CUDA_THREADS_PER_BLOCK")
.ok()
.and_then(|v| v.parse().ok())
.filter(|v: &u32| *v > 0 && v.is_multiple_of(32))
.unwrap_or(THREADS_PER_BLOCK_DEFAULT)
.unwrap_or(match kernel {
KernelChoice::Unrolled => THREADS_PER_BLOCK_DEFAULT,
KernelChoice::Loop => LOOP_THREADS_PER_BLOCK_DEFAULT,
})
}
/// lair: size every batch to whole grids. The CLI default batch (1M nonces)
@@ -93,6 +135,7 @@ struct Device {
func: CudaFunction,
name: String,
sm_count: u32,
kernel: KernelChoice,
threads_per_block: u32,
threads_per_sm: u32,
metrics: metrics::DeviceMetrics,
@@ -171,9 +214,10 @@ impl CudaEngine {
Ok(d) => {
log::info!(
target: "cuda_engine",
"CUDA device {ordinal}: {} ({} SMs) using {KERNEL_ID} kernel [sm_{{{}}}]",
"CUDA device {ordinal}: {} ({} SMs) using {} kernel [sm_{{{}}}]",
d.name,
d.sm_count,
d.kernel.id(),
CUDA_ARCHS
);
devices.push(Arc::new(d));
@@ -221,15 +265,17 @@ impl Device {
cudarc::driver::sys::CUdevice_attribute::CU_DEVICE_ATTRIBUTE_MULTIPROCESSOR_COUNT,
)? as u32;
let module = ctx.load_module(Ptx::from_binary(FATBIN.to_vec()))?;
let func = module.load_function("mining_main")?;
let kernel = KernelChoice::from_env();
let func = module.load_function(kernel.function())?;
Ok(Self {
ctx,
func,
name,
sm_count,
kernel,
threads_per_sm: threads_per_sm(),
threads_per_block: threads_per_block(),
metrics: metrics::DeviceMetrics::new(ordinal, KERNEL_ID),
threads_per_block: threads_per_block(kernel),
metrics: metrics::DeviceMetrics::new(ordinal, kernel.id()),
})
}
+2
View File
@@ -4,3 +4,5 @@
# from the node host as separate 0640 files.
QUANTUS_NODE_ADDR={{QUANTUS_NODE_ADDR}}
QUANTUS_GPU_DEVICES={{QUANTUS_GPU_DEVICES}}
# Kernel entry in the CUDA engine: unrolled (mining_main) or loop (mining_loop), quantus/miner#29.
MINER_CUDA_KERNEL={{MINER_CUDA_KERNEL}}