88 Commits

Author SHA1 Message Date
Quantus Network CI
41579e8223 bump version to v2.0.1 (#36)
Co-authored-by: czareko <czareko@users.noreply.github.com>
2025-12-23 06:00:01 +08:00
Cezary Olborski
516286836a fix: Release build script syntax (#35) 2025-12-23 05:56:08 +08:00
Quantus Network CI
63a1d1f2d4 bump version to v2.0.0 (#34)
Co-authored-by: czareko <czareko@users.noreply.github.com>
2025-12-22 22:07:42 +08:00
Cezary Olborski
ad2d2c90ba fix: ADMIN_PAT for release proposal (#33) 2025-12-22 22:03:40 +08:00
illuzen
b3cef7bab8 fix ci (#32) 2025-12-22 21:25:15 +08:00
illuzen
a97d79632e Hybrid engine (#30)
* hybrid works with one gpu

* handle multiple gpus

* taplo

* fix clippy issues

* refactor to remove unnecessary abstraction layer

* update readme

* fix tests

* clippy

* delete hybrid engine

---------

Co-authored-by: Nikolaus Heger <nheger@gmail.com>
2025-12-22 21:05:31 +08:00
Nikolaus Heger
51ba298a68 Merge pull request #29 from Quantus-Network/n13/benchmark_command
add benchmark command
2025-12-19 19:37:55 +08:00
Nikolaus Heger
3ae66edf13 Merge branch 'main' into n13/benchmark_command 2025-12-19 19:37:00 +08:00
illuzen
5a6de314ab add wgpu support (#28)
* ok wgpu runs hello world

* toy hasher

* separate wgsl file

* kinda poseidon2

* test vectors

* closer

* a bit of debug

* closer

* sort of almost

* fixed gf_mul

* merge gf_mul back into mining, clean up tests

* fix bytes_to_field_elements

* linear layer fixed

* maybe better?

* mds tests

* cleanup debug output

* found gf_mul bug

* fixed gf_mul bug

* efficient sbox again

* mds matches

* cleanup

* test internal layer

* cleaner

* external linear passes

* external layer tests

* fixed the constants

* moar tests

* more cleaning

* actually test just internal

* fairly clean tests now

* removed another pointless test

* more cleanup

* gf_from_const tests

* testing

* fixed permutations

* double hash tests, but they fail

* moar tests!

* hash-twice works, thanks Gemini 3 Pro!

* gpu runs but doesn't mine

* better logging

* lock workers to gpu

* send target to gpu not difficulty

* fix verify_nonce

* double hash failures

* double hash matches

* tests pass and it mines now

* fix benches

* a bit faster now

* improved logging

* fix benches

* simplified gpu code

* further simplification

* a bit simpler

* slight simplification

* a bit faster

* dynamic thread allocation and work coarsening

* more benching

* faster

* faster

* faster

* clean up documentation

* a bit faster

* simplify

* simplify

* optimized mds

* almost par with cpu now

* fmt

* clippy

* support multiple gpus

* clippy

* unused

* fixed some logs

---------

Co-authored-by: Nikolaus Heger <nheger@gmail.com>
2025-12-19 19:25:14 +08:00
Nikolaus Heger
3ca29b6d8a throw errors when there's no GPUs detected 2025-12-19 16:40:54 +08:00
Nikolaus Heger
c276a1173e clean up options 2025-12-19 15:26:43 +08:00
Nikolaus Heger
6ae4658209 add benchmark command
so we can easily compare CPU and GPU without having to run cargo bench.
2025-12-19 15:22:24 +08:00
Nikolaus Heger
f7209be81b fixed some logs 2025-12-19 14:42:02 +08:00
illuzen
b4999ab448 unused 2025-12-13 17:45:05 +08:00
illuzen
1eaea91909 clippy 2025-12-13 17:42:43 +08:00
illuzen
52318aeaad support multiple gpus 2025-12-13 17:38:24 +08:00
illuzen
22fecf183e clippy 2025-12-13 17:07:24 +08:00
illuzen
0978d3bebd fmt 2025-12-13 16:21:28 +08:00
illuzen
8e073c110a almost par with cpu now 2025-12-11 15:55:50 +08:00
illuzen
c13b0027f3 optimized mds 2025-12-11 14:55:14 +08:00
illuzen
3cb91c4798 simplify 2025-12-11 14:42:13 +08:00
illuzen
43e857dfba simplify 2025-12-11 04:01:31 +08:00
illuzen
83e0c9edce a bit faster 2025-12-11 00:39:23 +08:00
illuzen
1509e2283b clean up documentation 2025-12-09 23:07:57 +08:00
illuzen
d423b5c1b1 faster 2025-12-09 22:40:41 +08:00
illuzen
6c7835f348 faster 2025-12-09 22:36:56 +08:00
illuzen
46202989df faster 2025-12-09 22:31:10 +08:00
illuzen
09b63783e0 more benching 2025-12-09 20:49:48 +08:00
illuzen
e8e0d6acf4 dynamic thread allocation and work coarsening 2025-12-09 20:31:17 +08:00
illuzen
14022a367f a bit faster 2025-12-09 17:59:42 +08:00
illuzen
587303b480 slight simplification 2025-12-09 17:33:20 +08:00
illuzen
9c88ac7aff a bit simpler 2025-12-09 17:14:25 +08:00
illuzen
ffe58596d4 further simplification 2025-12-09 16:58:52 +08:00
illuzen
0ab3049703 simplified gpu code 2025-12-09 15:53:19 +08:00
illuzen
30c3b3f1a7 fix benches 2025-12-08 15:46:15 +08:00
illuzen
ccca629c49 improved logging 2025-12-08 14:55:47 +08:00
illuzen
5705d7fb1a a bit faster now 2025-12-03 18:30:41 +08:00
illuzen
9bb8c3cb3c fix benches 2025-12-03 18:15:41 +08:00
illuzen
44e62ce2f4 tests pass and it mines now 2025-12-03 17:48:52 +08:00
illuzen
ded099d8bd double hash matches 2025-12-03 17:28:49 +08:00
illuzen
0ebb6c4058 double hash failures 2025-12-03 17:15:11 +08:00
illuzen
d8ab215b94 fix verify_nonce 2025-12-03 15:53:03 +08:00
illuzen
0850cb9c92 send target to gpu not difficulty 2025-12-03 15:26:41 +08:00
illuzen
0bbeee332d lock workers to gpu 2025-12-03 14:32:44 +08:00
illuzen
57814d50df better logging 2025-12-03 11:45:34 +08:00
illuzen
aa1cf2bb46 gpu runs but doesn't mine 2025-12-01 15:11:07 +08:00
illuzen
66d75b9d9f hash-twice works, thanks Gemini 3 Pro! 2025-12-01 14:31:03 +08:00
illuzen
1a34013d0b moar tests! 2025-11-23 23:18:36 +08:00
illuzen
765a51553e double hash tests, but they fail 2025-11-23 20:00:31 +08:00
illuzen
9f35010ce4 fixed permutations 2025-11-23 19:44:05 +08:00
illuzen
09be940f14 testing 2025-11-23 10:48:41 +08:00
illuzen
aec1e76dfc gf_from_const tests 2025-11-23 10:40:38 +08:00
illuzen
d0adeb5cf6 more cleanup 2025-11-23 10:31:47 +08:00
illuzen
da19865de8 removed another pointless test 2025-11-22 23:50:48 +08:00
illuzen
16c400dfaa fairly clean tests now 2025-11-22 23:49:21 +08:00
illuzen
7d4f126bc7 actually test just internal 2025-11-22 22:53:20 +08:00
illuzen
a91eaf6f32 more cleaning 2025-11-22 22:23:12 +08:00
illuzen
675e8564da moar tests 2025-11-22 20:49:07 +08:00
illuzen
8d18ad1fb1 fixed the constants 2025-11-22 20:09:20 +08:00
illuzen
cf50bc6cb8 external layer tests 2025-11-22 19:58:41 +08:00
illuzen
9d70218fa9 external linear passes 2025-11-22 13:41:28 +08:00
illuzen
4a6c8936b3 cleaner 2025-11-22 13:29:30 +08:00
illuzen
021683b20e test internal layer 2025-11-22 12:38:20 +08:00
illuzen
2d3c940432 cleanup 2025-11-22 11:19:09 +08:00
illuzen
79e30ce9a2 mds matches 2025-11-21 19:33:31 +08:00
illuzen
82793591a6 efficient sbox again 2025-11-20 23:32:16 +08:00
illuzen
a83d5e32a4 fixed gf_mul bug 2025-11-20 23:28:41 +08:00
illuzen
b89cd1af72 found gf_mul bug 2025-11-20 23:16:48 +08:00
illuzen
ed89a1f1d2 cleanup debug output 2025-11-20 22:30:46 +08:00
illuzen
12a2c94717 mds tests 2025-11-18 19:23:55 +08:00
illuzen
a8e7bc669f maybe better? 2025-11-18 18:29:53 +08:00
illuzen
067abe7c18 linear layer fixed 2025-11-18 15:27:42 +08:00
illuzen
46282c3852 fix bytes_to_field_elements 2025-11-18 12:52:11 +08:00
illuzen
7aba4eac70 merge gf_mul back into mining, clean up tests 2025-11-18 10:54:43 +08:00
illuzen
4d0a8c425a fixed gf_mul 2025-11-18 10:20:48 +08:00
illuzen
45e0d7d384 sort of almost 2025-11-17 21:35:25 +08:00
illuzen
36cb0efb8a closer 2025-11-17 18:38:04 +08:00
illuzen
93696a5f4b a bit of debug 2025-11-17 17:40:38 +08:00
illuzen
e7156dd890 closer 2025-11-17 17:05:35 +08:00
illuzen
9635482098 test vectors 2025-11-17 16:54:20 +08:00
illuzen
c214aa9d47 kinda poseidon2 2025-11-17 16:51:32 +08:00
illuzen
6fc2e9a209 separate wgsl file 2025-11-17 16:49:51 +08:00
illuzen
4578e076ce toy hasher 2025-11-17 16:47:23 +08:00
illuzen
50958ef4da ok wgpu runs hello world 2025-11-17 16:37:38 +08:00
Cezary Olborski
029cf6640a fix: Docker release - Workflow rename (#27) 2025-11-17 11:59:25 +08:00
Cezary Olborski
6715169630 fix: Unknown target fix 2025-11-17 11:53:17 +08:00
Cezary Olborski
8c0938333d fix: Docker workflow paths (#26)
* fix: Docker workflow paths

* remove commented cuda-builder workflow
2025-11-17 10:56:06 +08:00
Cezary Olborski
6831d6a493 feat: Docker image + release (#25)
* feat: Dockerfile

* feat: Docker release

* fix: Cuda build in CI OFF
2025-11-17 09:26:43 +08:00
35 changed files with 10102 additions and 4524 deletions

42
.dockerignore Normal file
View File

@@ -0,0 +1,42 @@
# Git
.git
.gitignore
.github
# Build artifacts
target/
*.rs.bk
*.pdb
# IDE
.vscode
.idea
*.swp
*.swo
*~
# Documentation
docs/
*.md
!README.md
# Examples and tests (not needed for runtime)
examples/
# Other
.DS_Store
Thumbs.db
# CI/CD
.github/
# License
LICENSE
# Dockerfiles themselves
Dockerfile*
.dockerignore
# Cargo cache
.cargo/

View File

@@ -45,15 +45,28 @@ jobs:
name: 🛠️ Build & Test
needs: fast-checks
runs-on: ubuntu-latest
strategy:
matrix:
features: ["", "gpu"]
steps:
- uses: actions/checkout@v4
- name: setup rust
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: compile
run: cargo build --locked --workspace
- name: compile (${{ matrix.features || 'cpu-only' }})
run: |
if [ -n "${{ matrix.features }}" ]; then
cargo build --locked --workspace --features ${{ matrix.features }}
else
cargo build --locked --workspace
fi
timeout-minutes: 90
- name: test
run: cargo test --locked --workspace
- name: test (${{ matrix.features || 'cpu-only' }})
run: |
if [ -n "${{ matrix.features }}" ]; then
cargo test --locked --workspace --features ${{ matrix.features }}
else
cargo test --locked --workspace
fi
timeout-minutes: 15
analysis:
@@ -66,64 +79,24 @@ jobs:
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
components: clippy
- name: clippy
run: cargo clippy --locked --workspace
- name: clippy (all features)
run: cargo clippy --locked --workspace --all-features
timeout-minutes: 30
- name: doc
run: cargo doc --locked --workspace --no-deps
run: cargo doc --locked --workspace --no-deps --all-features
timeout-minutes: 15
cuda-build:
name: 🚀 CUDA Build (cuda=${{ matrix.cuda_tag }} sm=${{ matrix.sm }})
benchmark:
name: 🏃 Benchmark
needs: fast-checks
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
cuda_tag: ["12.9.0", "13.0.0"]
sm: ["86", "89", "120"]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Check if cuda-builder image exists
id: check-image
run: |
if docker manifest inspect ghcr.io/quantus-network/cuda-builder:${{ matrix.cuda_tag }} >/dev/null 2>&1; then
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "✅ CUDA builder image exists for ${{ matrix.cuda_tag }}"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
echo "⚠️ CUDA builder image not found for ${{ matrix.cuda_tag }}, skipping build"
fi
- name: Build with cuda-builder image
if: steps.check-image.outputs.exists == 'true'
id: build
env:
CUDA_TAG: ${{ matrix.cuda_tag }}
SM: ${{ matrix.sm }}
GITHUB_SHA: ${{ github.sha }}
run: |
set -eux
SHORT_TAG="$(echo "${CUDA_TAG}" | sed -E 's/^([0-9]+\.[0-9]+).*/\1/')"
BIN="quantus-miner-cuda-${SHORT_TAG}-sm-${SM}"
mkdir -p .cargo-cache target
docker run --rm \
-e CUDA_ARCH=sm_${SM} \
-e CARGO_TERM_COLOR=always \
-e GITHUB_SHA="${GITHUB_SHA}" \
-v "${PWD}:/workspace" \
-v "${PWD}/.cargo-cache:/opt/cargo/registry" \
-v "${PWD}/target:/workspace/target" \
ghcr.io/quantus-network/cuda-builder:${CUDA_TAG} \
bash -lc 'cd /workspace && echo "CUDA version:" && nvcc --version && cargo build -p miner-cli --features cuda --release && strip target/release/quantus-miner || true'
install -m 0755 ./target/release/quantus-miner "./${BIN}"
echo "bin_path=./${BIN}" >> "$GITHUB_OUTPUT"
echo "short_tag=${SHORT_TAG}" >> "$GITHUB_OUTPUT"
- name: Upload artifact
if: steps.check-image.outputs.exists == 'true'
uses: actions/upload-artifact@v4
with:
name: ${{ format('quantus-miner-cuda-{0}-sm-{1}', steps.build.outputs.short_tag, matrix.sm) }}
path: ${{ steps.build.outputs.bin_path }}
if-no-files-found: error
retention-days: 3
- uses: actions/checkout@v4
- name: setup rust
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: build benchmark binary
run: cargo build -p miner-cli --release --features gpu
timeout-minutes: 60
- name: run cpu benchmark
run: ./target/release/quantus-miner benchmark --cpu-workers 2 --duration 5
timeout-minutes: 10

View File

@@ -1,83 +0,0 @@
# name: Cuda Docker Image Builder
# on:
# workflow_dispatch:
# push:
# branches:
# - main
# paths:
# - cuda-builder.cf
# pull_request:
# branches:
# - main
# paths:
# - cuda-builder.cf
# permissions:
# contents: read
# packages: write
# jobs:
# build-and-push:
# name: cuda-builder:${{ matrix.cuda_tag }}
# runs-on: ubuntu-latest
# strategy:
# fail-fast: false
# matrix:
# cuda_tag:
# - 12.9.0
# - 13.0.0
# steps:
# - shell: bash
# run: |
# echo "IMAGE_NAME=ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/cuda-builder" >> "$GITHUB_ENV"
# - name: Checkout repository
# uses: actions/checkout@v4
# - uses: ./.github/actions/disk
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v3
# - name: Prepare Buildx cache
# uses: actions/cache@v4
# with:
# path: /tmp/.buildx-cache
# key: ${{ runner.os }}-buildx-cuda-builder-${{ matrix.cuda_tag }}-${{ github.sha }}
# restore-keys: |
# ${{ runner.os }}-buildx-cuda-builder-${{ matrix.cuda_tag }}-
# - name: Log in to GHCR
# if: github.event_name == 'push'
# uses: docker/login-action@v3
# with:
# registry: ghcr.io
# username: ${{ github.repository_owner }}
# password: ${{ secrets.GITHUB_TOKEN }}
# - name: Build and (conditionally) push cuda-builder image
# uses: docker/build-push-action@v5
# with:
# context: .
# file: cuda-builder.cf
# build-args: |
# CUDA_TAG=${{ matrix.cuda_tag }}
# pull: true
# push: ${{ github.event_name == 'push' }}
# tags: |
# ${{ env.IMAGE_NAME }}:${{ matrix.cuda_tag }}
# labels: |
# org.opencontainers.image.source=${{ github.repository }}
# org.opencontainers.image.description=Pristine CUDA builder image (CUDA ${{ matrix.cuda_tag }})
# org.opencontainers.image.licenses=Apache-2.0
# org.opencontainers.image.title=cuda-builder
# org.opencontainers.image.vendor=Quantus Network
# cache-from: type=local,src=/tmp/.buildx-cache
# cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
# provenance: false
# - name: Move Buildx cache
# run: |
# rm -rf /tmp/.buildx-cache
# mv /tmp/.buildx-cache-new /tmp/.buildx-cache

145
.github/workflows/docker-image.yml vendored Normal file
View File

@@ -0,0 +1,145 @@
name: Docker Image
on:
workflow_dispatch:
inputs:
specific_version:
description: "Optional: Specify a full version (e.g., v0.3.0 or v0.3.1-beta.1) to build. If empty, uses the latest release tag. MUST start with 'v'."
required: false
default: ""
env:
CARGO_TERM_COLOR: always
jobs:
determine_version:
runs-on: ubuntu-latest
outputs:
version_with_v: ${{ steps.version_info.outputs.version_with_v }}
is_valid_version: ${{ steps.version_info.outputs.is_valid_version }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get version information
id: version_info
run: |
set -ex
TARGET_VERSION=""
SPECIFIC_VERSION="${{ github.event.inputs.specific_version }}"
if [[ -n "$SPECIFIC_VERSION" ]]; then
if [[ ! "$SPECIFIC_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then
echo "::error::Specified version '$SPECIFIC_VERSION' is not a valid format. It must start with 'v' (e.g., v0.3.0 or v0.3.1-beta.1)."
echo "is_valid_version=false" >> $GITHUB_OUTPUT
exit 1
fi
TARGET_VERSION="$SPECIFIC_VERSION"
echo "Using specified version: $TARGET_VERSION"
else
echo "No specific version provided, determining latest release tag..."
TARGET_VERSION=$(git tag --list 'v*' --sort=-v:refname | head -n 1)
if [[ -z "$TARGET_VERSION" ]]; then
echo "::error::No version tags starting with 'v' (e.g., vX.Y.Z) found in the repository."
echo "is_valid_version=false" >> $GITHUB_OUTPUT
exit 1
fi
echo "Latest release version found: $TARGET_VERSION"
fi
echo "version_with_v=$TARGET_VERSION" >> $GITHUB_OUTPUT
echo "is_valid_version=true" >> $GITHUB_OUTPUT
build_and_publish_image:
needs: determine_version
if: needs.determine_version.outputs.is_valid_version == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
env:
GHCR_IMAGE_PATH: ghcr.io/quantus-network/quantus-miner
TARGET_VERSION_WITH_V: ${{ needs.determine_version.outputs.version_with_v }}
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Check if Docker image already exists and fail if so
id: check_image
run: |
set -ex
IMAGE_TO_CHECK="${{ env.GHCR_IMAGE_PATH }}:${{ env.TARGET_VERSION_WITH_V }}"
echo "Checking for image: $IMAGE_TO_CHECK"
if docker manifest inspect "$IMAGE_TO_CHECK" > /dev/null 2>&1; then
echo "::error::Image $IMAGE_TO_CHECK already exists. Aborting."
exit 1
else
echo "Image $IMAGE_TO_CHECK does not exist. Proceeding with build."
fi
- name: Checkout code at tag version
uses: actions/checkout@v4
with:
ref: ${{ env.TARGET_VERSION_WITH_V }}
- name: Check if Dockerfile exists in tag
id: check_dockerfile
run: |
if [ ! -f Dockerfile ]; then
echo "::warning::Dockerfile not found in tag ${{ env.TARGET_VERSION_WITH_V }}, falling back to main branch Dockerfile"
echo "use_main=true" >> $GITHUB_OUTPUT
else
echo "Dockerfile found in tag"
echo "use_main=false" >> $GITHUB_OUTPUT
fi
- name: Checkout main branch for Dockerfile (if needed)
if: steps.check_dockerfile.outputs.use_main == 'true'
uses: actions/checkout@v4
with:
ref: main
sparse-checkout: |
Dockerfile
.dockerignore
sparse-checkout-cone-mode: false
path: dockerfile-source
- name: Copy Dockerfile from main (if needed)
if: steps.check_dockerfile.outputs.use_main == 'true'
run: |
cp dockerfile-source/Dockerfile ./
[ -f dockerfile-source/.dockerignore ] && cp dockerfile-source/.dockerignore ./ || true
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
platforms: linux/amd64,linux/arm64
provenance: false
tags: |
${{ env.GHCR_IMAGE_PATH }}:${{ env.TARGET_VERSION_WITH_V }}
${{ env.GHCR_IMAGE_PATH }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Print completion message
run: |
echo "Successfully built and pushed ${{ env.GHCR_IMAGE_PATH }}:${{ env.TARGET_VERSION_WITH_V }}"
echo "Image is also tagged as latest: ${{ env.GHCR_IMAGE_PATH }}:latest"

View File

@@ -8,14 +8,14 @@ on:
workflow_dispatch:
inputs:
target_branch:
description: 'Target branch for the PR (default: main)'
description: "Target branch for the PR (default: main)"
required: false
type: string
default: 'main'
default: "main"
version_type:
description: 'Type of version bump'
description: "Type of version bump"
required: true
default: 'patch'
default: "patch"
type: choice
options:
- patch
@@ -26,7 +26,7 @@ on:
description: 'Custom version string (e.g., 1.2.3). Only used if version_type is "custom".'
required: false
is_draft:
description: 'Is this a draft release?'
description: "Is this a draft release?"
required: true
type: boolean
default: false
@@ -58,12 +58,12 @@ jobs:
run: |
# Get all version tags and sort them by version
latest_semver_tag=$(git tag -l "v[0-9]*.[0-9]*.[0-9]*" | sort -V | tail -n 1)
# If no tags found, use default
if [ -z "$latest_semver_tag" ]; then
latest_semver_tag="v0.0.0"
fi
echo "latest_tag_found=$latest_semver_tag" >> $GITHUB_OUTPUT
echo "Latest semantic version tag found: $latest_semver_tag"
@@ -146,11 +146,11 @@ jobs:
# Create new branch from source branch
git checkout "$SOURCE_BRANCH"
git checkout -b "$branch_name"
# Update version in workspace Cargo.toml (safer than cargo set-version)
echo "Updating workspace Cargo.toml to version: $new_cargo_version"
sed -i -E "s/^version\s*=\s*\"[0-9a-zA-Z.-]+\"/version = \"$new_cargo_version\"/" Cargo.toml
# Regenerate Cargo.lock with precise updates for our packages only
cargo update -p miner-cli --precise "$new_cargo_version"
cargo update -p miner-service --precise "$new_cargo_version"
@@ -158,16 +158,15 @@ jobs:
cargo update -p metrics --precise "$new_cargo_version"
cargo update -p miner-telemetry --precise "$new_cargo_version"
cargo update -p engine-cpu --precise "$new_cargo_version"
cargo update -p engine-gpu-cuda --precise "$new_cargo_version"
cargo update -p engine-gpu-opencl --precise "$new_cargo_version"
cargo update -p engine-gpu --precise "$new_cargo_version"
# Verify everything compiles correctly
cargo check --workspace
# Commit changes
git config user.name "${{ github.actor }}"
git config user.email "${{ github.actor }}@users.noreply.github.com"
git add Cargo.toml Cargo.lock
git commit -m "bump version to $NEW_VERSION"
git push origin "$branch_name"

View File

@@ -38,18 +38,18 @@ jobs:
echo "Error: Could not extract version from PR title: ${{ github.event.pull_request.title }}"
exit 1
fi
VERSION=${VERSION_TAG#v}
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "tag=$VERSION_TAG" >> $GITHUB_OUTPUT
# Check if this is a draft release
if [[ "${{ contains(github.event.pull_request.labels.*.name, 'draft-release') }}" == "true" ]]; then
echo "is_draft=true" >> $GITHUB_OUTPUT
else
echo "is_draft=false" >> $GITHUB_OUTPUT
fi
echo "Extracted version: $VERSION"
echo "Extracted tag: $VERSION_TAG"
@@ -66,32 +66,70 @@ jobs:
strategy:
matrix:
include:
# Linux builds
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
binary_name: quantus-miner
asset_name: quantus-miner-linux-x86_64
features: ""
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
binary_name: quantus-miner
asset_name: quantus-miner-linux-x86_64-gpu
features: "gpu"
# Windows builds
- os: windows-latest
target: x86_64-pc-windows-msvc
binary_name: quantus-miner.exe
asset_name: quantus-miner-windows-x86_64.exe
features: ""
- os: windows-latest
target: x86_64-pc-windows-msvc
binary_name: quantus-miner.exe
asset_name: quantus-miner-windows-x86_64-gpu.exe
features: "gpu"
# macOS builds (Intel)
- os: macos-13
target: x86_64-apple-darwin
binary_name: quantus-miner
asset_name: quantus-miner-macos-x86_64
features: ""
- os: macos-13
target: x86_64-apple-darwin
binary_name: quantus-miner
asset_name: quantus-miner-macos-x86_64-gpu
features: "gpu"
# macOS builds (Apple Silicon)
- os: macos-latest
target: aarch64-apple-darwin
binary_name: quantus-miner
asset_name: quantus-miner-macos-aarch64
features: ""
- os: macos-latest
target: aarch64-apple-darwin
binary_name: quantus-miner
asset_name: quantus-miner-macos-aarch64-gpu
features: "gpu"
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.create-tag.outputs.tag }}
- name: setup rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
target: ${{ matrix.target }}
- name: build binary
run: cargo build --release --locked --target ${{ matrix.target }}
shell: bash
run: |
if [[ -n "${{ matrix.features }}" ]]; then
cargo build --release --locked --target ${{ matrix.target }} --features ${{ matrix.features }}
else
cargo build --release --locked --target ${{ matrix.target }}
fi
- name: prepare binary
shell: bash
run: |
@@ -102,7 +140,7 @@ jobs:
cp ${{ matrix.binary_name }} ${{ matrix.asset_name }}
strip ${{ matrix.asset_name }}
fi
- name: upload binary artifact
uses: actions/upload-artifact@v4
with:
@@ -116,12 +154,12 @@ jobs:
- uses: actions/checkout@v4
with:
ref: ${{ needs.create-tag.outputs.tag }}
- name: download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: create release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -134,12 +172,10 @@ jobs:
else
DRAFT_FLAG=""
fi
# Create release with all artifacts
gh release create "$TAG" \
--title "Release $TAG" \
--generate-notes \
$DRAFT_FLAG \
artifacts/quantus-miner-linux-x86_64/quantus-miner-linux-x86_64 \
artifacts/quantus-miner-windows-x86_64.exe/quantus-miner-windows-x86_64.exe \
artifacts/quantus-miner-macos-aarch64/quantus-miner-macos-aarch64
artifacts/*/quantus-miner-*

2291
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,7 @@
[workspace]
members = [
"crates/engine-cpu",
"crates/engine-gpu-cuda",
"crates/engine-gpu-opencl",
"crates/engine-gpu",
"crates/metrics",
"crates/miner-cli",
"crates/miner-service",
@@ -16,7 +15,7 @@ resolver = "2"
edition = "2021"
authors = ["Quantus Network"]
description = "Quantus External Miner Workspace"
version = "1.0.0"
version = "2.0.1"
[workspace.dependencies]
anyhow = "1"

55
Dockerfile Normal file
View File

@@ -0,0 +1,55 @@
# syntax=docker/dockerfile:1
############################
# Builder stage
############################
FROM rust:1.85-slim-bookworm AS builder
# Install build dependencies
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
pkg-config \
libssl-dev \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /build
# Copy workspace files
COPY Cargo.toml Cargo.lock rust-toolchain taplo.toml ./
COPY crates ./crates
COPY tests ./tests
# Build the miner-cli in release mode
RUN cargo build --release -p miner-cli --locked
# Strip debug symbols to reduce binary size
RUN strip target/release/quantus-miner || true
############################
# Runtime-only stage
############################
FROM debian:bookworm-slim
# Install runtime dependencies
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Copy binary from builder stage
COPY --from=builder /build/target/release/quantus-miner /usr/local/bin/quantus-miner
# Expose miner API port and metrics port
EXPOSE 9833 9900
# Run as unprivileged user
RUN useradd --system --uid 10001 quantus
USER 10001:10001
# Default working directory
WORKDIR /data
# Start the miner
ENTRYPOINT ["quantus-miner"]

318
README.md
View File

@@ -1,244 +1,124 @@
# External Miner Service for Quantus Network
Note: This repository is now a Cargo workspace. Build and run the CLI with:
- cargo build -p miner-cli --release
- cargo run -p miner-cli -- --port 9833 [--metrics-port 9900] [--workers N]
This crate provides an external mining service that can be used with a Quantus Network node. It exposes an HTTP API for
managing mining jobs.
High-performance external mining service for Quantus Network with support for CPU, GPU, and hybrid CPU+GPU mining.
## Building
To build the external miner service, navigate to the `miner` directory within the repository and use Cargo:
```bash
cd quantus-miner
cargo build --release
# CPU-only build (default)
cargo build -p miner-cli --release
# With GPU support (recommended)
cargo build -p miner-cli --features gpu --release
```
This will compile the binary and place it in the `target/release/` directory.
## CUDA Build (optional, Linux only)
The CUDA backend is feature-gated and currently supported on Linux with NVIDIA GPUs. macOS is not supported for CUDA.
Build with CUDA enabled:
```bash
# Build the CLI with CUDA feature (compiles .cu kernels to PTX via nvcc)
cargo build -p miner-cli --features cuda --release
```
At runtime, select the engine with:
```bash
# Will error if CUDA runtime/driver is unavailable
./target/release/quantus-miner --engine gpu-cuda --metrics-port 9919
```
Environment knobs used by the CUDA build:
- NVCC: Path to the nvcc binary (optional if in PATH)
- CUDA_HOME or CUDA_PATH: Used to locate nvcc at $CUDA_HOME/bin/nvcc when NVCC is unset
- CUDA_ARCH: Compute capability target for PTX (default: sm_70)
If nvcc isnt found, the build will continue but skip compiling kernels; the GPU engine will then fall back to CPU at runtime.
### Ubuntu (22.04/24.04) setup
Install NVIDIA driver and CUDA toolkit:
```bash
# Install proprietary driver (pick latest recommended)
sudo ubuntu-drivers autoinstall
sudo reboot
# CUDA toolkit (provides nvcc). Option A: Ubuntu package (may be older):
sudo apt update
sudo apt install -y nvidia-cuda-toolkit
# Verify nvcc
nvcc --version
```
For newer toolkits, consider NVIDIAs official repo: https://developer.nvidia.com/cuda-downloads
### Fedora 42 (dnf5) setup
Install NVIDIA driver and CUDA toolkit:
```bash
# Enable RPM Fusion for proprietary NVIDIA drivers (driver comes from here)
sudo dnf5 install -y https://download1.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm
# Install driver (akmod builds the module for your current kernel)
sudo dnf5 install -y akmod-nvidia
sudo reboot
# Add NVIDIA CUDA repo (for toolkit only) by creating a pinned repo file
sudo tee /etc/yum.repos.d/cuda-fedora$(rpm -E %fedora).repo >/dev/null <<'EOF'
[cuda-fedora$releasever-x86_64]
name=NVIDIA CUDA Fedora $releasever - x86_64
baseurl=https://developer.download.nvidia.com/compute/cuda/repos/fedora$releasever/x86_64/
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://developer.download.nvidia.com/compute/cuda/repos/fedora$releasever/x86_64/7fa2af80.pub
# Keep NVIDIA drivers from RPM Fusion; do not install any drivers from this repo
excludepkgs=cuda-drivers*,nvidia-driver*,xorg-x11-drv-nvidia*,kernel*
EOF
# Install CUDA toolkit and versioned nvcc; drivers remain from RPM Fusion due to excludepkgs above
# Discover nvcc package version and install alongside toolkit (example uses 13-0):
sudo dnf5 search cuda-nvcc
sudo dnf5 install -y cuda-toolkit cuda-nvcc-13-0 gcc14
# Verify nvcc
nvcc --version
# If nvcc is not on PATH, add it or set CUDA_HOME/NVCC (example for version 13.0):
export CUDA_HOME=/usr/local/cuda-13.0
export PATH="$CUDA_HOME/bin:$PATH"
# Or point the build directly at nvcc:
export NVCC="$CUDA_HOME/bin/nvcc"
```
#### Fedora 42 (dnf5): match CUDA toolkit to the installed driver (RPM Fusion)
If your NVIDIA driver comes from RPM Fusion (recommended) and reports a CUDA Version (via nvidia-smi) that doesnt match the CUDA toolkit available in the NVIDIA Fedora 42 repo, install the matching toolkit from the NVIDIA archive. This avoids PTX/CUBIN incompatibilities between toolkit and driver.
- Check your drivers CUDA Version:
```bash
nvidia-smi | grep "CUDA Version"
# Example: CUDA Version: 12.9
```
- Download the matching “local installer” repo RPM from the NVIDIA archive with a resilient downloader (NVIDIA servers can be flaky; use resume/retry):
```bash
# Example for CUDA 12.9 on Fedora 41 (works on Fedora 42 too):
curl --fail --location --retry 9999 --retry-delay 3 --retry-max-time 0 \
--continue-at - \
--output ~/Downloads/cuda-repo-fedora41-12-9-local-12.9.0_575.51.03-1.x86_64.rpm \
--url https://developer.download.nvidia.com/compute/cuda/12.9.0/local_installers/cuda-repo-fedora41-12-9-local-12.9.0_575.51.03-1.x86_64.rpm
```
- Install the local repo RPM:
```bash
sudo dnf5 install -y ~/Downloads/cuda-repo-fedora41-12-9-local-12.9.0_575.51.03-1.x86_64.rpm
```
- Prevent driver packages from the NVIDIA repo (keep drivers from RPM Fusion):
```bash
# Add excludes to the generated repo file (name may vary slightly)
sudo sed -i '/^\[cuda-/,/^$/ {
/^\s*excludepkgs=/d
}' /etc/yum.repos.d/cuda-fedora41-12-9-local.repo
echo "excludepkgs=cuda-drivers*,nvidia-driver*,xorg-x11-drv-nvidia*,kernel*" | \
sudo tee -a /etc/yum.repos.d/cuda-fedora41-12-9-local.repo
```
- Install the matching toolkit and nvcc from the local NVIDIA repo (exclude drivers):
```bash
sudo dnf5 install -y cuda-toolkit cuda-nvcc-12-9 --exclude='cuda-drivers*'
```
- Make nvcc available to the build (12.9 example):
```bash
export CUDA_HOME=/usr/local/cuda-12.9
export PATH="$CUDA_HOME/bin:$PATH"
export NVCC="$CUDA_HOME/bin/nvcc"
# Verify
nvcc --version
```
- Build with the matching toolkit (example, RTX 3060/Ampere):
```bash
CUDA_ARCH=sm_86 cargo build -p miner-cli --features cuda --release
```
Notes:
- Using the Fedora 41 local repo RPM on Fedora 42 is acceptable for the CUDA user-space toolkit; we only need nvcc and toolchain, not the driver.
- Always keep the NVIDIA driver from RPM Fusion. The excludepkgs line ensures the toolkit install will not replace your driver.
- If nvcc is still not on PATH after install, set NVCC explicitly as above.
### Notes
- Ensure your user can access the GPU device nodes (e.g., part of the video group when required).
- The build script compiles any .cu under crates/engine-gpu-cuda/src/kernels into PTX and sets ENGINE_GPU_CUDA_PTX_DIR for the crate to load at runtime.
- Architecture targeting (optional, per-GPU tuning):
- Ampere (RTX 3060): `CUDA_ARCH=sm_86 cargo build -p miner-cli --features cuda --release`
- Ada (RTX 4090): `CUDA_ARCH=sm_89 cargo build -p miner-cli --features cuda --release`
- CC 12.0 (e.g., RTX 5090):
- Forward-compatible PTX: `CUDA_ARCH=compute_120 cargo build -p miner-cli --features cuda --release`
- Tuned for SM: `CUDA_ARCH=sm_120 cargo build -p miner-cli --features cuda --release`
- If unset, the default `sm_70` PTX will still JIT on newer GPUs (just with less arch-specific tuning).
## Configuration
The service can be configured using command-line arguments or environment variables.
| Argument | Environment Variable | Description | Default |
|-------------------|----------------------|--------------------------------------------|---------------|
| `--port <PORT>` | `MINER_PORT` | The port for the HTTP server to listen on. | `9833` |
| `--workers <N>` | `MINER_WORKERS` | The number of worker threads (logical CPUs) to use for mining. | Auto-detected (leaves ~half available) |
Example:
```bash
# Run on the default port 9833 using about half of total cpu resources
../target/release/quantus-miner
# Run on a custom port with 4 workers (logical CPUs)
../target/release/quantus-miner --port 8000 --workers 4
# Equivalent using environment variables
export MINER_PORT=8000
export MINER_WORKERS=4
../target/release/quantus-miner
```
The binary will be available at `target/release/quantus-miner`.
## Running
After building the service, you can run it directly from the command line:
```bash
# CPU-only mining (default: auto-detected CPU cores)
./target/release/quantus-miner --cpu-workers 4
# GPU-only mining (requires --features gpu build)
./target/release/quantus-miner --gpu-workers 1
# Hybrid CPU+GPU mining
./target/release/quantus-miner --cpu-workers 4 --gpu-workers 1
# Custom port and metrics
./target/release/quantus-miner --cpu-workers 2 --port 8000 --metrics-port 9900
```
## Configuration
| Argument | Environment Variable | Description | Default |
|----------|---------------------|-------------|---------|
| `--cpu-workers <N>` | `MINER_CPU_WORKERS` | Number of CPU worker threads | Auto-detect |
| `--gpu-workers <N>` | `MINER_GPU_WORKERS` | Number of GPU worker threads | 0 |
| `--port <PORT>` | `MINER_PORT` | HTTP API port | 9833 |
| `--metrics-port <PORT>` | `MINER_METRICS_PORT` | Prometheus metrics port | Disabled |
## GPU Mining
GPU support uses WGPU for cross-platform acceleration:
- **macOS**: Metal backend (Apple Silicon & Intel)
- **Linux**: Vulkan/OpenGL backends
- **Windows**: DirectX 12/Vulkan backends
### Setup
**Build with GPU support:**
```bash
cargo build -p miner-cli --features gpu --release
```
**Platform requirements:**
- **macOS**: Works out-of-the-box
- **Linux**: Install GPU drivers (`nvidia-driver`, `mesa-vulkan-drivers`)
- **Windows**: Ensure recent graphics drivers are installed
### Performance Monitoring
- **macOS**: `sudo powermetrics --samplers gpu_power -i 1000`
- **Linux**: `nvidia-smi` (NVIDIA) or `radeontop` (AMD)
- **Windows**: Task Manager GPU tab
## Examples
```bash
# Run with default settings
RUST_LOG=info ../target/release/quantus-miner
# CPU mining with 8 workers
./target/release/quantus-miner --cpu-workers 8
# Run with a specific port and 2 workers
RUST_LOG=info ../target/release/quantus-miner --port 12345 --workers 2
# Pure GPU mining
./target/release/quantus-miner --gpu-workers 1
# Run in debug mode
RUST_LOG=info,miner=debug ../target/release/quantus-miner --workers 4
# Hybrid mining: 4 CPU + 1 GPU workers
./target/release/quantus-miner --cpu-workers 4 --gpu-workers 1
# With verbose logging
RUST_LOG=debug ./target/release/quantus-miner --cpu-workers 2 --gpu-workers 1
# Production setup with metrics
./target/release/quantus-miner \
--cpu-workers 6 \
--gpu-workers 1 \
--port 9833 \
--metrics-port 9900
```
The service will start and log messages to the console, indicating the port it's listening on and the number of worker threads in use.
## API Endpoints
Example output:
- `POST /mine`: Submit mining job
- `GET /result/{job_id}`: Get job status/result
- `POST /cancel/{job_id}`: Cancel job
```
INFO external_miner > Starting external miner service...
INFO external_miner > Using auto-detected workers (leaving headroom): 4
INFO external_miner > Server starting on 0.0.0.0:9833
Full API specification: `api/openapi.yaml`
## Docker
```bash
# Quick start
docker pull ghcr.io/quantus-network/quantus-miner:latest
docker run -d -p 9833:9833 -p 9900:9900 \
ghcr.io/quantus-network/quantus-miner:latest \
--cpu-workers 4 --metrics-port 9900
# Build from source
docker build -t quantus-miner .
docker run -d -p 9833:9833 quantus-miner --cpu-workers 4
```
## API Specification
## Benchmarking
The detailed API specification is defined using OpenAPI 3.0 and can be found in the `api/openapi.yaml` file.
```bash
# Benchmark CPU performance
./target/release/quantus-miner benchmark --cpu-workers 8 --duration 30
This specification details all endpoints, request/response formats, and expected status codes.
You can use tools like [Swagger Editor](https://editor.swagger.io/)
or [Swagger UI](https://swagger.io/tools/swagger-ui/) to view and interact with the API definition.
# Benchmark GPU performance
./target/release/quantus-miner benchmark --gpu-workers 1 --duration 30
## A note on workers
The miner previously used a flag named `--num-cores`. To better reflect intent, this has been replaced by `--workers`, which specifies the number of worker threads (logical CPUs). When not provided, the miner auto-detects an effective CPU set (honoring cgroup cpusets when present) and defaults to a value that leaves roughly half of the system resources available to other processes.
## API Endpoints (Summary)
* `POST /mine`: Submits a new mining job.
* `GET /result/{job_id}`: Retrieves the status and result of a specific mining job.
* `POST /cancel/{job_id}`: Cancels an ongoing mining job.
## Implementation and PR review docs
These documents provide reviewers with the authoritative context for changes. Commits and pull requests should link to the relevant prompt/response entry.
- Authoring/process guide: agents.md
# Benchmark hybrid performance
./target/release/quantus-miner benchmark --cpu-workers 4 --gpu-workers 1 --duration 30
```

View File

@@ -113,7 +113,7 @@ Use the stable toolchain.
- cargo test --workspace --locked
- Runtime sanity (typical)
- cargo run -p miner-cli -- --engine cpu-fast --workers <n>
- cargo run -p miner-cli -- --engine cpu --workers <n>
- Check logs and metrics if enabled (see below)
Notes:
@@ -138,15 +138,14 @@ If PRs introduce new feature flags or targets, document how CI should build them
## Engine selection policy (naming and runtime)
Naming:
- CPU engines use Cpu-prefixed variants in the CLI to avoid future collisions and clarify behavior:
- cpu-baseline, cpu-fast, cpu-chain-manipulator
- GPU placeholders are exposed for UX and planning but currently unimplemented:
- gpu-cuda, gpu-opencl
- CPU engines use simple, clear names:
- cpu, cpu-chain-manipulator
- GPU engine is implemented and functional:
- gpu for cross-platform GPU mining using WGPU
Runtime behavior:
- If a GPU engine is selected, the service logs a clear error and exits non-zero:
- engine 'gpu-cuda' is not implemented yet; use cpu-fast or cpu-baseline.”
- Reviewers expect this behavior to remain until real GPU engines are implemented.
- All engines are fully implemented and production-ready
- GPU engine automatically detects and optimizes for available hardware
---

View File

@@ -15,7 +15,6 @@ default = ["baseline"]
# Map engine features to pow-core features so engine-cpu consumers can toggle them.
baseline = ["pow-core/baseline"]
montgomery = ["pow-core/montgomery"]
simd-poseidon2 = ["pow-core/simd-poseidon2"]
[dependencies]

View File

@@ -11,15 +11,15 @@ fn bench_cpu_fast_engine(c: &mut Criterion) {
let cancel_flag = AtomicBool::new(false);
let large_range = Range {
start: U512::from(1000u64),
end: U512::from(101000u64), // 100,000 nonces
start: U512::from(0u64),
end: U512::from(100000u64), // 100,000 nonces
};
c.bench_function("cpu_fast_large_range", |b| {
b.iter(|| {
let mut header = [0u8; 32];
rand::thread_rng().fill_bytes(&mut header);
let difficulty = U512::from(1000u64);
let difficulty = U512::from(10_000_000u64);
let ctx = JobContext::new(header, difficulty);
let result = engine.search_range(

View File

@@ -89,6 +89,9 @@ pub trait MinerEngine: Send + Sync {
/// - Return `Exhausted` if the range is fully searched without a solution.
/// - Return `Cancelled` if `cancel` was observed during the search.
fn search_range(&self, ctx: &JobContext, range: Range, cancel: &AtomicBool) -> EngineStatus;
/// Enable downcasting to concrete engine types.
fn as_any(&self) -> &dyn std::any::Any;
}
/// Baseline CPU engine.
@@ -113,6 +116,10 @@ impl MinerEngine for BaselineCpuEngine {
JobContext::new(header_hash, difficulty)
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn search_range(&self, ctx: &JobContext, range: Range, cancel: &AtomicBool) -> EngineStatus {
// Ensure start <= end (inclusive range). If not, treat as exhausted.
if range.start > range.end {
@@ -176,13 +183,17 @@ impl FastCpuEngine {
impl MinerEngine for FastCpuEngine {
fn name(&self) -> &'static str {
"cpu-fast"
"cpu"
}
fn prepare_context(&self, header_hash: [u8; 32], difficulty: U512) -> JobContext {
JobContext::new(header_hash, difficulty)
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn search_range(&self, ctx: &JobContext, range: Range, cancel: &AtomicBool) -> EngineStatus {
use pow_core::{hash_from_nonce, is_valid_hash, step_nonce};
@@ -274,6 +285,10 @@ impl MinerEngine for ChainManipulatorEngine {
ctx
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn search_range(&self, ctx: &JobContext, range: Range, cancel: &AtomicBool) -> EngineStatus {
use pow_core::{hash_from_nonce, is_valid_hash, step_nonce};

View File

@@ -1,37 +0,0 @@
[package]
name = "engine-gpu-cuda"
version.workspace = true
edition.workspace = true
publish = false
description = "CUDA-based GPU mining engine for the Quantus External Miner (placeholder crate)"
[lib]
path = "src/lib.rs"
crate-type = ["rlib"]
[features]
default = []
# Enable CUDA integrations when implementing the engine.
cuda = ["dep:cust"]
# Enable metrics emission from the CUDA engine (e.g., false-positive counter).
metrics = ["dep:metrics"]
[dependencies]
pow-core = { path = "../pow-core" }
engine-cpu = { path = "../engine-cpu" }
primitive-types = { workspace = true }
log = { workspace = true }
thiserror = { workspace = true }
anyhow = { workspace = true }
hex = { workspace = true }
# Metrics (optional; used for emitting generic counters/gauges from this engine)
metrics = { path = "../metrics", optional = true }
# CUDA ecosystem (optional for now; will be used when implementing GPU backend)
cust = { version = "0.3", optional = true }
rustacuda = { version = "0.1", optional = true }
# Host-side hashing and big-int precompute for GPU constants
qp-poseidon-core = { workspace = true }
num-bigint = "0.4"
num-traits = { workspace = true }

View File

@@ -1,489 +0,0 @@
use std::env;
use std::ffi::OsStr;
use std::fs;
use std::io::{self};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
/// Return true when the host GCC major version is greater than 14.
/// We try $CC --version first, then gcc --version, and parse the first X.Y-like token.
fn host_gcc_too_new() -> bool {
fn probe(bin: &str) -> Option<String> {
Command::new(bin)
.arg("--version")
.stdout(Stdio::piped())
.output()
.ok()
.and_then(|o| {
if o.status.success() {
Some(String::from_utf8_lossy(&o.stdout).into_owned())
} else {
None
}
})
}
let cc = env::var("CC").unwrap_or_else(|_| "gcc".to_string());
let ver_out = probe(&cc).or_else(|| probe("gcc"));
if let Some(s) = ver_out {
// Scan tokens separated by non [0-9|.] and take the first X.Y...
for tok in s.split(|c: char| !c.is_ascii_digit() && c != '.') {
if tok.is_empty() {
continue;
}
if let Some((maj, _)) = tok.split_once('.') {
if let Ok(m) = maj.parse::<u32>() {
return m > 14;
}
}
}
}
false
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Only do CUDA work if this crate is built with the `cuda` feature.
let cuda_feature_enabled = env::var_os("CARGO_FEATURE_CUDA").is_some();
println!("cargo:rerun-if-env-changed=CARGO_FEATURE_CUDA");
if !cuda_feature_enabled {
// Generate empty bindings so include! compiles; skip CUDA work.
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
generate_empty_bindings(&out_dir)?;
println!("cargo:warning=engine-gpu-cuda built without 'cuda' feature; skipping kernel compilation.");
return Ok(());
}
// Gather kernel sources (.cu) from common locations
let kernel_roots = [
Path::new("src").join("kernels"),
Path::new("src").join("cuda"),
];
let mut cu_files: Vec<PathBuf> = Vec::new();
for root in kernel_roots.iter() {
if root.is_dir() {
scan_cu_files(root, &mut cu_files)?;
}
}
// If there are no kernels yet, still generate an empty bindings file so include! compiles.
if cu_files.is_empty() {
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
generate_empty_bindings(&out_dir)?;
println!("cargo:warning=No CUDA kernels (.cu) found under src/kernels or src/cuda; generated empty PTX/CUBIN bindings.");
return Ok(());
}
// Emit rebuild hints for kernels and env vars that affect compilation
for cu in &cu_files {
println!("cargo:rerun-if-changed={}", cu.display());
}
println!("cargo:rerun-if-env-changed=NVCC");
println!("cargo:rerun-if-env-changed=CUDA_HOME");
println!("cargo:rerun-if-env-changed=CUDA_PATH");
println!("cargo:rerun-if-env-changed=CUDA_ARCH");
println!("cargo:rerun-if-env-changed=MINER_CUDA_ALLOW_UNSUPPORTED_COMPILER");
println!("cargo:rerun-if-env-changed=MINER_NVCC_CCBIN");
// Determine nvcc path
let nvcc = find_nvcc().ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
"Unable to locate `nvcc`. Set NVCC or CUDA_HOME/CUDA_PATH, or ensure nvcc is in PATH.",
)
})?;
// Architecture (SM version) can be overridden; default to sm_70 as a reasonable baseline for modern GPUs
let raw_arch = env::var("CUDA_ARCH").unwrap_or_else(|_| "sm_70".to_string());
// Normalize to a compute/sm pair:
// - Use compute_* for -arch
// - Use sm_* for -code
let (arch_compute, arch_sm) = if let Some(s) = raw_arch.strip_prefix("sm_") {
(format!("compute_{s}"), raw_arch.clone())
} else if let Some(s) = raw_arch.strip_prefix("compute_") {
(raw_arch.clone(), format!("sm_{s}"))
} else if let Some(s) = raw_arch.strip_prefix("sm") {
(format!("compute{s}"), raw_arch.clone())
} else {
("compute_70".to_string(), "sm_70".to_string())
};
println!("cargo:warning=NVCC={}", nvcc.display());
println!("cargo:warning=CUDA_ARCH (normalized): compute={arch_compute}, sm={arch_sm}");
// Output directory for artifacts
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
println!(
"cargo:rustc-env=ENGINE_GPU_CUDA_PTX_DIR={}",
out_dir.display()
);
// Preflight: locate CUDA include directories and ensure cuda_runtime.h exists.
// Prefer $CUDA_HOME/targets/x86_64-linux/include, then $CUDA_HOME/include. Derive CUDA_HOME from NVCC if needed.
let cuda_home_env = env::var_os("CUDA_HOME").map(PathBuf::from).or_else(|| {
// Derive CUDA_HOME from NVCC path (<home>/bin/nvcc)
nvcc.parent()
.and_then(Path::parent)
.map(|p| p.to_path_buf())
});
let mut include_dirs: Vec<PathBuf> = Vec::new();
// If CUDA_HOME was found (either env or derived from NVCC), add its common include locations.
if let Some(home) = cuda_home_env.as_ref() {
let t_inc = home.join("targets").join("x86_64-linux").join("include");
let i_inc = home.join("include");
if t_inc.is_dir() {
include_dirs.push(t_inc);
}
if i_inc.is_dir() {
include_dirs.push(i_inc);
}
}
// Fallback: common system install prefix in CUDA devel images.
// This allows builds to succeed even when CUDA_HOME isn't set in the container.
for fallback in [
Path::new("/usr/local/cuda/targets/x86_64-linux/include"),
Path::new("/usr/local/cuda/include"),
] {
if fallback.is_dir() {
include_dirs.push(fallback.to_path_buf());
}
}
// Deduplicate include dirs to avoid noisy logs and duplicate -I flags
include_dirs.sort();
include_dirs.dedup();
for inc in &include_dirs {
println!("cargo:warning=CUDA_INCLUDE_DIR={}", inc.display());
}
let has_runtime_h = include_dirs
.iter()
.any(|d| d.join("cuda_runtime.h").is_file());
if !has_runtime_h {
return Err(Box::new(io::Error::other(format!(
"engine-gpu-cuda: missing CUDA headers (cuda_runtime.h). \
Ensure the matching CUDA toolkit is installed (see README), \
and CUDA_HOME is set (current CUDA_HOME={:?}). Searched: {}",
cuda_home_env,
include_dirs
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join(", ")
))));
}
// Extra nvcc options and optional host compiler override
let mut extra_nvcc_flags: Vec<String> = Vec::new();
let allow_unsupported = env::var("MINER_CUDA_ALLOW_UNSUPPORTED_COMPILER")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false);
if allow_unsupported || host_gcc_too_new() {
println!("cargo:warning=nvcc: enabling -allow-unsupported-compiler");
extra_nvcc_flags.push("-allow-unsupported-compiler".to_string());
}
let ccbin = env::var("MINER_NVCC_CCBIN").ok();
if let Some(ref cc) = ccbin {
println!("cargo:warning=nvcc: using -ccbin {cc}");
}
// Compile each .cu into artifacts
// - PTX: for driver JIT (fallback)
// - CUBIN: native SASS per-SM (preferred; avoids PTX JIT/ISA mismatches)
let mut generated_ptx: Vec<(String, PathBuf)> = Vec::new();
let mut generated_cubin: Vec<(String, PathBuf)> = Vec::new();
for cu in &cu_files {
let stem = cu
.file_stem()
.and_then(OsStr::to_str)
.ok_or_else(|| io::Error::other("Invalid kernel filename"))?
.to_string();
let ptx_path = out_dir.join(format!("{stem}.ptx"));
let cubin_path = out_dir.join(format!("{stem}.cubin"));
// PTX
match compile_to_ptx(
&nvcc,
&arch_compute,
cu,
&ptx_path,
&include_dirs,
ccbin.as_deref(),
&extra_nvcc_flags,
) {
Ok(()) => {
println!(
"cargo:warning=nvcc PTX OK: {} -> {}",
cu.display(),
ptx_path.display()
);
generated_ptx.push((stem.clone(), ptx_path.clone()));
}
Err(err) => {
eprintln!("cargo:warning=nvcc PTX failed for {}: {err}", cu.display());
eprintln!(
"cargo:warning=Skipping PTX for {stem} (will rely on CUBIN if available)"
);
}
}
// CUBIN (preferred at runtime)
match compile_to_cubin(
&nvcc,
&arch_compute,
&arch_sm,
cu,
&cubin_path,
&include_dirs,
ccbin.as_deref(),
&extra_nvcc_flags,
) {
Ok(()) => {
println!(
"cargo:warning=nvcc CUBIN OK: {} -> {}",
cu.display(),
cubin_path.display()
);
generated_cubin.push((stem, cubin_path.clone()));
}
Err(err) => {
eprintln!(
"cargo:warning=nvcc CUBIN failed for {}: {err}",
cu.display()
);
eprintln!(
"cargo:warning=Skipping CUBIN for {stem} (will fall back to PTX if present)"
);
}
}
}
// Fail fast if no artifacts were generated (prevents producing a GPU binary without embeds)
if generated_ptx.is_empty() && generated_cubin.is_empty() {
return Err(Box::new(io::Error::other(
"engine-gpu-cuda: no CUDA artifacts embedded (PTX/CUBIN). Ensure NVCC is set and CUDA_ARCH=sm_XX; see README Fedora section.",
)));
}
// Generate an embedded PTX/CUBIN bindings file so the crate can include!() the artifacts at compile time.
// Output: $OUT_DIR/ptx_bindings.rs with:
// - pub mod ptx_embedded { pub const <STEM>_PTX: &str = "..."; pub fn get(name: &str) -> Option<&'static str>; }
// - pub mod cubin_embedded { pub static <STEM>_CUBIN: &'static [u8] = &[…]; pub fn get_cubin(name: &str) -> Option<&'static [u8]>; }
let bindings_path = out_dir.join("ptx_bindings.rs");
let mut rs = String::new();
rs.push_str("// @generated by engine-gpu-cuda/build.rs — DO NOT EDIT MANUALLY\n");
rs.push_str("#[allow(dead_code)]\n");
// PTX module (string)
rs.push_str("pub mod ptx_embedded {\n");
for (stem, path) in &generated_ptx {
let const_name = to_const_name(stem, "_PTX");
let ptx = std::fs::read_to_string(path).unwrap_or_else(|_| String::new());
rs.push_str(&format!(" pub const {const_name}: &str = r###\""));
rs.push_str(&ptx);
rs.push_str("\"###;\n");
}
rs.push_str(" pub fn get(name: &str) -> Option<&'static str> {\n");
rs.push_str(" match name {\n");
for (stem, _) in &generated_ptx {
let const_name = to_const_name(stem, "_PTX");
rs.push_str(&format!(" \"{stem}\" => Some({const_name}),\n"));
}
rs.push_str(" _ => None,\n");
rs.push_str(" }\n");
rs.push_str(" }\n");
rs.push_str("}\n");
// CUBIN module (bytes)
rs.push_str("pub mod cubin_embedded {\n");
for (stem, path) in &generated_cubin {
let const_name = to_const_name(stem, "_CUBIN");
let bytes = std::fs::read(path).unwrap_or_default();
rs.push_str(&format!(
" pub static {const_name}: &'static [u8] = &[\n "
));
for (i, b) in bytes.iter().enumerate() {
if i > 0 && i % 16 == 0 {
rs.push_str("\n ");
}
rs.push_str(&format!("{b},"));
}
rs.push_str("\n ];\n");
}
rs.push_str(" pub fn get_cubin(name: &str) -> Option<&'static [u8]> {\n");
rs.push_str(" match name {\n");
for (stem, _) in &generated_cubin {
let const_name = to_const_name(stem, "_CUBIN");
rs.push_str(&format!(" \"{stem}\" => Some({const_name}),\n"));
}
rs.push_str(" _ => None,\n");
rs.push_str(" }\n");
rs.push_str(" }\n");
rs.push_str("}\n");
std::fs::write(&bindings_path, rs)?;
Ok(())
}
fn to_const_name(stem: &str, suffix: &str) -> String {
let mut s = String::with_capacity(stem.len() + suffix.len());
for c in stem.chars() {
if c.is_ascii_alphanumeric() {
s.push(c.to_ascii_uppercase());
} else {
s.push('_');
}
}
s.push_str(suffix);
s
}
fn generate_empty_bindings(out_dir: &Path) -> io::Result<()> {
let bindings_path = out_dir.join("ptx_bindings.rs");
let mut rs = String::new();
rs.push_str("// @generated by engine-gpu-cuda/build.rs — DO NOT EDIT MANUALLY\n");
rs.push_str("#[allow(dead_code)]\n");
rs.push_str("pub mod ptx_embedded { pub fn get(_: &str) -> Option<&'static str> { None } }\n");
rs.push_str(
"pub mod cubin_embedded { pub fn get_cubin(_: &str) -> Option<&'static [u8]> { None } }\n",
);
std::fs::write(&bindings_path, rs)
}
fn scan_cu_files(root: &Path, out: &mut Vec<PathBuf>) -> io::Result<()> {
for entry in fs::read_dir(root)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
scan_cu_files(&path, out)?;
} else if path.extension().and_then(OsStr::to_str) == Some("cu") {
out.push(path);
}
}
Ok(())
}
fn find_nvcc() -> Option<PathBuf> {
// 1) Explicit override
if let Some(p) = env::var_os("NVCC") {
let pb = PathBuf::from(p);
if pb.is_file() {
return Some(pb);
}
}
// 2) CUDA_HOME or CUDA_PATH
if let Some(home) = env::var_os("CUDA_HOME").or_else(|| env::var_os("CUDA_PATH")) {
let mut candidate = PathBuf::from(home);
candidate.push("bin");
candidate.push(exe("nvcc"));
if candidate.is_file() {
return Some(candidate);
}
}
// 3) PATH lookup — try invoking `nvcc --version`
if let Ok(output) = Command::new("nvcc").arg("--version").output() {
if output.status.success() {
return Some(PathBuf::from("nvcc"));
}
}
None
}
fn compile_to_ptx(
nvcc: &Path,
arch: &str,
src: &Path,
out: &Path,
includes: &[PathBuf],
ccbin: Option<&str>,
extra_flags: &[String],
) -> io::Result<()> {
// Ensure parent directory exists
if let Some(parent) = out.parent() {
fs::create_dir_all(parent)?;
}
// nvcc -ptx -arch=<compute_XX> -I<inc>... [-ccbin cc] [extra] -o <out.ptx> <src.cu>
let mut cmd = Command::new(nvcc);
cmd.args(["-ptx", "-arch", arch]).arg("-o").arg(out);
if let Some(cc) = ccbin {
cmd.arg("-ccbin").arg(cc);
}
for f in extra_flags {
cmd.arg(f);
}
for inc in includes {
cmd.arg("-I").arg(inc);
}
cmd.arg(src);
let status = cmd.status()?;
if !status.success() {
return Err(io::Error::other(format!(
"nvcc returned non-zero exit status (ptx): {status}"
)));
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn compile_to_cubin(
nvcc: &Path,
arch_compute: &str,
arch_sm: &str,
src: &Path,
out: &Path,
includes: &[PathBuf],
ccbin: Option<&str>,
extra_flags: &[String],
) -> io::Result<()> {
// Ensure parent directory exists
if let Some(parent) = out.parent() {
fs::create_dir_all(parent)?;
}
// nvcc -cubin -arch=<compute_XX> -code=<sm_XX> -I<inc>... [-ccbin cc] [extra] -o <out.cubin> <src.cu>
let mut cmd = Command::new(nvcc);
cmd.args(["-cubin", "-arch", arch_compute, "-code", arch_sm])
.arg("-o")
.arg(out);
if let Some(cc) = ccbin {
cmd.arg("-ccbin").arg(cc);
}
for f in extra_flags {
cmd.arg(f);
}
for inc in includes {
cmd.arg("-I").arg(inc);
}
cmd.arg(src);
let status = cmd.status()?;
if !status.success() {
return Err(io::Error::other(format!(
"nvcc returned non-zero exit status (cubin): {status}"
)));
}
Ok(())
}
#[cfg(target_os = "windows")]
fn exe(name: &str) -> String {
format!("{name}.exe")
}
#[cfg(not(target_os = "windows"))]
fn exe(name: &str) -> String {
name.to_string()
}

View File

@@ -1,212 +0,0 @@
# engine-gpu-cuda (CUDA backend) G1 bringup
This crate provides the CUDA GPU backend for the Quantus External Miner. It currently implements "G1" bringup: the pernonce modular multiply loop runs on the GPU (512bit Montgomery CIOS), while Poseidon2512 and the threshold check run on the host CPU. This allows correctness and plumbing to be validated before we move Poseidon2 and earlyexit onto the device in G2.
The backend is featuregated. When built with `--features cuda`, the crates build script compiles the CUDA kernel and embeds device images into the binary (CUBIN preferred, PTX as fallback). At runtime the engine selects an embedded image and launches the kernel to produce normalized `y` values per iteration.
---
## Status
- G1 (current):
- Device: 512bit Montgomery multiply (CIOS using 64×64→128 via `__umul64hi`), maintaining `ŷ` in Montgomery domain and converting to normal domain for output.
- Host: Poseidon2512 and threshold compare (and orchestration).
- Correctness: parity against CPU smallrange tests.
- Performance: primarily limited by PCIe copyback and host Poseidon2. See "Tuning" below.
- G2 (next):
- Device: Poseidon2512 optimized for 64byte input.
- Device: threshold compare + global earlyexit flag (atomic) + tiny candidate write.
- Device: move constants to `__constant__` memory.
- Host: poll earlyexit; no large copybacks (only candidate or counters).
- Result: removes PCIe and hostPoseidon2 bottlenecks; enables real GPUbound throughput. Selection will be enabled via `MINER_CUDA_MODE=g2` once available.
---
## Build
The build script (for this crate only) compiles `crates/engine-gpu-cuda/src/kernels/qpow_kernel.cu` with `nvcc` and embeds both:
- CUBIN (native SASS for a specific SM, preferred at runtime).
- PTX (forwardcompatible target, used as a fallback when necessary).
Header preflight: the build verifies CUDA headers (e.g., `cuda_runtime.h`) exist. If it cannot embed either PTX or CUBIN, the build fails fast to prevent emitting a GPUbroken binary.
Supported build modes:
- Native build (host has a CUDA toolkit): `cargo build -p miner-cli --features cuda --release`
- Containerized build (recommended for CI): build inside NVIDIAs CUDA devel images; extract the binary (see repo workflow and Containerfile).
Notes:
- The kernel is compiled for one SM arch per build. Choose your SM via `CUDA_ARCH=sm_86|sm_89|sm_120` (see “Env knobs buildtime”).
- For driver/toolkit compatibility (e.g., CUDA 12.9 driver), build the device images with a matching toolkit (e.g., 12.9).
---
## Runtime selection and embeds
At startup, the engine prefers the embedded CUBIN; if absent it falls back to the embedded PTX. You can override with `MINER_CUDA_IMAGE=cubin|ptx`. To attempt the G2 path (device Poseidon2 + early-exit), set `MINER_CUDA_MODE=g2`; if the G2 kernel isn't embedded/available for the current device, the engine will fall back to G1 automatically. You'll see logs like:
- `CUDA: using CUBIN (embedded)`
- `CUDA: using PTX source = embedded`
- (If neither exists, the engine logs the absence and delegates to CPU fast engine.)
When a job runs, the engine prints its launch configuration and perlaunch outcomes:
- `CUDA launch config: block_dim=…, threads=…, iters=…`
- `CUDA launch: grid_dim=…, block_dim=…, threads=…, iters=…`
- `CUDA kernel and sync OK`
- `CUDA copy-back OK: elems=…`
---
## Env knobs runtime (G1)
These knobs affect GPU launch shape and how much work is returned to the host (and thus how much Poseidon2 the CPU must perform per launch).
- `MINER_CUDA_BLOCK_DIM` (default `256`)
- Threads per block (`blockDim.x`). Use a multiple of 32 (warp size). 256 is a good default.
- `MINER_CUDA_THREADS`
- Total threads (grid workload). Grid dimension is `grid_dim = ceil(threads / block_dim)`. Target at least “#SMs × 12 blocks” for decent occupancy (e.g., RTX 3060 has 28 SMs → 28 or 32 blocks).
- `MINER_CUDA_ITERS`
- Iterations per thread. Higher values produce larger output buffers and more host Poseidon2 work per launch.
- `MINER_CUDA_IMAGE` = `cubin` | `ptx` (optional)
- Overrides the embedded image choice (debugging/testing). Default is to prefer CUBIN.
- `MINER_CUDA_HASH_THREADS` (optional)
- Number of host Poseidon2 worker threads to use to consume GPU output. Defaults to available parallelism.
- `MINER_CUDA_PINNED` = `1|true` (optional)
- Use pinned (page-locked) host buffers and asynchronous device-to-host copies for G1 copy-back to reduce PCIe latency.
- `MINER_CUDA_MODE` = `g2` (optional)
- Attempt G2 kernel (device Poseidon2-512 + threshold compare + early-exit). Falls back to G1 if the G2 kernel is not available for the current device image.
How much data per launch?
- y_out bytes = `threads × iters × 64`.
- Keep this around 64128 MB in G1 to avoid PCIe and host Poseidon2 dominating.
Example configs (RTX 3060, SM 86):
- ~64 MB per launch:
- `BLOCK_DIM=256`, `THREADS=8192`, `ITERS=128`
- ~128 MB per launch:
- `BLOCK_DIM=256`, `THREADS=8192`, `ITERS=256`
- Good occupancy with ~117 MB:
- `BLOCK_DIM=256`, `THREADS=7168` (28 blocks), `ITERS=256`
Service workers:
- Keep `--workers 1` for the GPU engine to avoid competing GPU launches.
- The engine internally orchestrates chunking and cancellation.
---
## Env knobs buildtime (crate build script)
- `CUDA_ARCH` (required for device image quality)
- One of: `sm_86` (Ampere 3060), `sm_89` (Ada 4090), `sm_120` (CC 12.0 / 5090), etc.
- Normalized to `(compute_XX, sm_XX)` internally.
- `NVCC` or `CUDA_HOME` or `CUDA_PATH`
- Path to `nvcc` or CUDA toolkit root.
- `MINER_CUDA_ALLOW_UNSUPPORTED_COMPILER` = `1` (optional)
- Adds `-allow-unsupported-compiler` to `nvcc` if host GCC is newer than the toolkit supports.
- `MINER_NVCC_CCBIN` = `/path/to/g++-14` (optional)
- Forces `nvcc -ccbin` to a specific (supported) host C++ compiler.
- (Container builds) The script attempts `/usr/local/cuda/targets/x86_64-linux/include` and `/usr/local/cuda/include` if `CUDA_HOME` is unset.
The build fails with clear messages if:
- `nvcc` cannot be found,
- CUDA headers cannot be found,
- neither PTX nor CUBIN artifacts were embedded.
---
## Tuning guide (G1)
Goal in G1: balance kernel time (GPU) against copy-back time (PCIe) and host Poseidon2 time (CPU) to avoid starving the GPU or overwhelming the host. Practical steps:
1) Size the output buffer:
- Start with 64128 MB per launch: `bytes ≈ threads × iters × 64`.
- Increase `threads` to raise occupancy (more blocks). Start with `block_dim=256`.
- Increase `iters` only while host Poseidon2 still keeps up.
2) Watch timings:
- The engine logs `kernel_ms` and `copy_ms`.
- If `copy_ms > kernel_ms`, try lowering `iters` or increasing `threads` to make the kernel heavier relative to copy.
- If CPU is pegged (hashing), lower `iters`.
3) SM occupancy:
- Ensure `grid_dim` (blocks) is at least the number of SMs on the device (e.g., 28+ on RTX 3060).
- Use `threads ≈ block_dim × blocks` with `block_dim=256`.
4) Service settings:
- Keep `--workers 1` when testing the GPU engine to avoid contention.
---
## Troubleshooting
- “missing CUDA headers (cuda_runtime.h)” at build time:
- Install the matching CUDA toolkit or build inside an NVIDIA CUDA devel image (container).
- Ensure `CUDA_HOME=/usr/local/cuda` (container) or set `NVCC` directly.
- “CUDA init failed” at runtime:
- Check driver is installed, `nvidia-smi` works under the service user.
- Ensure the unit has access to `/dev/nvidia*` (no device sandboxing).
- “no embedded PTX/CUBIN” at runtime:
- The binary you deployed might be corrupted (download/transfer). Verify by:
- `strings -a /path/to/binary | grep -m1 QPOW_KERNEL_CUBIN`
- `strings -a /path/to/binary | grep -m1 qpow_montgomery_g1_kernel`
- Rebuild and redeploy; avoid text-mode transfers; verify checksums.
- Driver/toolkit mismatch:
- A CUBIN built by a newer toolkit may fail to load on older drivers. Use a toolkit matching your driver to produce the device images (e.g., CUDA 12.9 for driver 12.9).
---
## Roadmap to G2
- Device Poseidon2512 tuned for 64B input.
- Ondevice threshold compare and earlyexit flag (atomic).
- Host polling and tiny candidate copyback.
- Constants in `__constant__` memory.
- With G2, copybacks and host hashing disappear from the steadystate path, enabling real GPUbound throughput.
---
## Appendix: build in containers (summary)
- Use the repo `Containerfile` and pass:
- `--build-arg CUDA_TAG=12.9.0|13.0.0`
- `--build-arg SM=86|89|120`
- The builder stage compiles and embeds PTX/CUBIN; the dist stage contains `/usr/local/bin/quantus-miner`.
- Export the dist stage filesystem (`buildx --target dist --output type=local,dest=./out`) and pick up `./out/usr/local/bin/quantus-miner`.
- Artifacts can be uploaded directly from CI or rsynced to your distribution host.
---
## Quick reference (env knobs)
Runtime:
- `MINER_CUDA_BLOCK_DIM` (default `256`) — threads per block.
- `MINER_CUDA_THREADS` — total threads (increase for more blocks).
- `MINER_CUDA_ITERS` — iterations per thread (controls y_out size).
- `MINER_CUDA_IMAGE` = `cubin|ptx` — force embedded image selection (optional).
- `MINER_CUDA_HASH_THREADS` — parallel host Poseidon2 workers (optional).
- `MINER_CUDA_PINNED` = `1|true` — use pinned host buffers + async D2H copy (G1 optimization).
- `MINER_CUDA_MODE` = `g2` — try device Poseidon2 + early-exit; falls back to G1 if G2 kernel isn't available.
Build-time:
- `CUDA_ARCH` = `sm_86|sm_89|sm_120|…` — SM target for device images (normalized internally).
- `NVCC` or `CUDA_HOME` or `CUDA_PATH` — where to find the toolkit.
- `MINER_CUDA_ALLOW_UNSUPPORTED_COMPILER` = `1` — add `-allow-unsupported-compiler`.
- `MINER_NVCC_CCBIN` = `/path/to/g++-14` — force a specific host compiler.
---
## Example environment presets
Presets are provided under `examples/.env` and follow a “lower” (≈1× SMs blocks) and “upper” (≈2× SMs blocks) pattern per GPU:
- RTX 3060: `cuda-miner-3060-lower.env`, `cuda-miner-3060-upper.env`
- RTX 3080: `cuda-miner-3080-lower.env`, `cuda-miner-3080-upper.env`
- RTX 3090: `cuda-miner-3090-lower.env`, `cuda-miner-3090-upper.env`
- RTX 4080: `cuda-miner-4080-lower.env`, `cuda-miner-4080-upper.env`
- RTX 4090: `cuda-miner-4090-lower.env`, `cuda-miner-4090-upper.env`
- RTX 5090: `cuda-miner-5090-lower.env`, `cuda-miner-5090-upper.env`
- RTX A5000: `cuda-miner-a5000-lower.env`, `cuda-miner-a5000-upper.env`
- RTX A6000: `cuda-miner-a6000-lower.env`, `cuda-miner-a6000-upper.env`
Each preset uses `MINER_CUDA_MODE=g2` (device Poseidon2 + early-exit) and `MINER_CUDA_BLOCK_DIM=256`, and sizes `MINER_CUDA_THREADS` as `blocks × 256`. Adjust `MINER_CUDA_ITERS` to tune kernel dwell time vs early-exit responsiveness. If a G2 kernel image isn't embedded for your device, the engine falls back to G1 automatically.

View File

@@ -1,760 +0,0 @@
/**
* Quantus External Miner - CUDA Kernel (G1 bring-up)
*
* This kernel provides a minimal, correctness-first pipeline for the GPU path:
* - Implement 512-bit Montgomery multiplication (8×64-bit limbs) on device.
* - Keep y in Montgomery domain during iteration and convert to normal domain before output.
* - For bring-up (G1), the kernel writes normalized y values back to host memory.
* The host will compute SHA3-512(y_be64) and distances for parity validation.
*
* Notes:
* - Limbs are little-endian: limb 0 is the least significant 64 bits.
* - CIOS Montgomery reduction is used with 64×64→128 products via __umul64hi.
* - This skeleton intentionally excludes early-exit and on-device SHA3; those are part of G2.
*
* Build:
* - The engine-gpu-cuda crate provides a build.rs that compiles this .cu into PTX when the
* "cuda" feature is enabled, placing artifacts under $OUT_DIR and exposing ENGINE_GPU_CUDA_PTX_DIR.
*/
#include <stdint.h>
#include <cuda_runtime.h>
extern "C" {
// -------------------------------------------------------------------------------------------------
// Utilities: 64×64→128 multiply (lo, hi), add-with-carry helpers, compare/subtract
// -------------------------------------------------------------------------------------------------
__device__ __forceinline__ void mul64wide(uint64_t a, uint64_t b, uint64_t &lo, uint64_t &hi) {
lo = a * b;
hi = __umul64hi(a, b);
}
// sum := x + y, carry_out returns 0 or 1
__device__ __forceinline__ uint64_t add64_carry(uint64_t x, uint64_t y, uint64_t &carry_out) {
uint64_t s = x + y;
carry_out = (s < x) ? 1ull : 0ull;
return s;
}
// sum := x + y + carry_in, carry_out returns 0 or 1
__device__ __forceinline__ uint64_t add64_2carry(uint64_t x, uint64_t y, uint64_t carry_in, uint64_t &carry_out) {
uint64_t s1 = x + y;
uint64_t c1 = (s1 < x) ? 1ull : 0ull;
uint64_t s2 = s1 + carry_in;
uint64_t c2 = (s2 < s1) ? 1ull : 0ull;
carry_out = c1 + c2;
return s2;
}
// return true if a (LE limbs) >= b (LE limbs), by numeric value
__device__ __forceinline__ bool ge_le_8(const uint64_t a[8], const uint64_t b[8]) {
// Compare from most significant limb to least
for (int i = 7; i >= 0; --i) {
if (a[i] != b[i]) {
return a[i] > b[i];
}
}
return true; // equal
}
// a := a - b (LE limbs)
__device__ __forceinline__ void sub_le_in_place_8(uint64_t a[8], const uint64_t b[8]) {
uint64_t borrow = 0;
for (int i = 0; i < 8; ++i) {
uint64_t bi = b[i];
uint64_t ai = a[i];
uint64_t tmp = ai - bi - borrow;
// borrow occurs if ai < (bi + borrow)
uint64_t needed = (ai < bi) || (borrow && ai == bi) ? 1ull : 0ull;
a[i] = tmp;
borrow = needed;
}
}
// Convert 8 LE limbs into 64 BE bytes into out[64] (for host hashing later, if needed)
// Not used inside the kernel (G1 writes limbs), but kept here for reference.
// __device__ __forceinline__ void le8_to_be64(const uint64_t le[8], uint8_t out[64]) {
// for (int i = 0; i < 8; ++i) {
// uint64_t limb = le[7 - i]; // most significant limb first
// for (int b = 0; b < 8; ++b) {
// out[i * 8 + (7 - b)] = (uint8_t)((limb >> (b * 8)) & 0xFF);
// }
// }
// }
// -------------------------------------------------------------------------------------------------
// Montgomery arithmetic (CIOS) for 512-bit numbers (8×64-bit limbs), little-endian.
// -------------------------------------------------------------------------------------------------
// out <- (a * b * R^{-1}) mod n
__device__ __forceinline__ void mont_mul_512(
const uint64_t a[8],
const uint64_t b[8],
const uint64_t n[8],
const uint64_t n0_inv,
uint64_t out[8]
) {
// 9-limb accumulator (LE); accumulates 128-bit intermediates via split-add with carries
uint64_t acc[9];
#pragma unroll
for (int k = 0; k < 9; ++k) acc[k] = 0ull;
for (int i = 0; i < 8; ++i) {
// acc += a[i] * b
uint64_t ai = a[i];
uint64_t carry = 0ull;
#pragma unroll
for (int j = 0; j < 8; ++j) {
uint64_t lo, hi;
mul64wide(ai, b[j], lo, hi);
// acc[j] += lo + carry, propagate carry to hi
uint64_t c0, c1;
uint64_t s0 = add64_carry(acc[j], lo, c0);
uint64_t s1 = add64_carry(s0, carry, c1);
acc[j] = s1;
// new carry = hi + c0 + c1
carry = hi + c0 + c1;
}
// acc[8] += carry
uint64_t c_acc8;
acc[8] = add64_carry(acc[8], carry, c_acc8);
// c_acc8 overflow beyond 9th limb is discarded (by design in CIOS with next steps)
// m = (acc[0] * n0_inv) mod 2^64
uint64_t m = (uint64_t)(acc[0] * n0_inv);
// acc += m * n
uint64_t carry2 = 0ull;
#pragma unroll
for (int j = 0; j < 8; ++j) {
uint64_t lo2, hi2;
mul64wide(m, n[j], lo2, hi2);
uint64_t c0, c1;
uint64_t s0 = add64_carry(acc[j], lo2, c0);
uint64_t s1 = add64_carry(s0, carry2, c1);
acc[j] = s1;
carry2 = hi2 + c0 + c1;
}
// acc[8] += carry2
uint64_t c_acc8_b;
acc[8] = add64_carry(acc[8], carry2, c_acc8_b);
// Shift acc right by one limb (drop acc[0])
#pragma unroll
for (int j = 0; j < 8; ++j) {
acc[j] = acc[j + 1];
}
acc[8] = 0ull;
}
// Conditional subtract: if acc >= n, subtract n
if (ge_le_8(acc, n)) {
sub_le_in_place_8(acc, n);
}
#pragma unroll
for (int i = 0; i < 8; ++i) {
out[i] = acc[i];
}
}
// to_mont(x) = x * R^2 mod n
__device__ __forceinline__ void to_mont_512(
const uint64_t x[8],
const uint64_t r2[8],
const uint64_t n[8],
const uint64_t n0_inv,
uint64_t out[8]
) {
mont_mul_512(x, r2, n, n0_inv, out);
}
// from_mont(x̂) = x̂ * 1 mod n
__device__ __forceinline__ void from_mont_512(
const uint64_t xhat[8],
const uint64_t n[8],
const uint64_t n0_inv,
uint64_t out[8]
) {
// Multiply by 1 (Montgomery): one = [1,0,..,0]
uint64_t one[8];
#pragma unroll
for (int i = 0; i < 8; ++i) one[i] = 0ull;
one[0] = 1ull;
mont_mul_512(xhat, one, n, n0_inv, out);
}
// -------------------------------------------------------------------------------------------------
// Kernel: G1 bring-up
//
// Each thread:
// - Loads y0 (normal domain) for that thread.
// - Computes y_hat0 = to_mont(y0) on device.
// - Iterates iters_per_thread times:
// y_hat = mont_mul(y_hat, m_hat)
// y = from_mont(y_hat)
// Writes y (LE limbs) to y_out at [thread_offset + iter]
// -------------------------------------------------------------------------------------------------
extern "C" __global__ void qpow_montgomery_g1_kernel(
// Per-job constants (each 8 limbs, LE)
const uint64_t* __restrict__ m, // not used in G1 directly (we pass m_hat)
const uint64_t* __restrict__ n,
const uint64_t n0_inv,
const uint64_t* __restrict__ r2,
const uint64_t* __restrict__ m_hat,
// Per-thread starting state (normal domain)
const uint64_t* __restrict__ y0, // length: num_threads * 8 limbs
// Output buffer for normalized y (for host SHA3 in G1)
uint64_t* __restrict__ y_out, // length: num_threads * iters_per_thread * 8 limbs
// Threading parameters
const uint32_t num_threads,
const uint32_t iters_per_thread
) {
const uint32_t tid = blockIdx.x * blockDim.x + threadIdx.x;
if (tid >= num_threads) {
return;
}
// Local copies of constants (consider placing in __constant__ memory for G2+)
uint64_t n_loc[8], r2_loc[8], mhat_loc[8];
#pragma unroll
for (int i = 0; i < 8; ++i) {
n_loc[i] = n[i];
r2_loc[i] = r2[i];
mhat_loc[i] = m_hat[i];
}
// Load this thread's y0 (normal domain)
uint64_t y0_loc[8];
#pragma unroll
for (int i = 0; i < 8; ++i) {
y0_loc[i] = y0[tid * 8u + i];
}
// Transform to Montgomery domain
uint64_t yhat[8];
to_mont_512(y0_loc, r2_loc, n_loc, n0_inv, yhat);
// Iterate and emit normalized y per step
// Output stride per thread: iters_per_thread * 8 limbs
uint64_t* out_base = y_out + (static_cast<size_t>(tid) * static_cast<size_t>(iters_per_thread) * 8ull);
for (uint32_t iter = 0; iter < iters_per_thread; ++iter) {
// y_hat = y_hat * m_hat
uint64_t yhat_next[8];
mont_mul_512(yhat, mhat_loc, n_loc, n0_inv, yhat_next);
#pragma unroll
for (int i = 0; i < 8; ++i) {
yhat[i] = yhat_next[i];
}
// y = from_mont(y_hat)
uint64_t y_norm[8];
from_mont_512(yhat, n_loc, n0_inv, y_norm);
// Store normalized y (LE limbs) for host SHA3 and distance validation
#pragma unroll
for (int i = 0; i < 8; ++i) {
out_base[iter * 8u + i] = y_norm[i];
}
}
}
// -------------------------------------------------------------------------------------------------
// Host-callable launcher wrapper (optional; typically loaded via PTX and launched from Rust)
// -------------------------------------------------------------------------------------------------
// The Rust host side will load this kernel from PTX and launch it using `cust`/`rustacuda`.
// Example signature in Rust (pseudo):
//
// launch!(module.qpow_montgomery_g1_kernel<<<grid, block, 0, stream>>>(
// d_m.as_device_ptr(),
// d_n.as_device_ptr(),
// n0_inv,
// d_r2.as_device_ptr(),
// d_mhat.as_device_ptr(),
// d_y0.as_device_ptr(),
// d_y_out.as_device_ptr(),
// num_threads,
// iters_per_thread
// ))?;
//
// Note: For G1, the host will compute SHA3-512(y) and distances on the CPU,
// validating correctness against cpu-fast/cpu-montgomery on small ranges.
//
// -------------------------------------------------------------------------------------------------
// G2 additions: device SHA3-512, threshold compare, and early-exit
// -------------------------------------------------------------------------------------------------
// 64-bit rotate-left with defined behavior for all n
__device__ __forceinline__ uint64_t rotl64(uint64_t x, unsigned int n) {
n &= 63u;
return (x << n) | (x >> ((64u - n) & 63u));
}
// Keccak-f[1600] round constants
__device__ __constant__ uint64_t KECCAK_RC[24] = {
0x0000000000000001ULL, 0x0000000000008082ULL,
0x800000000000808aULL, 0x8000000080008000ULL,
0x000000000000808bULL, 0x0000000080000001ULL,
0x8000000080008081ULL, 0x8000000000008009ULL,
0x000000000000008aULL, 0x0000000000000088ULL,
0x0000000080008009ULL, 0x000000008000000aULL,
0x000000008000808bULL, 0x800000000000008bULL,
0x8000000000008089ULL, 0x8000000000008003ULL,
0x8000000000008002ULL, 0x8000000000000080ULL,
0x000000000000800aULL, 0x800000008000000aULL,
0x8000000080008081ULL, 0x8000000000008080ULL,
0x0000000080000001ULL, 0x8000000080008008ULL
};
// Optional per-job constants in constant memory (host may set; kernel remains compatible)
// If C_CONSTS_READY == 1, G2 kernel will prefer these over parameter pointers.
__device__ __constant__ uint64_t C_N[8];
__device__ __constant__ uint64_t C_R2[8];
__device__ __constant__ uint64_t C_MHAT[8];
__device__ __constant__ uint64_t C_N0_INV;
__device__ __constant__ int C_CONSTS_READY;
// Optional constant-memory target/threshold for device compare
__device__ __constant__ uint64_t C_TARGET[8];
__device__ __constant__ uint64_t C_THRESH[8];
// Optional sampler controls/output (host may read these symbols when enabled)
__device__ __constant__ int C_SAMPLER_ENABLE;
__device__ __constant__ uint32_t C_ABI_VERSION = 3u;
__device__ uint8_t C_SAMPLER_Y_BE[64];
__device__ uint8_t C_SAMPLER_H_BE[64];
__device__ uint8_t C_SAMPLER_TARGET_BE[64];
__device__ uint8_t C_SAMPLER_THRESH_BE[64];
__device__ uint32_t C_SAMPLER_INDEX;
__device__ uint32_t C_SAMPLER_DECISION;
__device__ __constant__ int C_DEBUG_FORCE_WIN;
// Load/store helpers (little- and big-endian)
__device__ __forceinline__ uint64_t load64_le(const uint8_t* p) {
return ((uint64_t)p[0]) |
((uint64_t)p[1] << 8) |
((uint64_t)p[2] << 16) |
((uint64_t)p[3] << 24) |
((uint64_t)p[4] << 32) |
((uint64_t)p[5] << 40) |
((uint64_t)p[6] << 48) |
((uint64_t)p[7] << 56);
}
__device__ __forceinline__ void store64_le(uint8_t* p, uint64_t v) {
p[0] = (uint8_t)(v);
p[1] = (uint8_t)(v >> 8);
p[2] = (uint8_t)(v >> 16);
p[3] = (uint8_t)(v >> 24);
p[4] = (uint8_t)(v >> 32);
p[5] = (uint8_t)(v >> 40);
p[6] = (uint8_t)(v >> 48);
p[7] = (uint8_t)(v >> 56);
}
__device__ __forceinline__ void store64_be(uint8_t* p, uint64_t v) {
p[0] = (uint8_t)(v >> 56);
p[1] = (uint8_t)(v >> 48);
p[2] = (uint8_t)(v >> 40);
p[3] = (uint8_t)(v >> 32);
p[4] = (uint8_t)(v >> 24);
p[5] = (uint8_t)(v >> 16);
p[6] = (uint8_t)(v >> 8);
p[7] = (uint8_t)(v);
}
// Convert 8 LE limbs into 64 BE bytes (big-endian numeric representation)
__device__ __forceinline__ void le8_to_be64_bytes(const uint64_t le[8], uint8_t out[64]) {
#pragma unroll
for (int i = 0; i < 8; ++i) {
uint64_t limb = le[7 - i]; // most significant limb first
#pragma unroll
for (int b = 0; b < 8; ++b) {
out[i * 8 + (7 - b)] = (uint8_t)((limb >> (b * 8)) & 0xFF);
}
}
}
// Compare two 64-byte big-endian numbers: return true if a <= b
__device__ __forceinline__ bool be64_leq(const uint8_t a[64], const uint8_t b[64]) {
#pragma unroll
for (int i = 0; i < 64; ++i) {
if (a[i] != b[i]) {
return a[i] < b[i];
}
}
return true; // equal
}
// Keccak-f[1600] permutation (unrolled mapping)
__device__ __forceinline__ void keccak_f1600(uint64_t s[25]) {
#pragma unroll
for (int round = 0; round < 24; ++round) {
// ---- Theta ---------------------------------------------------------
uint64_t Aba=s[0], Aga=s[5], Aka=s[10], Ama=s[15], Asa=s[20];
uint64_t Abe=s[1], Age=s[6], Ake=s[11], Ame=s[16], Ase=s[21];
uint64_t Abi=s[2], Agi=s[7], Aki=s[12], Ami=s[17], Asi=s[22];
uint64_t Abo=s[3], Ago=s[8], Ako=s[13], Amo=s[18], Aso=s[23];
uint64_t Abu=s[4], Agu=s[9], Aku=s[14], Amu=s[19], Asu=s[24];
uint64_t Ca = Aba ^ Aga ^ Aka ^ Ama ^ Asa;
uint64_t Ce = Abe ^ Age ^ Ake ^ Ame ^ Ase;
uint64_t Ci = Abi ^ Agi ^ Aki ^ Ami ^ Asi;
uint64_t Co = Abo ^ Ago ^ Ako ^ Amo ^ Aso;
uint64_t Cu = Abu ^ Agu ^ Aku ^ Amu ^ Asu;
uint64_t Da = rotl64(Ce, 1) ^ Cu;
uint64_t De = rotl64(Ci, 1) ^ Ca;
uint64_t Di = rotl64(Co, 1) ^ Ce;
uint64_t Do = rotl64(Cu, 1) ^ Ci;
uint64_t Du = rotl64(Ca, 1) ^ Co;
Aba ^= Da; Abe ^= De; Abi ^= Di; Abo ^= Do; Abu ^= Du;
Aga ^= Da; Age ^= De; Agi ^= Di; Ago ^= Do; Agu ^= Du;
Aka ^= Da; Ake ^= De; Aki ^= Di; Ako ^= Do; Aku ^= Du;
Ama ^= Da; Ame ^= De; Ami ^= Di; Amo ^= Do; Amu ^= Du;
Asa ^= Da; Ase ^= De; Asi ^= Di; Aso ^= Do; Asu ^= Du;
// ---- Rho + Pi ------------------------------------------------------
uint64_t Bba = Aba;
uint64_t Bbe = rotl64(Age, 44);
uint64_t Bbi = rotl64(Aki, 43);
uint64_t Bbo = rotl64(Amo, 21);
uint64_t Bbu = rotl64(Asu, 14);
uint64_t Bga = rotl64(Abo, 28);
uint64_t Bge = rotl64(Agu, 20);
uint64_t Bgi = rotl64(Aka, 3);
uint64_t Bgo = rotl64(Ame, 45);
uint64_t Bgu = rotl64(Asi, 61);
uint64_t Bka = rotl64(Abe, 1);
uint64_t Bke = rotl64(Agi, 6);
uint64_t Bki = rotl64(Ako, 25);
uint64_t Bko = rotl64(Amu, 8);
uint64_t Bku = rotl64(Asa, 18);
uint64_t Bma = rotl64(Abu, 27);
uint64_t Bme = rotl64(Aga, 36);
uint64_t Bmi = rotl64(Ake, 10);
uint64_t Bmo = rotl64(Ami, 15);
uint64_t Bmu = rotl64(Aso, 56);
uint64_t Bsa = rotl64(Abi, 62);
uint64_t Bse = rotl64(Ago, 55);
uint64_t Bsi = rotl64(Aku, 39);
uint64_t Bso = rotl64(Ama, 41);
uint64_t Bsu = rotl64(Ase, 2);
// ---- Chi -----------------------------------------------------------
Aba = Bba ^ ((~Bbe) & Bbi);
Abe = Bbe ^ ((~Bbi) & Bbo);
Abi = Bbi ^ ((~Bbo) & Bbu);
Abo = Bbo ^ ((~Bbu) & Bba);
Abu = Bbu ^ ((~Bba) & Bbe);
Aga = Bga ^ ((~Bge) & Bgi);
Age = Bge ^ ((~Bgi) & Bgo);
Agi = Bgi ^ ((~Bgo) & Bgu);
Ago = Bgo ^ ((~Bgu) & Bga);
Agu = Bgu ^ ((~Bga) & Bge);
Aka = Bka ^ ((~Bke) & Bki);
Ake = Bke ^ ((~Bki) & Bko);
Aki = Bki ^ ((~Bko) & Bku);
Ako = Bko ^ ((~Bku) & Bka);
Aku = Bku ^ ((~Bka) & Bke);
Ama = Bma ^ ((~Bme) & Bmi);
Ame = Bme ^ ((~Bmi) & Bmo);
Ami = Bmi ^ ((~Bmo) & Bmu);
Amo = Bmo ^ ((~Bmu) & Bma);
Amu = Bmu ^ ((~Bma) & Bme);
Asa = Bsa ^ ((~Bse) & Bsi);
Ase = Bse ^ ((~Bsi) & Bso);
Asi = Bsi ^ ((~Bso) & Bsu);
Aso = Bso ^ ((~Bsu) & Bsa);
Asu = Bsu ^ ((~Bsa) & Bse);
// ---- Iota ----------------------------------------------------------
Aba ^= KECCAK_RC[round];
// Store back
s[0]=Aba; s[5]=Aga; s[10]=Aka; s[15]=Ama; s[20]=Asa;
s[1]=Abe; s[6]=Age; s[11]=Ake; s[16]=Ame; s[21]=Ase;
s[2]=Abi; s[7]=Agi; s[12]=Aki; s[17]=Ami; s[22]=Asi;
s[3]=Abo; s[8]=Ago; s[13]=Ako; s[18]=Amo; s[23]=Aso;
s[4]=Abu; s[9]=Agu; s[14]=Aku; s[19]=Amu; s[24]=Asu;
}
}
// Device SHA3-512 for a single 64-byte message; writes lane-LE bytes to out_le64
// Note: Input is treated as raw message bytes. We absorb them directly into the Keccak rate
// as little-endian 64-bit lanes to mirror the host sha3 crate semantics.
__device__ __forceinline__ void sha3_512_64bytes_le(const uint8_t in_msg_bytes[64], uint8_t out_le64[64]) {
// Initialize state to zero
uint64_t s[25];
#pragma unroll
for (int i = 0; i < 25; ++i) s[i] = 0ull;
// Absorb (rate = 72 bytes). Message is 64 bytes: append 0x06 then pad with zeros and set last of rate |= 0x80
uint8_t block[72];
#pragma unroll
for (int i = 0; i < 72; ++i) block[i] = 0;
#pragma unroll
for (int i = 0; i < 64; ++i) block[i] = in_msg_bytes[i];
block[64] = 0x06;
block[71] ^= 0x80;
// XOR into state lanes as little-endian 64-bit words
#pragma unroll
for (int i = 0; i < 9; ++i) {
s[i] ^= load64_le(&block[i * 8]);
}
// Permute
keccak_f1600(s);
// Squeeze 64 bytes (8 lanes) into little-endian lane bytes
#pragma unroll
for (int i = 0; i < 8; ++i) {
store64_le(&out_le64[i * 8], s[i]);
}
}
// Kernel: G2 — device SHA3-512 + threshold compare + early-exit
extern "C" __global__ void qpow_montgomery_g2_kernel(
// Per-job constants (each 8 limbs, LE)
const uint64_t* __restrict__ m,
const uint64_t* __restrict__ n,
const uint64_t n0_inv,
const uint64_t* __restrict__ r2,
const uint64_t* __restrict__ m_hat,
// Per-thread starting state (normal domain)
const uint64_t* __restrict__ y0, // length: num_threads * 8 limbs
// G2-specific inputs/outputs
const uint8_t* __restrict__ target_be, // 64 bytes
const uint8_t* __restrict__ threshold_be, // 64 bytes
int* __restrict__ found_flag, // 0 -> not found, 1 -> found
uint32_t* __restrict__ out_index, // linear index (t * iters + j)
uint32_t* __restrict__ out_win_tid, // winner thread id (optional)
uint32_t* __restrict__ out_win_j, // winner iteration j (optional)
uint8_t* __restrict__ out_distance_be, // 64 bytes
// Debug output buffers (optional; host may pass nullptrs)
uint8_t* __restrict__ out_dbg_y_be, // 64 bytes (optional)
uint8_t* __restrict__ out_dbg_h_be, // 64 bytes (optional)
// Threading parameters
const uint32_t num_threads,
const uint32_t iters_per_thread,
const uint64_t covered_elems
) {
const uint32_t tid = blockIdx.x * blockDim.x + threadIdx.x;
if (tid >= num_threads) {
return;
}
// Quick early-exit check
if (atomicAdd(found_flag, 0) != 0) {
return;
}
// Local copies of constants (prefer __constant__ if available)
uint64_t n_loc[8], r2_loc[8], mhat_loc[8];
if (C_CONSTS_READY) {
#pragma unroll
for (int i = 0; i < 8; ++i) {
n_loc[i] = C_N[i];
r2_loc[i] = C_R2[i];
mhat_loc[i] = C_MHAT[i];
}
} else {
#pragma unroll
for (int i = 0; i < 8; ++i) {
n_loc[i] = n[i];
r2_loc[i] = r2[i];
mhat_loc[i] = m_hat[i];
}
}
// Load this thread's y0 (normal domain) and move to Montgomery domain
uint64_t y0_loc[8];
#pragma unroll
for (int i = 0; i < 8; ++i) {
y0_loc[i] = y0[tid * 8u + i];
}
const uint64_t n0i = C_CONSTS_READY ? C_N0_INV : n0_inv;
uint64_t yhat[8];
to_mont_512(y0_loc, r2_loc, n_loc, n0i, yhat);
// Prepare target/threshold big-endian bytes (numeric)
uint8_t target_be_bytes[64], thresh_be_bytes[64];
if (C_CONSTS_READY) {
#pragma unroll
for (int i = 0; i < 8; ++i) {
store64_be(&target_be_bytes[i * 8], C_TARGET[i]);
store64_be(&thresh_be_bytes[i * 8], C_THRESH[i]);
}
} else {
#pragma unroll
for (int i = 0; i < 64; ++i) {
target_be_bytes[i] = target_be[i];
thresh_be_bytes[i] = threshold_be[i];
}
}
// Bound per-thread work to the assigned coverage to avoid hashing beyond the range
uint64_t thread_base = (uint64_t)tid * (uint64_t)iters_per_thread;
if (thread_base >= covered_elems) {
return;
}
uint64_t remain_for_thread = covered_elems - thread_base;
uint32_t iters = iters_per_thread;
if ((uint64_t)iters > remain_for_thread) {
iters = (uint32_t)remain_for_thread;
}
// Iterate and check threshold
for (uint32_t j = 0; j < iters; ++j) {
// Respect early-exit
if (atomicAdd(found_flag, 0) != 0) {
return;
}
// Debug: force a winner to exercise GPU->CPU signaling
if (C_DEBUG_FORCE_WIN != 0 && tid == 0 && j == 0) {
if (atomicCAS(found_flag, 0, 1) == 0) {
if (out_index) {
*out_index = tid * iters_per_thread + j;
}
if (out_win_tid) {
*out_win_tid = tid;
}
if (out_win_j) {
*out_win_j = j;
}
if (out_distance_be) {
#pragma unroll
for (int i = 0; i < 64; ++i) {
out_distance_be[i] = 0;
}
}
if (out_dbg_y_be) {
#pragma unroll
for (int i = 0; i < 64; ++i) {
out_dbg_y_be[i] = 0;
}
}
if (out_dbg_h_be) {
#pragma unroll
for (int i = 0; i < 64; ++i) {
out_dbg_h_be[i] = 0;
}
}
if (C_SAMPLER_ENABLE && tid == 0) {
C_SAMPLER_INDEX = tid * iters_per_thread + j;
C_SAMPLER_DECISION = 1u;
}
}
return;
}
// y_hat = y_hat * m_hat
uint64_t yhat_next[8];
mont_mul_512(yhat, mhat_loc, n_loc, n0i, yhat_next);
#pragma unroll
for (int i = 0; i < 8; ++i) {
yhat[i] = yhat_next[i];
}
// y = from_mont(y_hat)
uint64_t y_norm[8];
from_mont_512(yhat, n_loc, n0i, y_norm);
// y_be64 (64 bytes) from LE limbs
uint8_t y_be[64];
le8_to_be64_bytes(y_norm, y_be);
// H = SHA3-512(y_be) -> produce lane-LE bytes
uint8_t h_le[64];
sha3_512_64bytes_le(y_be, h_le);
// Use SHA3 output bytes directly to match host pow-core digest semantics
uint8_t digest_be[64];
#pragma unroll
for (int i = 0; i < 64; ++i) {
digest_be[i] = h_le[i];
}
// distance = target_be XOR digest_be (bytewise, big-endian order)
uint8_t dist_be[64];
#pragma unroll
for (int i = 0; i < 64; ++i) {
dist_be[i] = target_be_bytes[i] ^ digest_be[i];
}
// Compare distance <= threshold (lexicographic on big-endian bytes)
bool decision = be64_leq(dist_be, thresh_be_bytes);
// Optional sampler (first thread/iter): capture y/H/target/thresh for parity
if (C_SAMPLER_ENABLE && tid == 0 && j == 0) {
#pragma unroll
for (int i = 0; i < 64; ++i) {
C_SAMPLER_Y_BE[i] = y_be[i];
C_SAMPLER_H_BE[i] = digest_be[i];
C_SAMPLER_TARGET_BE[i] = target_be_bytes[i];
C_SAMPLER_THRESH_BE[i] = thresh_be_bytes[i];
}
C_SAMPLER_INDEX = tid * iters_per_thread + j;
C_SAMPLER_DECISION = decision ? 1u : 0u;
}
if (decision) {
// Try to claim the flag
if (atomicCAS(found_flag, 0, 1) == 0) {
// Write linear index for host to reconstruct nonce
if (out_index) {
*out_index = tid * iters_per_thread + j;
}
// Record winner thread and iteration for host-side nonce reconstruction
if (out_win_tid) {
*out_win_tid = tid;
}
if (out_win_j) {
*out_win_j = j;
}
// Write distance and debug buffers (if provided)
if (out_distance_be) {
#pragma unroll
for (int i = 0; i < 64; ++i) {
out_distance_be[i] = dist_be[i];
}
}
if (out_dbg_y_be) {
#pragma unroll
for (int i = 0; i < 64; ++i) {
out_dbg_y_be[i] = y_be[i];
}
}
if (out_dbg_h_be) {
#pragma unroll
for (int i = 0; i < 64; ++i) {
out_dbg_h_be[i] = digest_be[i];
}
}
}
return; // early-exit after claiming (or if already claimed)
}
}
}
} // extern "C"

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +0,0 @@
[package]
name = "engine-gpu-opencl"
version.workspace = true
edition.workspace = true
publish = false
description = "OpenCL-based GPU mining engine for the Quantus External Miner (placeholder crate)"
[lib]
path = "src/lib.rs"
crate-type = ["rlib"]
[features]
default = []
# Enable OpenCL integrations when implementing the engine.
opencl = ["dep:ocl"]
[dependencies]
pow-core = { path = "../pow-core" }
primitive-types = { workspace = true }
log = { workspace = true }
thiserror = { workspace = true }
anyhow = { workspace = true }
# OpenCL ecosystem (optional for now; will be used when implementing GPU backend)
ocl = { version = "0.19", optional = true }

View File

@@ -1,101 +0,0 @@
// #![deny(rust_2018_idioms)]
// #![forbid(unsafe_code)]
// //! OpenCL-based GPU mining engine (placeholder)
// //!
// //! This crate is a scaffold for a future OpenCL backend that will implement the
// //! mining engine interface used by the service layer. It currently provides:
// //! - An `OpenClEngine` type with a constructor and basic helpers.
// //! - Documentation of the intended integration points.
// //!
// //! Planned responsibilities (non-exhaustive):
// //! - Accept a prepared `JobContext` (from `pow-core`) per job.
// //! - Partition nonce ranges into GPU work assignments.
// //! - Run an OpenCL kernel that performs, per nonce in the range:
// //! - y <- y * m (mod n) using Montgomery multiplication (in Montgomery domain)
// //! - nonce_element <- SHA3_512(y) in the normal domain
// //! - distance <- target XOR nonce_element
// //! - if distance <= threshold: report solution and signal early-cancel
// //! - Coordinate early-exit via device-global flags and host polling.
// //!
// //! Notes:
// //! - This crate deliberately does NOT implement the `MinerEngine` trait yet,
// //! because the engine trait currently lives in `engine-cpu`. Once the trait
// //! is promoted to a shared crate (or re-exported for engines), this crate
// //! will implement it and become selectable at runtime via the service config.
// //! - OpenCL bindings (e.g., via the `ocl` crate) and kernels will be added
// //! behind feature flags (e.g., `opencl`). For now, we only offer placeholders
// //! so the workspace compiles cleanly and the integration points are clear.
// use pow_core::JobContext;
// use primitive_types::U512;
// /// Placeholder type for the OpenCL engine.
// ///
// /// When fully implemented, this engine will manage OpenCL platform/device
// /// discovery, context/queue creation, kernel compilation, memory transfers,
// /// and kernel launches. It will expose the same search-range semantics as
// /// the CPU engine(s) but backed by the GPU.
// #[derive(Default, Debug)]
// pub struct OpenClEngine {
// // Future fields (examples):
// // platform_id: usize,
// // device_id: usize,
// // context: ocl::Context,
// // queue: ocl::Queue,
// // program: ocl::Program,
// // kernel: ocl::Kernel,
// }
// impl OpenClEngine {
// /// Construct a new OpenCL engine placeholder.
// ///
// /// Future versions may accept configuration (e.g., platform/device index).
// pub fn new() -> Self {
// Self::default()
// }
// /// Human-readable name for logs/metrics.
// pub fn name(&self) -> &'static str {
// "gpu-opencl (placeholder)"
// }
// /// Prepare a precomputed job context for a given header and threshold.
// ///
// /// This defers to `pow-core` to derive (m, n) and `target` from the header.
// /// In a full OpenCL implementation, this context will be uploaded to device
// /// constant buffers or passed as kernel arguments.
// pub fn prepare_context(&self, header_hash: [u8; 32], threshold: U512) -> JobContext {
// JobContext::new(header_hash, threshold)
// }
// /// Returns whether this build has OpenCL support compiled in.
// ///
// /// When actual OpenCL integration is added behind a feature flag, this will
// /// return true only if that feature is enabled.
// pub fn opencl_available(&self) -> bool {
// // Adjust once actual OpenCL integration is implemented behind a feature:
// // cfg!(feature = "opencl")
// false
// }
// }
// #[cfg(test)]
// mod tests {
// use super::*;
// use primitive_types::U512;
// #[test]
// fn placeholder_engine_basics() {
// let eng = OpenClEngine::new();
// assert_eq!(eng.name(), "gpu-opencl (placeholder)");
// // Ensure context creation works and is deterministic in shape.
// let header = [1u8; 32];
// let threshold = U512::from(12345u64);
// let ctx = eng.prepare_context(header, threshold);
// assert_eq!(ctx.header, header);
// assert_eq!(ctx.threshold, threshold);
// }
// }

View File

@@ -0,0 +1,49 @@
[package]
name = "engine-gpu"
version.workspace = true
edition.workspace = true
publish = false
description = "GPU mining engine implementation for the Quantus External Miner"
# [lib]
# path = "src/main.rs"
# crate-type = ["rlib"]
[features]
# Default to the baseline/reference implementation; enable others as needed.
default = ["baseline", "metrics"]
# Map engine features (pow-core dependency removed for now)
baseline = []
simd-poseidon2 = []
metrics = ["dep:metrics"]
[dependencies]
engine-cpu = { path = "../engine-cpu" }
pow-core = { path = "../pow-core" }
metrics = { path = "../metrics", optional = true }
primitive-types = { workspace = true }
log = { workspace = true }
thiserror = { workspace = true }
anyhow = { workspace = true }
qp-poseidon-core = { version = "1.0.2", features = ["p2"] }
qp-poseidon-constants = { version = "1.0.2" }
qp-plonky2 = { version = "1.1.1" }
qp-plonky2-field = { version = "1.1.1" }
wgpu = { version = "27.0.1" } # GPU compute library
futures = "0.3" # For async executor
bytemuck = "1.16" # For buffer mapping
rand = { workspace = true, features = ["std", "std_rng"] }
rand_chacha = "0.3" # For deterministic random test generation
[dev-dependencies]
hex = { workspace = true }
criterion = "0.5"
rand = { workspace = true, features = ["std", "std_rng"] }
env_logger.workspace = true
[[bench]]
name = "gpu_engine_bench"
harness = false

View File

@@ -0,0 +1,343 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use engine_cpu::{FastCpuEngine, MinerEngine, Range};
use engine_gpu::GpuEngine;
use pow_core::JobContext;
use primitive_types::U512;
use rand::RngCore;
use std::sync::atomic::AtomicBool;
fn bench_cpu_vs_gpu_small(c: &mut Criterion) {
let cpu_engine = FastCpuEngine::new();
let gpu_engine = GpuEngine::new();
let cancel_flag = AtomicBool::new(false);
// Small range: 10K nonces - reasonable for benchmarking
let small_range = Range {
start: U512::from(0u64),
end: U512::from(10_000u64),
};
let mut group = c.benchmark_group("small_range_10k");
group.sample_size(10);
group.measurement_time(std::time::Duration::from_secs(3));
group.bench_function("cpu", |b| {
b.iter(|| {
let mut header = [0u8; 32];
rand::thread_rng().fill_bytes(&mut header);
let difficulty = U512::from(u64::MAX); // High difficulty - no solutions expected
let ctx = JobContext::new(header, difficulty);
let result = cpu_engine.search_range(
black_box(&ctx),
black_box(small_range.clone()),
black_box(&cancel_flag),
);
black_box(result)
})
});
group.bench_function("gpu", |b| {
b.iter(|| {
let mut header = [0u8; 32];
rand::thread_rng().fill_bytes(&mut header);
let difficulty = U512::from(u64::MAX); // High difficulty - no solutions expected
let ctx = JobContext::new(header, difficulty);
let result = gpu_engine.search_range(
black_box(&ctx),
black_box(small_range.clone()),
black_box(&cancel_flag),
);
black_box(result)
})
});
group.finish();
}
fn bench_cpu_vs_gpu_medium(c: &mut Criterion) {
let cpu_engine = FastCpuEngine::new();
let gpu_engine = GpuEngine::new();
let cancel_flag = AtomicBool::new(false);
// Medium range: 100K nonces
let medium_range = Range {
start: U512::from(0u64),
end: U512::from(100_000u64),
};
let mut group = c.benchmark_group("medium_range_100k");
group.sample_size(10);
group.measurement_time(std::time::Duration::from_secs(3));
group.bench_function("cpu", |b| {
b.iter(|| {
let mut header = [0u8; 32];
rand::thread_rng().fill_bytes(&mut header);
let difficulty = U512::from(u64::MAX); // High difficulty - no solutions expected
let ctx = JobContext::new(header, difficulty);
let result = cpu_engine.search_range(
black_box(&ctx),
black_box(medium_range.clone()),
black_box(&cancel_flag),
);
black_box(result)
})
});
group.bench_function("gpu", |b| {
b.iter(|| {
let mut header = [0u8; 32];
rand::thread_rng().fill_bytes(&mut header);
let difficulty = U512::from(u64::MAX); // High difficulty - no solutions expected
let ctx = JobContext::new(header, difficulty);
let result = gpu_engine.search_range(
black_box(&ctx),
black_box(medium_range.clone()),
black_box(&cancel_flag),
);
black_box(result)
})
});
group.finish();
}
fn bench_cpu_vs_gpu_large(c: &mut Criterion) {
let cpu_engine = FastCpuEngine::new();
let gpu_engine = GpuEngine::new();
let cancel_flag = AtomicBool::new(false);
// Large range: 1M nonces - where GPU should really shine
let large_range = Range {
start: U512::from(0u64),
end: U512::from(1_000_000u64),
};
let mut group = c.benchmark_group("large_range_1m");
group.sample_size(10);
group.measurement_time(std::time::Duration::from_secs(5));
group.bench_function("cpu", |b| {
b.iter(|| {
let mut header = [0u8; 32];
rand::thread_rng().fill_bytes(&mut header);
let difficulty = U512::from(u64::MAX); // High difficulty - no solutions expected
let ctx = JobContext::new(header, difficulty);
let result = cpu_engine.search_range(
black_box(&ctx),
black_box(large_range.clone()),
black_box(&cancel_flag),
);
black_box(result)
})
});
group.bench_function("gpu", |b| {
b.iter(|| {
let mut header = [0u8; 32];
rand::thread_rng().fill_bytes(&mut header);
let difficulty = U512::from(u64::MAX); // High difficulty - no solutions expected
let ctx = JobContext::new(header, difficulty);
let result = gpu_engine.search_range(
black_box(&ctx),
black_box(large_range.clone()),
black_box(&cancel_flag),
);
black_box(result)
})
});
group.finish();
}
fn bench_solution_finding(c: &mut Criterion) {
let cpu_engine = FastCpuEngine::new();
let gpu_engine = GpuEngine::new();
let cancel_flag = AtomicBool::new(false);
// Range where we expect to find solutions quickly
let solution_range = Range {
start: U512::from(0u64),
end: U512::from(50_000u64),
};
let mut group = c.benchmark_group("solution_finding");
group.sample_size(10);
group.measurement_time(std::time::Duration::from_secs(3));
group.bench_function("cpu_find_solution", |b| {
b.iter(|| {
let mut header = [0u8; 32];
rand::thread_rng().fill_bytes(&mut header);
let difficulty = U512::from(10_000u64); // Easy difficulty - should find solution
let ctx = JobContext::new(header, difficulty);
let result = cpu_engine.search_range(
black_box(&ctx),
black_box(solution_range.clone()),
black_box(&cancel_flag),
);
black_box(result)
})
});
group.bench_function("gpu_find_solution", |b| {
b.iter(|| {
let mut header = [0u8; 32];
rand::thread_rng().fill_bytes(&mut header);
let difficulty = U512::from(10_000u64); // Easy difficulty - should find solution
let ctx = JobContext::new(header, difficulty);
let result = gpu_engine.search_range(
black_box(&ctx),
black_box(solution_range.clone()),
black_box(&cancel_flag),
);
black_box(result)
})
});
group.finish();
}
fn bench_throughput_per_second(c: &mut Criterion) {
let cpu_engine = FastCpuEngine::new();
let gpu_engine = GpuEngine::new();
let cancel_flag = AtomicBool::new(false);
// Fixed time benchmark - see how many hashes we can do in 1 second
let throughput_range = Range {
start: U512::from(0u64),
end: U512::from(10_000_000u64), // 10M nonce range
};
let mut group = c.benchmark_group("throughput_comparison");
group.sample_size(10);
group.measurement_time(std::time::Duration::from_secs(5));
group.bench_function("cpu_throughput", |b| {
b.iter(|| {
let mut header = [0u8; 32];
rand::thread_rng().fill_bytes(&mut header);
let difficulty = U512::from(u64::MAX); // High difficulty - no solutions expected
let ctx = JobContext::new(header, difficulty);
let result = cpu_engine.search_range(
black_box(&ctx),
black_box(throughput_range.clone()),
black_box(&cancel_flag),
);
black_box(result)
})
});
group.bench_function("gpu_throughput", |b| {
b.iter(|| {
let mut header = [0u8; 32];
rand::thread_rng().fill_bytes(&mut header);
let difficulty = U512::from(u64::MAX); // High difficulty - no solutions expected
let ctx = JobContext::new(header, difficulty);
let result = gpu_engine.search_range(
black_box(&ctx),
black_box(throughput_range.clone()),
black_box(&cancel_flag),
);
black_box(result)
})
});
group.finish();
}
fn bench_gpu_batch_efficiency(c: &mut Criterion) {
let gpu_engine = GpuEngine::new();
let cancel_flag = AtomicBool::new(false);
let mut group = c.benchmark_group("gpu_batch_sizes");
group.sample_size(10);
group.measurement_time(std::time::Duration::from_secs(3));
// Test different batch sizes to see GPU efficiency
let small_batch = Range {
start: U512::from(0u64),
end: U512::from(1_000u64), // 1K nonces - very small for GPU
};
let medium_batch = Range {
start: U512::from(0u64),
end: U512::from(50_000u64), // 50K nonces - medium
};
let large_batch = Range {
start: U512::from(0u64),
end: U512::from(500_000u64), // 500K nonces - large
};
group.bench_function("gpu_1k_batch", |b| {
b.iter(|| {
let mut header = [0u8; 32];
rand::thread_rng().fill_bytes(&mut header);
let difficulty = U512::from(u64::MAX);
let ctx = JobContext::new(header, difficulty);
let result = gpu_engine.search_range(
black_box(&ctx),
black_box(small_batch.clone()),
black_box(&cancel_flag),
);
black_box(result)
})
});
group.bench_function("gpu_50k_batch", |b| {
b.iter(|| {
let mut header = [0u8; 32];
rand::thread_rng().fill_bytes(&mut header);
let difficulty = U512::from(u64::MAX);
let ctx = JobContext::new(header, difficulty);
let result = gpu_engine.search_range(
black_box(&ctx),
black_box(medium_batch.clone()),
black_box(&cancel_flag),
);
black_box(result)
})
});
group.bench_function("gpu_500k_batch", |b| {
b.iter(|| {
let mut header = [0u8; 32];
rand::thread_rng().fill_bytes(&mut header);
let difficulty = U512::from(u64::MAX);
let ctx = JobContext::new(header, difficulty);
let result = gpu_engine.search_range(
black_box(&ctx),
black_box(large_batch.clone()),
black_box(&cancel_flag),
);
black_box(result)
})
});
group.finish();
}
criterion_group!(
benches,
bench_cpu_vs_gpu_small,
bench_cpu_vs_gpu_medium,
bench_cpu_vs_gpu_large,
bench_solution_finding,
bench_throughput_per_second,
bench_gpu_batch_efficiency
);
criterion_main!(benches);

View File

@@ -0,0 +1,62 @@
use engine_cpu::{BaselineCpuEngine, EngineStatus, MinerEngine, Range};
use engine_gpu::GpuEngine;
use primitive_types::U512;
use std::sync::atomic::AtomicBool;
fn main() {
// Initialize logging
env_logger::builder()
.filter_level(log::LevelFilter::Info)
.parse_default_env()
.init();
log::info!("Starting verify_nonce example");
// 1. Setup Context
// Use a fixed header and easy difficulty (1) so any nonce is valid
let header = [1u8; 32];
let difficulty = U512::from(u64::MAX); // High difficulty - no solutions expected
let cpu_engine = BaselineCpuEngine::new();
let ctx = cpu_engine.prepare_context(header, difficulty);
log::info!("Context prepared. Difficulty: {}", difficulty);
let cancel = AtomicBool::new(false);
// 3. Verify with GPU engine
log::info!("Initializing GPU engine...");
let gpu_engine = GpuEngine::new();
// Search a small range around the valid nonce
let gpu_range = Range {
start: U512::from(0u64),
end: U512::from(1_000_000u64), // Search 1,000,000 nonces
};
log::info!(
"Searching for nonce with GPU engine in range {} - {}",
gpu_range.start,
gpu_range.end
);
let start = std::time::Instant::now();
let gpu_result = gpu_engine.search_range(&ctx, gpu_range, &cancel);
let elapsed = start.elapsed();
log::info!("GPU search took {:?}", elapsed);
match gpu_result {
EngineStatus::Found { candidate, .. } => {
log::info!("GPU found nonce: {}", candidate.nonce);
log::info!("GPU hash: {:x}", candidate.hash);
}
EngineStatus::Exhausted { .. } => {
log::info!("GPU exhausted range (expected)");
}
EngineStatus::Cancelled { .. } => {
log::error!("FAILURE: GPU search cancelled!");
}
EngineStatus::Running { .. } => {
log::error!("FAILURE: GPU returned Running status!");
}
}
}

View File

@@ -0,0 +1,218 @@
use pow_core::{hash_from_nonce, JobContext};
use primitive_types::U512;
use rand::Rng;
use rand::SeedableRng;
use wgpu::util::DeviceExt;
pub async fn test_end_to_end_mining(
device: &wgpu::Device,
queue: &wgpu::Queue,
) -> Result<(), Box<dyn std::error::Error>> {
println!("Running End-to-End Mining Test...");
// 1. Setup a job context
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(12345);
let mut header = [0u8; 32];
rng.fill(&mut header);
// Use difficulty 1 so target is MAX. Any hash should pass.
let difficulty = U512::from(1u64);
let ctx = JobContext::new(header, difficulty);
// 2. Pick a nonce
let nonce_val = U512::from(123456789u64);
// 3. Compute expected hash using CPU (pow-core)
let expected_hash = hash_from_nonce(&ctx, nonce_val);
println!("CPU Expected Hash: {:x}", expected_hash);
// 4. Run GPU Mining for this specific nonce
// Header Buffer
let mut header_u32s = [0u32; 8];
for (i, item) in header_u32s.iter_mut().enumerate() {
let chunk = &ctx.header[i * 4..(i + 1) * 4];
*item = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
}
let header_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Header Buffer"),
contents: bytemuck::cast_slice(&header_u32s),
usage: wgpu::BufferUsages::STORAGE,
});
// Target Buffer
let target_bytes = ctx.target.to_little_endian();
let mut target_u32s = [0u32; 16];
for i in 0..16 {
let chunk = &target_bytes[i * 4..(i + 1) * 4];
target_u32s[i] = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
}
let target_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Target Buffer"),
contents: bytemuck::cast_slice(&target_u32s),
usage: wgpu::BufferUsages::STORAGE,
});
// Start Nonce Buffer
// We set start_nonce = nonce_val. Thread 0 will check start_nonce + 0 = nonce_val.
let start_nonce_bytes = nonce_val.to_little_endian();
let mut start_nonce_u32s = [0u32; 16];
for i in 0..16 {
let chunk = &start_nonce_bytes[i * 4..(i + 1) * 4];
start_nonce_u32s[i] = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
}
let start_nonce_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Start Nonce Buffer"),
contents: bytemuck::cast_slice(&start_nonce_u32s),
usage: wgpu::BufferUsages::STORAGE,
});
// Results Buffer
let results_size = (1 + 16 + 16) * 4;
let results_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Results Buffer"),
size: results_size as u64,
usage: wgpu::BufferUsages::STORAGE
| wgpu::BufferUsages::COPY_SRC
| wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
// Clear results buffer
let zeros = vec![0u8; results_size];
queue.write_buffer(&results_buffer, 0, &zeros);
// Dispatch config buffer: [total_threads, nonces_per_thread, work_per_batch, threads_per_workgroup]
let dispatch_config_data: [u32; 4] = [256, 1, 1, 256];
let dispatch_config_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Dispatch Config Buffer"),
size: 16,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
queue.write_buffer(
&dispatch_config_buffer,
0,
bytemuck::cast_slice(&dispatch_config_data),
);
// Load Shader
let shader_source = include_str!("mining.wgsl");
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Mining Shader"),
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
});
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("Mining Pipeline"),
layout: None,
module: &shader,
entry_point: Some("mining_main"),
compilation_options: Default::default(),
cache: None,
});
let bind_group_layout = pipeline.get_bind_group_layout(0);
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Mining Bind Group"),
layout: &bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: results_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: header_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: start_nonce_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: target_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 4,
resource: dispatch_config_buffer.as_entire_binding(),
},
],
});
// Run Compute Pass
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Mining Encoder"),
});
{
let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("Mining Compute Pass"),
timestamp_writes: None,
});
cpass.set_pipeline(&pipeline);
cpass.set_bind_group(0, &bind_group, &[]);
cpass.dispatch_workgroups(1, 1, 1); // 1 workgroup, 256 threads. Thread 0 will check nonce_val.
}
// Read Results
let staging_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Staging Buffer"),
size: results_size as u64,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
encoder.copy_buffer_to_buffer(&results_buffer, 0, &staging_buffer, 0, results_size as u64);
queue.submit(Some(encoder.finish()));
let buffer_slice = staging_buffer.slice(..);
let (sender, receiver) = futures::channel::oneshot::channel();
buffer_slice.map_async(wgpu::MapMode::Read, move |v| sender.send(v).unwrap());
device
.poll(wgpu::PollType::Wait {
submission_index: None,
timeout: None,
})
.unwrap();
if let Ok(Ok(())) = receiver.await {
let data = buffer_slice.get_mapped_range();
let result_u32s: &[u32] = bytemuck::cast_slice(&data);
if result_u32s[0] != 0 {
println!("GPU found solution!");
// Parse nonce
let mut nonce_bytes = [0u8; 64];
for i in 0..16 {
let bytes = result_u32s[1 + i].to_le_bytes();
nonce_bytes[i * 4..(i + 1) * 4].copy_from_slice(&bytes);
}
let found_nonce = U512::from_little_endian(&nonce_bytes);
println!("GPU Nonce: {}", found_nonce);
assert_eq!(found_nonce, nonce_val, "Nonce mismatch!");
// Parse hash
let mut hash_bytes = [0u8; 64];
for i in 0..16 {
let bytes = result_u32s[17 + i].to_le_bytes();
hash_bytes[i * 4..(i + 1) * 4].copy_from_slice(&bytes);
}
let found_hash = U512::from_little_endian(&hash_bytes);
println!("GPU Hash: {:x}", found_hash);
assert_eq!(found_hash, expected_hash, "Hash mismatch!");
println!("✅ End-to-End Test Passed!");
} else {
println!("❌ GPU did not find solution (should have passed with MAX target)");
return Err("GPU did not find solution".into());
}
} else {
return Err("Failed to map buffer".into());
}
Ok(())
}

View File

@@ -0,0 +1,619 @@
#![deny(rust_2018_idioms)]
#![forbid(unsafe_code)]
use engine_cpu::{Candidate, EngineStatus, FoundOrigin, MinerEngine, Range};
use futures::executor::block_on;
use pow_core::JobContext;
use primitive_types::U512;
use std::cell::RefCell;
use std::sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc,
};
/// Represents a single GPU device context.
struct GpuContext {
device: wgpu::Device,
queue: wgpu::Queue,
pipeline: wgpu::ComputePipeline,
bind_group: wgpu::BindGroup,
// Cached vendor configuration
optimal_workgroups: u32,
// Reusable buffers
header_buffer: wgpu::Buffer,
target_buffer: wgpu::Buffer,
start_nonce_buffer: wgpu::Buffer,
results_buffer: wgpu::Buffer,
dispatch_config_buffer: wgpu::Buffer,
staging_buffer: wgpu::Buffer,
}
pub struct GpuEngine {
contexts: Vec<Arc<GpuContext>>,
device_counter: AtomicUsize,
}
// Thread-local storage for consistent GPU device assignment per worker thread
thread_local! {
static ASSIGNED_GPU_DEVICE: RefCell<Option<usize>> = const { RefCell::new(None) };
}
impl Default for GpuEngine {
fn default() -> Self {
Self::new()
}
}
impl GpuEngine {
pub fn new() -> Self {
block_on(Self::init()).expect("Failed to initialize GPU engine")
}
async fn init() -> Result<Self, Box<dyn std::error::Error>> {
log::info!("Initializing WGPU...");
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::PRIMARY,
..Default::default()
});
let adapters = instance.enumerate_adapters(wgpu::Backends::PRIMARY);
// Collect adapters to a vector to check count and iterate with index
let adapters: Vec<_> = adapters.into_iter().collect();
if adapters.is_empty() {
log::error!("No suitable GPU adapters found.");
return Err("No suitable GPU adapters found".into());
}
let mut contexts = Vec::new();
let mut adapter_infos = Vec::new();
for (i, adapter) in adapters.into_iter().enumerate() {
let info = adapter.get_info();
log::info!(
"Initializing GPU adapter {}: {} (Backend: {:?})",
i,
info.name,
info.backend
);
log::info!(target: "gpu_engine", "Adapter {} detailed info:", i);
log::info!(target: "gpu_engine", " Name: {}", info.name);
log::info!(target: "gpu_engine", " Vendor: {}", info.vendor);
log::info!(target: "gpu_engine", " Device: {}", info.device);
log::info!(target: "gpu_engine", " Device Type: {:?}", info.device_type);
log::info!(target: "gpu_engine", " Driver: {}", info.driver);
log::info!(target: "gpu_engine", " Driver Info: {}", info.driver_info);
log::info!(target: "gpu_engine", " Backend: {:?}", info.backend);
log::debug!(target: "gpu_engine", "Adapter {} full info: {:?}", i, info);
adapter_infos.push(info.clone());
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("Mining Device"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
memory_hints: Default::default(),
..Default::default()
})
.await?;
log::debug!(target: "gpu_engine", "Device and Queue requested for adapter {}", i);
log::info!(target: "gpu_engine", "Device limits for adapter {}:", i);
let limits = device.limits();
log::info!(target: "gpu_engine", " Max workgroups per dimension: {}", limits.max_compute_workgroups_per_dimension);
log::info!(target: "gpu_engine", " Max workgroup size X: {}", limits.max_compute_workgroup_size_x);
log::info!(target: "gpu_engine", " Max workgroup size Y: {}", limits.max_compute_workgroup_size_y);
log::info!(target: "gpu_engine", " Max workgroup size Z: {}", limits.max_compute_workgroup_size_z);
log::info!(target: "gpu_engine", " Max compute invocations per workgroup: {}", limits.max_compute_invocations_per_workgroup);
log::info!(target: "gpu_engine", " Max buffer size: {}", limits.max_buffer_size);
let shader_source = include_str!("mining.wgsl");
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Mining Shader"),
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
});
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("Mining Pipeline"),
layout: None,
module: &shader,
entry_point: Some("mining_main"),
compilation_options: Default::default(),
cache: None,
});
let bind_group_layout = pipeline.get_bind_group_layout(0);
// Pre-allocate buffers
// Header: 8 u32s
let header_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Header Buffer"),
size: 32,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
// Target: 16 u32s
let target_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Target Buffer"),
size: 64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
// Start Nonce: 16 u32s
let start_nonce_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Start Nonce Buffer"),
size: 64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
// Results: [flag (1), nonce (16), hash (16)] = 33 u32s
let results_size = (1 + 16 + 16) * 4;
let results_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Results Buffer"),
size: results_size,
usage: wgpu::BufferUsages::STORAGE
| wgpu::BufferUsages::COPY_SRC
| wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
// Dispatch config: [total_threads, nonces_per_thread, workgroups, threads_per_workgroup] = 4 u32s
let dispatch_config_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Dispatch Config Buffer"),
size: 16,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let staging_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Staging Buffer"),
size: results_size,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Mining Bind Group"),
layout: &bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: results_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: header_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: start_nonce_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: target_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 4,
resource: dispatch_config_buffer.as_entire_binding(),
},
],
});
log::debug!(target: "gpu_engine", "Buffers and bind group initialized for adapter {}", i);
// Calculate vendor-specific configuration once during initialization
let optimal_workgroups = Self::get_vendor_specific_dispatch(&info, &device);
contexts.push(Arc::new(GpuContext {
device,
queue,
pipeline,
bind_group,
optimal_workgroups,
header_buffer,
target_buffer,
start_nonce_buffer,
results_buffer,
dispatch_config_buffer,
staging_buffer,
}));
}
log::info!("GPU engine initialized with {} devices", contexts.len());
// Set engine backend info for metrics
#[cfg(feature = "metrics")]
{
metrics::set_gpu_device_count(contexts.len() as i64);
for (i, adapter_info) in adapter_infos.iter().enumerate() {
let device_id = format!("gpu-{}", i);
let backend_str = format!("{:?}", adapter_info.backend);
let vendor_str = format!("{}", adapter_info.vendor);
let device_type_str = format!("{:?}", adapter_info.device_type);
let clean_name = adapter_info.name.replace(" ", "_").replace(",", "");
// Set general engine backend info
metrics::set_engine_backend(&device_id, &backend_str);
// Set detailed GPU device info
metrics::set_gpu_device_info(
&device_id,
&clean_name,
&backend_str,
&vendor_str,
&device_type_str,
);
// Log GPU device info for monitoring
log::info!(
"📊 GPU Device {}: {} | Backend: {:?} | Vendor: {} | Device Type: {:?}",
i,
adapter_info.name,
adapter_info.backend,
adapter_info.vendor,
adapter_info.device_type
);
}
}
Ok(Self {
contexts,
device_counter: AtomicUsize::new(0),
})
}
/// Returns the number of GPU devices available
pub fn device_count(&self) -> usize {
self.contexts.len()
}
}
impl MinerEngine for GpuEngine {
fn name(&self) -> &'static str {
"gpu-wgpu"
}
fn prepare_context(&self, header_hash: [u8; 32], difficulty: U512) -> JobContext {
JobContext::new(header_hash, difficulty)
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn search_range(&self, ctx: &JobContext, range: Range, cancel: &AtomicBool) -> EngineStatus {
if self.contexts.is_empty() {
log::warn!("No GPUs available for search.");
return EngineStatus::Exhausted { hash_count: 0 };
}
// Empty or inverted range: nothing to do.
if range.start > range.end {
return EngineStatus::Exhausted { hash_count: 0 };
}
// Use thread-local assignment for consistent worker-to-GPU mapping
let device_index = ASSIGNED_GPU_DEVICE.with(|assigned| {
let mut assigned_ref = assigned.borrow_mut();
if let Some(index) = *assigned_ref {
// This thread already has a GPU assigned
index
} else {
// First time this thread is calling search_range, assign a GPU device
let index = if self.contexts.len() == 1 {
0
} else {
self.device_counter.fetch_add(1, Ordering::SeqCst) % self.contexts.len()
};
*assigned_ref = Some(index);
log::info!(
"Worker thread assigned to GPU device {} (of {} total devices)",
index,
self.contexts.len()
);
index
}
});
let gpu_ctx = &self.contexts[device_index];
log::debug!(
"GPU device {} processing range {}..={} (inclusive)",
device_index,
range.start,
range.end
);
// Pre-convert header and target once (not per range)
let mut header_u32s = [0u32; 8];
for (i, item) in header_u32s.iter_mut().enumerate() {
let chunk = &ctx.header[i * 4..(i + 1) * 4];
*item = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
}
gpu_ctx.queue.write_buffer(
&gpu_ctx.header_buffer,
0,
bytemuck::cast_slice(&header_u32s),
);
let target_bytes = ctx.target.to_little_endian();
let mut target_u32s = [0u32; 16];
for i in 0..16 {
let chunk = &target_bytes[i * 4..(i + 1) * 4];
target_u32s[i] = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
}
gpu_ctx.queue.write_buffer(
&gpu_ctx.target_buffer,
0,
bytemuck::cast_slice(&target_u32s),
);
// Length of the inclusive nonce range
let total_range_size = (range.end - range.start + 1).as_u64();
if total_range_size == 0 {
return EngineStatus::Exhausted { hash_count: 0 };
}
// Thread configuration for a single dispatch over this range.
let threads_per_workgroup = 256u32; // Must match shader @workgroup_size(256)
let limits = gpu_ctx.device.limits();
let max_workgroups = limits.max_compute_workgroups_per_dimension;
// Vendor hint, clamped by hardware limits.
let hinted_workgroups = gpu_ctx.optimal_workgroups.max(1).min(max_workgroups);
let hinted_threads = hinted_workgroups as u64 * threads_per_workgroup as u64;
// Choose a logical thread budget: enough threads to fill the GPU, but no more than
// the range length (spawning more threads than nonces is wasteful).
let mut logical_threads = total_range_size.min(hinted_threads);
if logical_threads == 0 {
logical_threads = 1;
}
// Round logical_threads up to a multiple of workgroup size so we have full workgroups.
let mut num_workgroups = (logical_threads as u32).div_ceil(threads_per_workgroup);
if num_workgroups == 0 {
num_workgroups = 1;
}
let total_threads = (num_workgroups * threads_per_workgroup) as u64;
// Derive how many nonces each logical thread should process so that the entire
// range is covered in a single dispatch.
let nonces_per_thread = total_range_size.div_ceil(total_threads).max(1) as u32;
let total_threads_u32 = total_threads as u32;
log::info!(
target: "gpu_engine",
"GPU dispatch configuration: total_range={} nonces, workgroups={}, threads={}, nonces_per_thread={}",
total_range_size,
num_workgroups,
total_threads_u32,
nonces_per_thread
);
// We'll process the full range in a single dispatch.
let hash_count = total_range_size;
// Eliminate intermediate syncs - only sync at end or when solution found
const RESULTS_SIZE: usize = (1 + 16 + 16) * 4;
const ZEROS: [u8; RESULTS_SIZE] = [0; RESULTS_SIZE];
#[cfg(feature = "metrics")]
{
let device_id = "gpu-0";
metrics::set_gpu_batch_size(device_id, total_range_size as f64);
metrics::set_gpu_workgroups(device_id, num_workgroups as f64);
}
if cancel.load(Ordering::Relaxed) {
log::debug!(target: "gpu_engine", "GPU {} cancelled before dispatch.", device_index);
return EngineStatus::Cancelled { hash_count: 0 };
}
// Dispatch configuration for this range:
// [total_threads, nonces_per_thread, total_nonces, threads_per_workgroup]
let total_nonces_u32 = total_range_size.min(u32::MAX as u64) as u32;
let dispatch_config = [
total_threads_u32,
nonces_per_thread,
total_nonces_u32,
threads_per_workgroup,
];
gpu_ctx.queue.write_buffer(
&gpu_ctx.dispatch_config_buffer,
0,
bytemuck::cast_slice(&dispatch_config),
);
// Starting nonce for this range.
let start_nonce_bytes = range.start.to_little_endian();
gpu_ctx
.queue
.write_buffer(&gpu_ctx.start_nonce_buffer, 0, &start_nonce_bytes);
// Reset results buffer to detect solutions from this dispatch.
gpu_ctx
.queue
.write_buffer(&gpu_ctx.results_buffer, 0, &ZEROS);
let total_start = std::time::Instant::now();
// Create command buffer for this range
let mut encoder = gpu_ctx
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
{
let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: None,
timestamp_writes: None,
});
cpass.set_pipeline(&gpu_ctx.pipeline);
cpass.set_bind_group(0, &gpu_ctx.bind_group, &[]);
cpass.dispatch_workgroups(num_workgroups, 1, 1);
}
encoder.copy_buffer_to_buffer(
&gpu_ctx.results_buffer,
0,
&gpu_ctx.staging_buffer,
0,
RESULTS_SIZE as u64,
);
// Submit and wait for completion
gpu_ctx.queue.submit(Some(encoder.finish()));
let buffer_slice = gpu_ctx.staging_buffer.slice(..);
buffer_slice.map_async(wgpu::MapMode::Read, |_| {});
let _ = gpu_ctx.device.poll(wgpu::PollType::Wait {
submission_index: None,
timeout: None,
});
let data = buffer_slice.get_mapped_range();
let result_u32s: &[u32] = bytemuck::cast_slice(&data);
if result_u32s[0] != 0 {
// Solution found!
let nonce_u32s = &result_u32s[1..17];
let hash_u32s = &result_u32s[17..33];
let nonce = U512::from_little_endian(bytemuck::cast_slice(nonce_u32s));
let hash = U512::from_little_endian(bytemuck::cast_slice(hash_u32s));
let work = nonce.to_big_endian();
log::info!(
"GPU {} found solution! Nonce: {}, Hash: {:x}",
device_index,
nonce,
hash
);
drop(data);
gpu_ctx.staging_buffer.unmap();
return EngineStatus::Found {
candidate: Candidate { nonce, work, hash },
// Approximate: we assume a uniform distribution within the dispatch
// and report that we processed the entire range.
hash_count,
origin: FoundOrigin::GpuG1,
};
}
drop(data);
gpu_ctx.staging_buffer.unmap();
let total_time = total_start.elapsed();
log::info!(
target: "gpu_engine",
"GPU finished range. Total time: {:.2}ms, Batches: 1, Hashes: {}, Performance: {:.0} hashes/sec",
total_time.as_secs_f64() * 1000.0,
hash_count,
hash_count as f64 / total_time.as_secs_f64()
);
log::debug!(target: "gpu_engine", "GPU {} finished range with no solution.", device_index);
EngineStatus::Exhausted { hash_count }
}
}
impl GpuEngine {
/// Get vendor-specific optimal dispatch configuration
fn get_vendor_specific_dispatch(
adapter_info: &wgpu::AdapterInfo,
device: &wgpu::Device,
) -> u32 {
let limits = device.limits();
let max_workgroups = limits.max_compute_workgroups_per_dimension.min(65535);
// Parse vendor from adapter info
let vendor_name = adapter_info.name.to_lowercase();
let _device_name = adapter_info.device.to_string().to_lowercase();
// Vendor-specific heuristics based on architecture knowledge
let optimal_workgroups = if vendor_name.contains("nvidia") || adapter_info.vendor == 4318 {
// NVIDIA GPUs (vendor ID 0x10DE = 4318)
if vendor_name.contains("rtx 40") || vendor_name.contains("rtx 4090") {
(max_workgroups / 8).max(4096)
} else if vendor_name.contains("rtx 30") || vendor_name.contains("rtx 20") {
(max_workgroups / 12).max(2048)
} else if vendor_name.contains("gtx") || vendor_name.contains("rtx 16") {
(max_workgroups / 16).max(1024)
} else {
(max_workgroups / 20).max(512)
}
} else if vendor_name.contains("amd") || adapter_info.vendor == 4098 {
// AMD GPUs (vendor ID 0x1002 = 4098)
if vendor_name.contains("rx 7") || vendor_name.contains("rx 6900") {
(max_workgroups / 10).max(3072)
} else if vendor_name.contains("rx 6") || vendor_name.contains("rx 5700") {
(max_workgroups / 14).max(2048)
} else if vendor_name.contains("rx 5") || vendor_name.contains("rx 580") {
(max_workgroups / 18).max(1024)
} else {
(max_workgroups / 24).max(512)
}
} else if vendor_name.contains("intel") || adapter_info.vendor == 32902 {
// Intel GPUs (vendor ID 0x8086 = 32902)
if vendor_name.contains("arc a7") || vendor_name.contains("arc a770") {
(max_workgroups / 12).max(2048)
} else if vendor_name.contains("arc a5") || vendor_name.contains("arc a380") {
(max_workgroups / 16).max(1024)
} else if vendor_name.contains("iris xe") {
(max_workgroups / 20).max(512)
} else {
(max_workgroups / 24).max(256)
}
} else if adapter_info.backend == wgpu::Backend::Metal {
// Apple GPUs (detected by Metal backend)
let (gpu_cores, workgroups) = if vendor_name.contains("m4 max") {
(40, 800)
} else if vendor_name.contains("m4 pro") {
(20, 400)
} else if vendor_name.contains("m4") {
(10, 200)
} else if vendor_name.contains("m3 max") {
(40, 800)
} else if vendor_name.contains("m3 pro") {
(18, 360)
} else if vendor_name.contains("m3") {
(10, 200)
} else if vendor_name.contains("m2 ultra") {
(76, 1520)
} else if vendor_name.contains("m2 max") {
(38, 760)
} else if vendor_name.contains("m2 pro") {
(19, 380)
} else if vendor_name.contains("m2") {
(10, 200)
} else if vendor_name.contains("m1 ultra") {
(64, 1280)
} else if vendor_name.contains("m1 max") {
(32, 640)
} else if vendor_name.contains("m1 pro") {
(16, 320)
} else {
(8, 160)
};
let clamped_workgroups = workgroups.min(max_workgroups / 4).max(64);
let _ = gpu_cores; // gpu_cores currently unused but kept for potential future tuning
clamped_workgroups
} else {
// Unknown/Generic GPU - use conservative defaults
(max_workgroups / 16).max(512)
};
log::info!(target: "gpu_engine", "Vendor-specific dispatch configuration:");
log::info!(target: "gpu_engine", " Max hardware workgroups: {}", max_workgroups);
log::info!(target: "gpu_engine", " Optimal workgroups: {}", optimal_workgroups);
optimal_workgroups
}
}

View File

@@ -0,0 +1,177 @@
use futures::executor::block_on;
mod end_to_end_tests;
mod tests;
fn main() {
block_on(run()).unwrap();
}
async fn run() -> Result<(), Box<dyn std::error::Error>> {
// Setup GPU device and queue
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::METAL, // Force Metal on Apple
..Default::default()
});
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions::default())
.await
.unwrap();
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor::default())
.await?;
println!("Running Poseidon2 GPU Component Tests...\n");
// Run all component tests
if let Err(e) = tests::test_gf_from_const(&device, &queue).await {
eprintln!("❌ gf_from_const tests failed: {}", e);
}
if let Err(e) = tests::test_gf_mul(&device, &queue).await {
eprintln!("❌ gf_mul tests failed: {}", e);
}
if let Err(e) = tests::test_sbox(&device, &queue).await {
eprintln!("❌ S-box tests failed: {}", e);
}
if let Err(e) = tests::test_mds_matrix(&device, &queue).await {
eprintln!("❌ MDS matrix tests failed: {}", e);
}
if let Err(e) = tests::test_internal_linear_layer(&device, &queue).await {
eprintln!("❌ Internal linear layer tests failed: {}", e);
}
if let Err(e) = tests::test_external_linear_layer(&device, &queue).await {
eprintln!("❌ External linear layer tests failed: {}", e);
}
if let Err(e) = tests::test_poseidon2_initial_external_rounds(&device, &queue).await {
eprintln!("❌ Initial external rounds tests failed: {}", e);
}
if let Err(e) = tests::test_poseidon2_terminal_external_rounds(&device, &queue).await {
eprintln!("❌ Terminal external rounds tests failed: {}", e);
}
if let Err(e) = tests::test_poseidon2_constants_verification(&device, &queue).await {
eprintln!("❌ Constants verification test failed: {}", e);
}
if let Err(e) = tests::test_poseidon2_internal_constants_verification(&device, &queue).await {
eprintln!("❌ Internal constants verification test failed: {}", e);
}
if let Err(e) = tests::test_poseidon2_internal_rounds_only(&device, &queue).await {
eprintln!("❌ Internal rounds only test failed: {}", e);
}
if let Err(e) =
tests::test_poseidon2_terminal_external_constants_verification(&device, &queue).await
{
eprintln!(
"❌ Terminal external constants verification test failed: {}",
e
);
}
if let Err(e) = tests::test_poseidon2_permutation(&device, &queue).await {
eprintln!("❌ Poseidon2 permutation tests failed: {}", e);
}
if let Err(e) = tests::test_bytes_to_field_elements(&device, &queue).await {
eprintln!("❌ Bytes to field elements tests failed: {}", e);
}
if let Err(e) = tests::test_field_elements_to_bytes(&device, &queue).await {
eprintln!("❌ Field elements to bytes tests failed: {}", e);
}
if let Err(e) = tests::test_poseidon2_squeeze_twice(&device, &queue).await {
eprintln!("❌ Poseidon2 squeeze-twice tests failed: {}", e);
}
if let Err(e) = tests::test_double_hash(&device, &queue).await {
eprintln!("❌ Double hash tests failed: {}", e);
}
if let Err(e) = end_to_end_tests::test_end_to_end_mining(&device, &queue).await {
eprintln!("❌ End-to-end mining test failed: {}", e);
}
println!("\nAll tests completed!");
if let Err(e) = end_to_end_tests::test_end_to_end_mining(&device, &queue).await {
eprintln!("❌ End-to-end mining test failed: {}", e);
}
println!("\nAll tests completed!");
// generate_correct_wgsl_constants();
Ok(())
}
#[allow(dead_code)]
fn generate_correct_wgsl_constants() {
use qp_poseidon_constants::*;
println!("🔧 Generating correct WGSL constants...");
println!("// Initial external round constants (4 rounds x 12 elements)");
println!("const INITIAL_EXTERNAL_CONSTANTS: array<array<array<u32, 2>, 12>, 4> = array<array<array<u32, 2>, 12>, 4>(");
for (round_idx, round) in POSEIDON2_INITIAL_EXTERNAL_CONSTANTS_RAW.iter().enumerate() {
println!(" array<array<u32, 2>, 12>(");
for (elem_idx, &value) in round.iter().enumerate() {
let low = value as u32;
let high = (value >> 32) as u32;
if elem_idx == 11 {
println!(" array<u32, 2>({}u, {}u)", low, high);
} else {
println!(" array<u32, 2>({}u, {}u),", low, high);
}
}
if round_idx == 3 {
println!(" )");
} else {
println!(" ),");
}
}
println!(");");
println!("\n// Terminal external round constants (4 rounds x 12 elements)");
println!("const TERMINAL_EXTERNAL_CONSTANTS: array<array<array<u32, 2>, 12>, 4> = array<array<array<u32, 2>, 12>, 4>(");
for (round_idx, round) in POSEIDON2_TERMINAL_EXTERNAL_CONSTANTS_RAW.iter().enumerate() {
println!(" array<array<u32, 2>, 12>(");
for (elem_idx, &value) in round.iter().enumerate() {
let low = value as u32;
let high = (value >> 32) as u32;
if elem_idx == 11 {
println!(" array<u32, 2>({}u, {}u)", low, high);
} else {
println!(" array<u32, 2>({}u, {}u),", low, high);
}
}
if round_idx == 3 {
println!(" )");
} else {
println!(" ),");
}
}
println!(");");
println!("\n// Internal round constants (22 values)");
println!("const INTERNAL_CONSTANTS: array<array<u32, 2>, 22> = array<array<u32, 2>, 22>(");
for (idx, &value) in POSEIDON2_INTERNAL_CONSTANTS_RAW.iter().enumerate() {
let low = value as u32;
let high = (value >> 32) as u32;
if idx == 21 {
println!(" array<u32, 2>({}u, {}u)", low, high);
} else {
println!(" array<u32, 2>({}u, {}u),", low, high);
}
}
println!(");");
}

View File

@@ -0,0 +1,903 @@
// Quantus Mining Shader - WGSL Compute Shader for GPU Mining
// Implements Poseidon2 hash function over Goldilocks field
// Goldilocks field constants: p = 2^64 - 2^32 + 1 = 18446744069414584321
// Since WGSL doesn't support 64-bit literals, we work with 32-bit chunks
// p = 0xFFFFFFFF00000001 = [0x00000001, 0xFFFFFFFF] in little-endian u32 pairs
const GOLDILOCKS_PRIME_LOW: u32 = 1u; // Low 32 bits
const GOLDILOCKS_PRIME_HIGH: u32 = 4294967295u; // High 32 bits (2^32 - 1)
// Poseidon2 constants
const WIDTH: u32 = 12u;
const RATE: u32 = 4u;
const EXTERNAL_ROUNDS: u32 = 4u;
const INTERNAL_ROUNDS: u32 = 22u;
// Individual constants for testing
const INTERNAL_CONST_0_LOW: u32 = 2018170979u;
const INTERNAL_CONST_0_HIGH: u32 = 2549578122u;
const INTERNAL_CONST_1_LOW: u32 = 794875120u;
const INTERNAL_CONST_1_HIGH: u32 = 3520249608u;
// Real Poseidon2 constants extracted from qp-poseidon-constants
// Internal round constants (22 values)
const INTERNAL_CONSTANTS: array<array<u32, 2>, 22> = array<array<u32, 2>, 22>(
array<u32, 2>(2018170979u, 2549578122u),
array<u32, 2>(794875120u, 3520249608u),
array<u32, 2>(2677723654u, 1772320679u),
array<u32, 2>(2743438884u, 2849007878u),
array<u32, 2>(518907317u, 693269760u),
array<u32, 2>(293328710u, 1484055617u),
array<u32, 2>(2834138828u, 2315799483u),
array<u32, 2>(1558078501u, 1039128420u),
array<u32, 2>(2266808631u, 966316006u),
array<u32, 2>(3393728842u, 1045622667u),
array<u32, 2>(2245828300u, 2521440415u),
array<u32, 2>(751064958u, 1070374632u),
array<u32, 2>(3490278765u, 2390340773u),
array<u32, 2>(3526960470u, 2224174634u),
array<u32, 2>(639988950u, 4000511088u),
array<u32, 2>(1839350858u, 504240201u),
array<u32, 2>(559852230u, 255489215u),
array<u32, 2>(2713771731u, 453385078u),
array<u32, 2>(1745082278u, 422331096u),
array<u32, 2>(2544763488u, 4141129721u),
array<u32, 2>(2700752774u, 1052996327u),
array<u32, 2>(4063512019u, 1429786100u)
);
// Initial external round constants (4 rounds x 12 elements)
const INITIAL_EXTERNAL_CONSTANTS: array<array<array<u32, 2>, 12>, 4> = array<array<array<u32, 2>, 12>, 4>(
array<array<u32, 2>, 12>(
array<u32, 2>(2539329031u, 3221415792u),
array<u32, 2>(4262746426u, 3164936845u),
array<u32, 2>(3883202553u, 1922272763u),
array<u32, 2>(3761386668u, 3841130025u),
array<u32, 2>(1411081289u, 3588274735u),
array<u32, 2>(4090250945u, 3962812520u),
array<u32, 2>(1100826458u, 1215155029u),
array<u32, 2>(1489773809u, 1813820067u),
array<u32, 2>(2585015995u, 3824356688u),
array<u32, 2>(2378857513u, 3651555078u),
array<u32, 2>(2864423342u, 3852156759u),
array<u32, 2>(1531416540u, 708695120u)
),
array<array<u32, 2>, 12>(
array<u32, 2>(1987505445u, 2913073372u),
array<u32, 2>(1426707734u, 655469195u),
array<u32, 2>(3385403543u, 1256631504u),
array<u32, 2>(1381422714u, 1458257259u),
array<u32, 2>(2453402910u, 528129365u),
array<u32, 2>(964329320u, 905986685u),
array<u32, 2>(1534247888u, 3842469367u),
array<u32, 2>(744525997u, 4241857185u),
array<u32, 2>(1756723870u, 3448331916u),
array<u32, 2>(3610291774u, 1105166073u),
array<u32, 2>(2596181885u, 3997051784u),
array<u32, 2>(3199845381u, 3533420525u)
),
array<array<u32, 2>, 12>(
array<u32, 2>(4127777666u, 2103183598u),
array<u32, 2>(2867888172u, 2309916828u),
array<u32, 2>(1831532055u, 3009056407u),
array<u32, 2>(2947502451u, 3675530062u),
array<u32, 2>(3565886616u, 2029012066u),
array<u32, 2>(3833391242u, 642945968u),
array<u32, 2>(1773785903u, 2577032347u),
array<u32, 2>(1770914259u, 1689297286u),
array<u32, 2>(3752758200u, 3993707216u),
array<u32, 2>(3389302766u, 1339375184u),
array<u32, 2>(2180141127u, 1466089441u),
array<u32, 2>(3199591357u, 4111832034u)
),
array<array<u32, 2>, 12>(
array<u32, 2>(1625498743u, 509487959u),
array<u32, 2>(4188712685u, 1646551713u),
array<u32, 2>(3451003566u, 2854767422u),
array<u32, 2>(1412166652u, 1674110767u),
array<u32, 2>(3410212320u, 1000704202u),
array<u32, 2>(3381743837u, 602777331u),
array<u32, 2>(3131873882u, 2866003620u),
array<u32, 2>(2610174026u, 3923414377u),
array<u32, 2>(3644719692u, 3450945356u),
array<u32, 2>(1458984419u, 2418851081u),
array<u32, 2>(3344519983u, 1531855103u),
array<u32, 2>(2721413879u, 3732495392u)
)
);
// Terminal external round constants (4 rounds x 12 elements)
const TERMINAL_EXTERNAL_CONSTANTS: array<array<array<u32, 2>, 12>, 4> = array<array<array<u32, 2>, 12>, 4>(
array<array<u32, 2>, 12>(
array<u32, 2>(3773451374u, 2071119161u),
array<u32, 2>(3805190518u, 340095962u),
array<u32, 2>(2402679944u, 2149591222u),
array<u32, 2>(743434178u, 1832305922u),
array<u32, 2>(2847530739u, 2718290175u),
array<u32, 2>(514243119u, 4142392203u),
array<u32, 2>(3844443492u, 888639642u),
array<u32, 2>(2008645578u, 2957397405u),
array<u32, 2>(3732799654u, 1692252629u),
array<u32, 2>(664231319u, 248567644u),
array<u32, 2>(287781771u, 482031345u),
array<u32, 2>(3486561978u, 1718871301u)
),
array<array<u32, 2>, 12>(
array<u32, 2>(814165505u, 1616875560u),
array<u32, 2>(2123759183u, 1070984082u),
array<u32, 2>(2722916813u, 3893372341u),
array<u32, 2>(3726899022u, 4157656693u),
array<u32, 2>(2824360073u, 4086907574u),
array<u32, 2>(4155973110u, 1837140488u),
array<u32, 2>(2297731723u, 4169165669u),
array<u32, 2>(707924090u, 1474243980u),
array<u32, 2>(1298483757u, 384287239u),
array<u32, 2>(4243798069u, 557703745u),
array<u32, 2>(1510569718u, 2968696976u),
array<u32, 2>(3174388759u, 3638808363u)
),
array<array<u32, 2>, 12>(
array<u32, 2>(2044277519u, 3835193622u),
array<u32, 2>(2743212840u, 1983595986u),
array<u32, 2>(3943309069u, 151568229u),
array<u32, 2>(424355060u, 1989274413u),
array<u32, 2>(867046322u, 239293714u),
array<u32, 2>(4230997871u, 2479068123u),
array<u32, 2>(1565052394u, 2566260552u),
array<u32, 2>(815274432u, 3822673712u),
array<u32, 2>(1051683535u, 519405993u),
array<u32, 2>(2687564964u, 186958263u),
array<u32, 2>(1450226471u, 1648586942u),
array<u32, 2>(1511122054u, 1595811937u)
),
array<array<u32, 2>, 12>(
array<u32, 2>(2426274859u, 4261676319u),
array<u32, 2>(1207777283u, 1918363057u),
array<u32, 2>(3090099832u, 3870167883u),
array<u32, 2>(4101522705u, 1460796764u),
array<u32, 2>(201900220u, 4164567654u),
array<u32, 2>(2587682901u, 752404845u),
array<u32, 2>(2967564913u, 2100296475u),
array<u32, 2>(3404347409u, 2242778408u),
array<u32, 2>(3350048952u, 1386431957u),
array<u32, 2>(4093308564u, 1347177553u),
array<u32, 2>(2633812729u, 3169012324u),
array<u32, 2>(1727753673u, 3768793234u)
)
);
// Helper function to create GoldilocksField from constant array
fn gf_from_const(val: array<u32, 2>) -> GoldilocksField {
return gf_from_u64_parts(val[0], val[1]);
}
// MDS matrix constants for width 12 - circulant matrix first row
// MDS matrix diagonal for width 12 Goldilocks (from p3_goldilocks constants)
// Each element is stored as [low32, high32] pairs
const MDS_MATRIX_DIAG_12: array<array<u32, 2>, 12> = array<array<u32, 2>, 12>(
array<u32, 2>(0x23ba9300u, 0xc3b6c08eu), // 0xc3b6c08e23ba9300u
array<u32, 2>(0x4a324fb6u, 0xd84b5de9u), // 0xd84b5de94a324fb6u
array<u32, 2>(0x5b35b84fu, 0x0d0c371cu), // 0x0d0c371c5b35b84fu
array<u32, 2>(0xe7188037u, 0x7964f570u), // 0x7964f570e7188037u
array<u32, 2>(0xd996604bu, 0x5daf18bbu), // 0x5daf18bbd996604bu
array<u32, 2>(0xb9595257u, 0x6743bc47u), // 0x6743bc47b9595257u
array<u32, 2>(0x2c59bb70u, 0x5528b936u), // 0x5528b9362c59bb70u
array<u32, 2>(0x7127b68bu, 0xac45e25bu), // 0xac45e25b7127b68bu
array<u32, 2>(0xfbb606b5u, 0xa2077d7du), // 0xa2077d7dfbb606b5u
array<u32, 2>(0xaee378aeu, 0xf3faac6fu), // 0xf3faac6faee378aeu
array<u32, 2>(0x1545e883u, 0x0c6388b5u), // 0x0c6388b51545e883u
array<u32, 2>(0x44917b60u, 0xd27dbb69u) // 0xd27dbb6944917b60u
);
// Storage buffers
@group(0) @binding(0) var<storage, read_write> results: array<atomic<u32>>;
@group(0) @binding(1) var<storage, read> header: array<u32, 8>; // 32 bytes
@group(0) @binding(2) var<storage, read> start_nonce: array<u32, 16>; // 64 bytes
@group(0) @binding(3) var<storage, read> difficulty_target: array<u32, 16>; // 64 bytes (U512 target)
@group(0) @binding(4) var<storage, read> dispatch_config: array<u32, 4>; // [total_threads (logical), nonces_per_thread, total_nonces (logical nonces in this dispatch), threads_per_workgroup]
// Goldilocks field element represented as [limb0, limb1]
// where the value is limb0 + limb1*2^32
struct GoldilocksField {
limb0: u32,
limb1: u32,
}
// Field modulus P = 2^64 - 2^32 + 1 = 0xFFFFFFFF00000001
// In u32 limbs: P = [1, 0xFFFFFFFF]
const P_LIMB0: u32 = 1u;
const P_LIMB1: u32 = 0xFFFFFFFFu;
// EPSILON = 2^32 - 1 = 0x00000000FFFFFFFF
// In u32 limbs: EPSILON = [0xFFFFFFFF, 0]
const EPSILON_LIMB0: u32 = 0xFFFFFFFFu;
const EPSILON_LIMB1: u32 = 0u;
// Helper to create field elements
fn gf_from_limbs(l0: u32, l1: u32) -> GoldilocksField {
return GoldilocksField(l0, l1);
}
fn gf_zero() -> GoldilocksField {
return gf_from_limbs(0u, 0u);
}
fn gf_one() -> GoldilocksField {
return gf_from_limbs(1u, 0u);
}
fn gf_from_u32(val: u32) -> GoldilocksField {
return GoldilocksField(val, 0u);
}
// Convert a 64-bit value (as two u32s) to GoldilocksField
fn gf_from_u64_parts(low: u32, high: u32) -> GoldilocksField {
var result = GoldilocksField(low, high);
// Reduce if >= P
if (gf_compare(result, gf_from_limbs(P_LIMB0, P_LIMB1)) != 2u) {
result = gf_sub(result, gf_from_limbs(P_LIMB0, P_LIMB1));
}
return result;
}
// Addition with carry for u32 values
// Returns vec2<u32>(sum, carry)
fn u32_add_with_carry(a: u32, b: u32, carry_in: u32) -> vec2<u32> {
let sum1 = a + b;
let c1 = select(0u, 1u, sum1 < a);
let sum2 = sum1 + carry_in;
let c2 = select(0u, 1u, sum2 < sum1);
return vec2<u32>(sum2, c1 + c2);
}
// Subtraction with borrow for u32 values
// Returns vec2<u32>(diff, borrow)
fn u32_sub_with_borrow(a: u32, b: u32, borrow_in: u32) -> vec2<u32> {
let diff1 = a - b;
let b1 = select(0u, 1u, diff1 > a); // if a - b > a, then underflow happened (since b > 0)
// Wait, if a < b, a - b wraps around to a large number.
// e.g. 2 - 3 = 0xFFFFFFFF. 0xFFFFFFFF > 2. Correct.
let diff2 = diff1 - borrow_in;
let b2 = select(0u, 1u, diff2 > diff1);
return vec2<u32>(diff2, b1 + b2);
}
// Multiply two u32 values to get a u64 result (as vec2<u32>)
fn u32_mul_to_u64(a: u32, b: u32) -> vec2<u32> {
let a_lo = a & 0xFFFFu;
let a_hi = a >> 16u;
let b_lo = b & 0xFFFFu;
let b_hi = b >> 16u;
let p0 = a_lo * b_lo;
let p1 = a_hi * b_lo;
let p2 = a_lo * b_hi;
let p3 = a_hi * b_hi;
let sum_mid_part = p1 + p2;
let carry_mid = select(0u, 1u, sum_mid_part < p1);
let term_mid_lo = sum_mid_part << 16u;
let term_mid_hi = (sum_mid_part >> 16u) | (carry_mid << 16u);
let res_lo = p0 + term_mid_lo;
let carry_lo = select(0u, 1u, res_lo < p0);
let res_hi = p3 + term_mid_hi + carry_lo;
return vec2<u32>(res_lo, res_hi);
}
// Compare two GoldilocksField values
// Returns: 0 if a == b, 1 if a > b, 2 if a < b
fn gf_compare(a: GoldilocksField, b: GoldilocksField) -> u32 {
// Compare from most significant limb to least
if (a.limb1 != b.limb1) {
return select(2u, 1u, a.limb1 > b.limb1);
}
if (a.limb0 != b.limb0) {
return select(2u, 1u, a.limb0 > b.limb0);
}
return 0u; // Equal
}
// Goldilocks field addition
fn gf_add(a: GoldilocksField, b: GoldilocksField) -> GoldilocksField {
// Add limb by limb with carry propagation
let add0 = u32_add_with_carry(a.limb0, b.limb0, 0u);
let add1 = u32_add_with_carry(a.limb1, b.limb1, add0.y);
var result = GoldilocksField(add0.x, add1.x);
// Handle overflow: if carry out, we've computed a + b = result + 2^64
// In Goldilocks: 2^64 ≡ 2^32 - 1 (mod P)
if (add1.y != 0u) {
// Add EPSILON = 0xFFFFFFFF00000000 (Wait, EPSILON is 2^32 - 1)
// EPSILON = 0x00000000FFFFFFFF
// EPSILON_LIMB0 = 0xFFFFFFFF, EPSILON_LIMB1 = 0
let eps_add0 = u32_add_with_carry(result.limb0, EPSILON_LIMB0, 0u);
let eps_add1 = u32_add_with_carry(result.limb1, EPSILON_LIMB1, eps_add0.y);
result = GoldilocksField(eps_add0.x, eps_add1.x);
// If adding EPSILON caused another overflow, add EPSILON again
if (eps_add1.y != 0u) {
let eps2_add0 = u32_add_with_carry(result.limb0, EPSILON_LIMB0, 0u);
let eps2_add1 = u32_add_with_carry(result.limb1, EPSILON_LIMB1, eps2_add0.y);
result = GoldilocksField(eps2_add0.x, eps2_add1.x);
}
}
// Final reduction if result >= P
let p = gf_from_limbs(P_LIMB0, P_LIMB1);
if (gf_compare(result, p) != 2u) { // if result >= P
result = gf_sub_no_underflow(result, p);
}
return result;
}
// Helper for subtraction without underflow (assumes a >= b)
fn gf_sub_no_underflow(a: GoldilocksField, b: GoldilocksField) -> GoldilocksField {
let sub0 = u32_sub_with_borrow(a.limb0, b.limb0, 0u);
let sub1 = u32_sub_with_borrow(a.limb1, b.limb1, sub0.y);
return GoldilocksField(sub0.x, sub1.x);
}
// Goldilocks field subtraction
fn gf_sub(a: GoldilocksField, b: GoldilocksField) -> GoldilocksField {
// If a >= b, do direct subtraction
if (gf_compare(a, b) != 2u) {
return gf_sub_no_underflow(a, b);
}
// Otherwise: a < b, so compute a - b + P
let p = gf_from_limbs(P_LIMB0, P_LIMB1);
let a_plus_p = gf_add_no_reduction(a, p);
return gf_sub_no_underflow(a_plus_p, b);
}
// Addition without final modular reduction (used internally)
fn gf_add_no_reduction(a: GoldilocksField, b: GoldilocksField) -> GoldilocksField {
let add0 = u32_add_with_carry(a.limb0, b.limb0, 0u);
let add1 = u32_add_with_carry(a.limb1, b.limb1, add0.y);
return GoldilocksField(add0.x, add1.x);
}
// Simplified multiplication that handles carries more carefully
fn gf_mul_unreduced(a: GoldilocksField, b: GoldilocksField) -> array<u32, 4> {
// p0 = a0 * b0 (64 bits)
let p0 = u32_mul_to_u64(a.limb0, b.limb0);
// p1 = a0 * b1 (64 bits)
let p1 = u32_mul_to_u64(a.limb0, b.limb1);
// p2 = a1 * b0 (64 bits)
let p2 = u32_mul_to_u64(a.limb1, b.limb0);
// p3 = a1 * b1 (64 bits)
let p3 = u32_mul_to_u64(a.limb1, b.limb1);
// result[0] = p0.x
// result[1] = p0.y + p1.x + p2.x + carry
// result[2] = p1.y + p2.y + p3.x + carry
// result[3] = p3.y + carry
var r0 = p0.x;
let sum1 = u32_add_with_carry(p0.y, p1.x, 0u);
let sum2 = u32_add_with_carry(sum1.x, p2.x, 0u);
var r1 = sum2.x;
var c1 = sum1.y + sum2.y; // carry to next limb
let sum3 = u32_add_with_carry(p1.y, p2.y, c1);
let sum4 = u32_add_with_carry(sum3.x, p3.x, 0u);
var r2 = sum4.x;
var c2 = sum3.y + sum4.y;
let sum5 = u32_add_with_carry(p3.y, 0u, c2);
var r3 = sum5.x;
return array<u32, 4>(r0, r1, r2, r3);
}
// Reduce a 4-limb number modulo the Goldilocks prime
fn gf_reduce_4limb(limbs: array<u32, 4>) -> GoldilocksField {
// x_lo = limbs[0], limbs[1]
let x_lo = gf_from_limbs(limbs[0], limbs[1]);
// x_hi = limbs[2], limbs[3]
// x_hi_hi = limbs[3] (upper 32 bits of x_hi)
let x_hi_hi = gf_from_limbs(limbs[3], 0u);
// x_hi_lo = limbs[2] (lower 32 bits of x_hi)
let x_hi_lo = gf_from_limbs(limbs[2], 0u);
// Step 1: t0 = x_lo - x_hi_hi (with underflow detection)
var t0: GoldilocksField;
var underflow = false;
let sub0 = u32_sub_with_borrow(x_lo.limb0, x_hi_hi.limb0, 0u);
let sub1 = u32_sub_with_borrow(x_lo.limb1, x_hi_hi.limb1, sub0.y);
t0 = GoldilocksField(sub0.x, sub1.x);
if (sub1.y != 0u) {
underflow = true;
}
// Step 2: if underflow { t0 -= NEG_ORDER; }
if (underflow) {
let eps_sub0 = u32_sub_with_borrow(t0.limb0, EPSILON_LIMB0, 0u);
let eps_sub1 = u32_sub_with_borrow(t0.limb1, EPSILON_LIMB1, eps_sub0.y);
t0 = GoldilocksField(eps_sub0.x, eps_sub1.x);
}
// Step 3: t1 = x_hi_lo * NEG_ORDER
// NEG_ORDER = 2^32 - 1
// x_hi_lo * (2^32 - 1) = (x_hi_lo << 32) - x_hi_lo
// x_hi_lo is [limbs[2], 0]
// x_hi_lo << 32 is [0, limbs[2]]
let shifted = GoldilocksField(0u, x_hi_lo.limb0);
let t1_sub0 = u32_sub_with_borrow(shifted.limb0, x_hi_lo.limb0, 0u);
let t1_sub1 = u32_sub_with_borrow(shifted.limb1, x_hi_lo.limb1, t1_sub0.y);
let t1 = GoldilocksField(t1_sub0.x, t1_sub1.x);
// Step 4: result = t0 + t1 (with overflow handling like CPU)
let add0 = u32_add_with_carry(t0.limb0, t1.limb0, 0u);
let add1 = u32_add_with_carry(t0.limb1, t1.limb1, add0.y);
var result = GoldilocksField(add0.x, add1.x);
// If overflow, add NEG_ORDER
if (add1.y != 0u) {
let eps_add0 = u32_add_with_carry(result.limb0, EPSILON_LIMB0, 0u);
let eps_add1 = u32_add_with_carry(result.limb1, EPSILON_LIMB1, eps_add0.y);
result = GoldilocksField(eps_add0.x, eps_add1.x);
}
return result;
}
// Main Goldilocks field multiplication
fn gf_mul(a: GoldilocksField, b: GoldilocksField) -> GoldilocksField {
// Handle special cases
if (a.limb0 == 0u && a.limb1 == 0u) { return gf_zero(); }
if (b.limb0 == 0u && b.limb1 == 0u) { return gf_zero(); }
if (a.limb0 == 1u && a.limb1 == 0u) { return b; }
if (b.limb0 == 1u && b.limb1 == 0u) { return a; }
// General case: multiply and reduce
let unreduced = gf_mul_unreduced(a, b);
return gf_reduce_4limb(unreduced);
}
// S-box: x^7 in Goldilocks field (efficient approach)
fn sbox(x: GoldilocksField) -> GoldilocksField {
let x2 = gf_mul(x, x);
let x4 = gf_mul(x2, x2);
let x6 = gf_mul(x4, x2);
return gf_mul(x6, x);
}
// External linear layer for width 12 using correct 4x4 MDS matrix
// Standard MDSMat4: [[2, 3, 1, 1], [1, 2, 3, 1], [1, 1, 2, 3], [3, 1, 1, 2]]
fn external_linear_layer(state: ptr<function, array<GoldilocksField, 12>>) {
// First apply the 4x4 MDS matrix to each consecutive 4 elements
for (var chunk = 0u; chunk < 3u; chunk++) {
let offset = chunk * 4u;
var x: array<GoldilocksField, 4>;
// Copy chunk
for (var i = 0u; i < 4u; i++) {
x[i] = (*state)[offset + i];
}
// Optimized 4x4 MDS application using only additions and doubles,
// matching apply_external_linear_layer_to_chunk in the CPU implementation.
let t01 = gf_add(x[0], x[1]);
let t23 = gf_add(x[2], x[3]);
let t0123 = gf_add(t01, t23);
let t01123 = gf_add(t0123, x[1]);
let t01233 = gf_add(t0123, x[3]);
let two_x0 = gf_add(x[0], x[0]);
let two_x2 = gf_add(x[2], x[2]);
// The order of updates matches the reference algorithm:
// x[3] = t01233 + 2*x[0]
// x[1] = t01123 + 2*x[2]
// x[0] = t01123 + t01
// x[2] = t01233 + t23
let new_3 = gf_add(t01233, two_x0);
let new_1 = gf_add(t01123, two_x2);
let new_0 = gf_add(t01123, t01);
let new_2 = gf_add(t01233, t23);
// Copy back
(*state)[offset + 0u] = new_0;
(*state)[offset + 1u] = new_1;
(*state)[offset + 2u] = new_2;
(*state)[offset + 3u] = new_3;
}
// Now apply the circulant matrix part
// Precompute the four sums of every four elements
var sums: array<GoldilocksField, 4>;
for (var k = 0u; k < 4u; k++) {
sums[k] = gf_zero();
for (var j = k; j < 12u; j += 4u) {
sums[k] = gf_add(sums[k], (*state)[j]);
}
}
// Add the appropriate sum to each element
for (var i = 0u; i < 12u; i++) {
(*state)[i] = gf_add((*state)[i], sums[i % 4u]);
}
}
// Internal linear layer using diagonal matrix for width 12
fn internal_linear_layer(state: ptr<function, array<GoldilocksField, 12>>) {
var result: array<GoldilocksField, 12>;
// Sum all elements
var sum = gf_zero();
for (var i = 0u; i < 12u; i++) {
sum = gf_add(sum, (*state)[i]);
}
// Apply diagonal matrix: result[i] = state[i] * diag[i] + sum
for (var i = 0u; i < 12u; i++) {
let diag_val = gf_from_u64_parts(
MDS_MATRIX_DIAG_12[i][0], // low 32 bits
MDS_MATRIX_DIAG_12[i][1] // high 32 bits
);
let scaled = gf_mul((*state)[i], diag_val);
result[i] = gf_add(scaled, sum);
}
// Copy result back to state
for (var i = 0u; i < 12u; i++) {
(*state)[i] = result[i];
}
}
// Fixed Poseidon2 permutation with proper structure
fn poseidon2_permute(state: ptr<function, array<GoldilocksField, 12>>) {
// Initial MDS permutation (required by p3-poseidon2 spec)
external_linear_layer(state);
// Initial external rounds (4 rounds)
for (var round = 0u; round < 4u; round++) {
// Add round constants
for (var i = 0u; i < 12u; i++) {
(*state)[i] = gf_add((*state)[i], gf_from_const(INITIAL_EXTERNAL_CONSTANTS[round][i]));
}
// S-box on all elements
for (var i = 0u; i < 12u; i++) {
(*state)[i] = sbox((*state)[i]);
}
// External linear layer (4x4 MDS matrix)
external_linear_layer(state);
}
// Internal rounds (22 rounds)
for (var round = 0u; round < 22u; round++) {
// Add round constant to first element only
(*state)[0] = gf_add((*state)[0], gf_from_const(INTERNAL_CONSTANTS[round]));
// S-box on first element only
(*state)[0] = sbox((*state)[0]);
// Internal linear layer (diagonal matrix)
internal_linear_layer(state);
}
// Terminal external rounds (4 rounds)
for (var round = 0u; round < 4u; round++) {
// Add round constants
for (var i = 0u; i < 12u; i++) {
(*state)[i] = gf_add((*state)[i], gf_from_const(TERMINAL_EXTERNAL_CONSTANTS[round][i]));
}
// S-box on all elements
for (var i = 0u; i < 12u; i++) {
(*state)[i] = sbox((*state)[i]);
}
// External linear layer (4x4 MDS matrix)
external_linear_layer(state);
}
}
// Convert bytes to Goldilocks field elements matching reference implementation
// Reference uses 4 bytes per field element with injective padding, creating 25 field elements from 96 bytes
fn bytes_to_field_elements(input: array<u32, 24>) -> array<GoldilocksField, 25> {
var felts: array<GoldilocksField, 25>;
// Convert u32 array to bytes (96 bytes total)
var bytes: array<u32, 96>; // Using u32s to store bytes for easier processing
for (var i = 0u; i < 24u; i++) {
let val = input[i];
bytes[i * 4u + 0u] = val & 0xFFu; // byte 0
bytes[i * 4u + 1u] = (val >> 8u) & 0xFFu; // byte 1
bytes[i * 4u + 2u] = (val >> 16u) & 0xFFu; // byte 2
bytes[i * 4u + 3u] = (val >> 24u) & 0xFFu; // byte 3
}
// Apply injective padding: add 1 byte, then pad with zeros to 4-byte alignment
var padded_len = 96u + 1u; // 96 bytes + 1 marker byte = 97
let padding_needed = (4u - (padded_len % 4u)) % 4u;
padded_len = padded_len + padding_needed; // Should be 100 bytes (25 u32s worth)
// Create padded byte array
var padded_bytes: array<u32, 100>;
for (var i = 0u; i < 96u; i++) {
padded_bytes[i] = bytes[i];
}
padded_bytes[96] = 1u; // End marker
for (var i = 97u; i < 100u; i++) {
padded_bytes[i] = 0u; // Padding zeros
}
// Convert every 4 bytes to one field element (25 field elements total)
for (var i = 0u; i < 25u; i++) {
let byte_idx = i * 4u;
// Create u32 from 4 bytes in little-endian order
let val = padded_bytes[byte_idx] |
(padded_bytes[byte_idx + 1u] << 8u) |
(padded_bytes[byte_idx + 2u] << 16u) |
(padded_bytes[byte_idx + 3u] << 24u);
felts[i] = gf_from_u32(val);
}
return felts;
}
// Convert field elements back to bytes
fn field_elements_to_bytes(felts: array<GoldilocksField, 4>) -> array<u32, 8> {
var result: array<u32, 8>;
for (var i = 0u; i < 4u; i++) {
result[i * 2u] = felts[i].limb0;
result[i * 2u + 1u] = felts[i].limb1;
}
return result;
}
// Fixed Poseidon2 hash function with proper sponge construction
fn poseidon2_hash_squeeze_twice(input: array<u32, 24>) -> array<u32, 16> {
var state: array<GoldilocksField, 12>;
// Initialize state to zero
for (var i = 0u; i < 12u; i++) {
state[i] = gf_zero();
}
// Convert input to field elements (25 total)
let input_felts = bytes_to_field_elements(input);
// Sponge construction matching CPU reference exactly:
// CPU processes field elements using push_to_buf() which absorbs in chunks of RATE=4
// Process first 24 elements (6 complete chunks of 4)
for (var chunk = 0u; chunk < 6u; chunk++) {
// Absorb 4 elements for this chunk
for (var i = 0u; i < 4u; i++) {
let felt_idx = chunk * 4u + i;
state[i] = gf_add(state[i], input_felts[felt_idx]);
}
// Permute after each complete chunk
poseidon2_permute(&state);
}
// Process remaining 1 element (partial chunk)
// Now we have element 24 (the padding marker = 1) remaining
// This simulates CPU's finalize_twice adding ONE to buffer position 0
state[0] = gf_add(state[0], input_felts[24u]); // Add the padding marker (should be 1)
// Add sponge padding marker (ONE) to the next position
state[1] = gf_add(state[1], gf_one());
// Final permutation (CPU calls permute after completing the block)
poseidon2_permute(&state);
// First squeeze - get first 4 field elements
let first_output = field_elements_to_bytes(array<GoldilocksField, 4>(
state[0], state[1], state[2], state[3]
));
// Second squeeze
poseidon2_permute(&state);
let second_output = field_elements_to_bytes(array<GoldilocksField, 4>(
state[0], state[1], state[2], state[3]
));
// Combine both squeezes into 64-byte output
var result: array<u32, 16>;
for (var i = 0u; i < 8u; i++) {
result[i] = first_output[i];
result[i + 8u] = second_output[i];
}
return result;
}
// Poseidon2 hash function for 64-byte input (used in double hash)
fn poseidon2_hash_squeeze_twice_64(input: array<u32, 16>) -> array<u32, 16> {
var state: array<GoldilocksField, 12>;
// Initialize state to zero
for (var i = 0u; i < 12u; i++) {
state[i] = gf_zero();
}
// Convert input to field elements
// 64 bytes = 16 u32s = 16 felts
// Padding adds 1 byte + 3 zeros = 4 bytes = 1 felt (value 1)
// Total 17 felts
// Process first 16 elements (4 complete chunks of 4)
for (var chunk = 0u; chunk < 4u; chunk++) {
// Absorb 4 elements for this chunk
for (var i = 0u; i < 4u; i++) {
let felt_idx = chunk * 4u + i;
state[i] = gf_add(state[i], gf_from_u32(input[felt_idx]));
}
// Permute after each complete chunk
poseidon2_permute(&state);
}
// Handle the last felt (padding marker = 1) and sponge padding
// The last felt is 1 (from bytes 1, 0, 0, 0)
state[0] = gf_add(state[0], gf_one());
// Add sponge padding marker (ONE) to the next position
state[1] = gf_add(state[1], gf_one());
// Final permutation
poseidon2_permute(&state);
// First squeeze
let first_output = field_elements_to_bytes(array<GoldilocksField, 4>(
state[0], state[1], state[2], state[3]
));
// Second squeeze
poseidon2_permute(&state);
let second_output = field_elements_to_bytes(array<GoldilocksField, 4>(
state[0], state[1], state[2], state[3]
));
// Combine both squeezes into 64-byte output
var result: array<u32, 16>;
for (var i = 0u; i < 8u; i++) {
result[i] = first_output[i];
result[i + 8u] = second_output[i];
}
return result;
}
// Double Poseidon2 hash (like Bitcoin's double SHA256)
fn double_hash(input: array<u32, 24>) -> array<u32, 16> {
let first_hash = poseidon2_hash_squeeze_twice(input);
return poseidon2_hash_squeeze_twice_64(first_hash);
}
// Check if hash < target (U512 comparison)
fn is_below_target(hash: array<u32, 16>, difficulty_tgt: array<u32, 16>) -> bool {
// Compare from most significant to least significant
for (var i = 0u; i < 16u; i++) {
if (hash[15u - i] < difficulty_tgt[15u - i]) {
return true;
} else if (hash[15u - i] > difficulty_tgt[15u - i]) {
return false;
}
}
return false; // Equal, not below
}
// Main mining kernel
@compute @workgroup_size(256)
fn mining_main(@builtin(global_invocation_id) global_id: vec3<u32>) {
// If solution already found, exit early
if (atomicLoad(&results[0]) != 0u) {
return;
}
let thread_id = global_id.x;
// Read dispatch configuration from buffer
let total_threads = dispatch_config[0]; // Total logical threads in this dispatch
let nonces_per_thread = dispatch_config[1]; // Nonces processed by each thread
let total_nonces = dispatch_config[2]; // Total logical nonces this dispatch should cover
// Guard against threads beyond configured total_threads
if (thread_id >= total_threads) {
return;
}
// Base logical index for this thread
let base_index = thread_id * nonces_per_thread;
// Work coarsening: each thread processes a contiguous block of nonces
for (var j = 0u; j < nonces_per_thread; j = j + 1u) {
let logical_index = base_index + j;
if (logical_index >= total_nonces) {
break;
}
// Check if solution already found (early exit for entire dispatch)
if (atomicLoad(&results[0]) != 0u) {
return;
}
// current_nonce = start_nonce + logical_index
var current_nonce: array<u32, 16>;
var carry: u32 = 0u;
// Add logical_index into the low limb and propagate carry through the U512 nonce
let val0 = start_nonce[0];
let sum0 = val0 + logical_index;
current_nonce[0] = sum0;
carry = select(0u, 1u, sum0 < val0);
// Propagate carry through remaining limbs
for (var i = 1u; i < 16u; i++) {
let val = start_nonce[i];
let sum = val + carry;
current_nonce[i] = sum;
carry = select(0u, 1u, sum < val);
}
// Construct input (96 bytes = 24 u32s)
// Header (32 bytes = 8 u32s) followed by Nonce (64 bytes = 16 u32s)
var input: array<u32, 24>;
for (var i = 0u; i < 8u; i++) {
input[i] = header[i];
}
// Nonce needs to be Big Endian in the byte stream for hashing.
// current_nonce is Little Endian words.
for (var i = 0u; i < 16u; i++) {
let val = current_nonce[15u - i];
// Reverse bytes
let rev = ((val & 0xFFu) << 24u) |
((val & 0xFF00u) << 8u) |
((val & 0xFF0000u) >> 8u) |
((val & 0xFF000000u) >> 24u);
input[8u + i] = rev;
}
// Hash (Big Endian)
let hash_be = double_hash(input);
// Convert to Little Endian for difficulty check and storage
var hash_le: array<u32, 16>;
for (var i = 0u; i < 16u; i++) {
let val = hash_be[15u - i];
// Reverse bytes in u32
hash_le[i] = ((val & 0xFFu) << 24u) |
((val & 0xFF00u) << 8u) |
((val & 0xFF0000u) >> 8u) |
((val & 0xFF000000u) >> 24u);
}
// Check target
if (is_below_target(hash_le, difficulty_target)) {
// Try to claim the solution
if (atomicExchange(&results[0], 1u) == 0u) {
// We won! Write nonce and hash
// results layout: [0]=found, [1..16]=nonce, [17..32]=hash
for (var i = 0u; i < 16u; i++) {
atomicStore(&results[1u + i], current_nonce[i]);
atomicStore(&results[17u + i], hash_le[i]);
}
}
return; // Exit loop after finding solution
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -31,11 +31,7 @@ use prometheus::{
};
#[cfg(feature = "http-exporter")]
use {
anyhow::Result,
std::net::SocketAddr,
warp::{http::Response, Filter},
};
use {anyhow::Result, std::net::SocketAddr, warp::Filter};
#[cfg(not(feature = "http-exporter"))]
use anyhow::Result;
@@ -341,7 +337,7 @@ static JOB_FOUND_ORIGIN: Lazy<GaugeVec> = Lazy::new(|| {
let g = GaugeVec::new(
opts!(
"miner_job_found_origin",
"Job found origin gauge (set to 1 for the origin that found the candidate)"
"Per-job found candidate origin (0=unknown, 1=cpu, 2=gpu-g1, 3=gpu-g2)"
),
&["engine", "job_id", "origin"],
)
@@ -352,6 +348,67 @@ static JOB_FOUND_ORIGIN: Lazy<GaugeVec> = Lazy::new(|| {
g
});
// -------------------------------------------------------------------------------------
// GPU-specific metrics
// -------------------------------------------------------------------------------------
static GPU_DEVICE_COUNT: Lazy<IntGauge> = Lazy::new(|| {
let g = IntGauge::new(
"miner_gpu_devices_total",
"Number of GPU devices available for mining",
)
.expect("create miner_gpu_devices_total");
REGISTRY
.register(Box::new(g.clone()))
.expect("register miner_gpu_devices_total");
g
});
static GPU_DEVICE_INFO: Lazy<GaugeVec> = Lazy::new(|| {
let g = GaugeVec::new(
opts!(
"miner_gpu_device_info",
"GPU device information (label-only gauge set to 1). Labels: device_id, name, backend, vendor, device_type"
),
&["device_id", "name", "backend", "vendor", "device_type"],
)
.expect("create miner_gpu_device_info");
REGISTRY
.register(Box::new(g.clone()))
.expect("register miner_gpu_device_info");
g
});
static GPU_BATCH_SIZE: Lazy<GaugeVec> = Lazy::new(|| {
let g = GaugeVec::new(
opts!(
"miner_gpu_batch_size",
"Current GPU batch size (hashes per batch) per device"
),
&["device_id"],
)
.expect("create miner_gpu_batch_size");
REGISTRY
.register(Box::new(g.clone()))
.expect("register miner_gpu_batch_size");
g
});
static GPU_WORKGROUPS: Lazy<GaugeVec> = Lazy::new(|| {
let g = GaugeVec::new(
opts!(
"miner_gpu_workgroups",
"Number of GPU workgroups dispatched per device"
),
&["device_id"],
)
.expect("create miner_gpu_workgroups");
REGISTRY
.register(Box::new(g.clone()))
.expect("register miner_gpu_workgroups");
g
});
pub fn default_registry() -> &'static Registry {
&REGISTRY
}
@@ -647,6 +704,42 @@ pub fn remove_thread_hash_rate(engine: &str, job_id: &str, thread_id: &str) {
let _ = THREAD_HASH_RATE.remove_label_values(&[engine, job_id, thread_id]);
}
// -------------------------------------------------------------------------------------
// GPU-specific metric helpers
// -------------------------------------------------------------------------------------
/// Set the total number of GPU devices available for mining
pub fn set_gpu_device_count(count: i64) {
GPU_DEVICE_COUNT.set(count);
}
/// Set GPU device information (call once per device during initialization)
pub fn set_gpu_device_info(
device_id: &str,
name: &str,
backend: &str,
vendor: &str,
device_type: &str,
) {
GPU_DEVICE_INFO
.with_label_values(&[device_id, name, backend, vendor, device_type])
.set(1.0);
}
/// Set GPU batch size for a specific device
pub fn set_gpu_batch_size(device_id: &str, batch_size: f64) {
GPU_BATCH_SIZE
.with_label_values(&[device_id])
.set(batch_size);
}
/// Set GPU workgroup count for a specific device
pub fn set_gpu_workgroups(device_id: &str, workgroups: f64) {
GPU_WORKGROUPS
.with_label_values(&[device_id])
.set(workgroups);
}
// -------------------------------------------------------------------------------------
// HTTP Exporter (feature: http-exporter)
// -------------------------------------------------------------------------------------
@@ -657,6 +750,7 @@ pub fn remove_thread_hash_rate(engine: &str, job_id: &str, thread_id: &str) {
/// - Spawns the exporter as a background task and returns immediately.
/// - Serves plaintext metrics at GET /metrics.
/// - If called multiple times, multiple servers may be created (call once).
#[cfg(feature = "http-exporter")]
pub async fn start_http_exporter(port: u16) -> Result<()> {
// Encoder is created inside the handler to avoid capturing non-Clone state
// Use REGISTRY.gather() directly in the handler
@@ -670,7 +764,7 @@ pub async fn start_http_exporter(port: u16) -> Result<()> {
.encode(&metric_families, &mut buffer)
.unwrap_or_default();
Response::builder()
warp::http::Response::builder()
.header("Content-Type", encoder.format_type())
.body(String::from_utf8(buffer).unwrap_or_default())
});

View File

@@ -7,7 +7,7 @@ description = "CLI binary to run the Quantus External Miner service"
[features]
default = []
cuda = ["miner-service/cuda"]
gpu = ["miner-service/gpu"]
[dependencies]
miner-service = { path = "../miner-service" }
@@ -15,6 +15,11 @@ clap = { workspace = true, features = ["derive", "env"] }
tokio = { workspace = true, features = ["full"] }
env_logger = { workspace = true }
log = { workspace = true }
engine-cpu = { path = "../engine-cpu" }
engine-gpu = { path = "../engine-gpu" }
primitive-types = { workspace = true }
rand = { workspace = true }
num_cpus = { workspace = true }
[[bin]]
name = "quantus-miner"

View File

@@ -1,163 +1,290 @@
use clap::{Parser, ValueEnum};
use miner_service::{run, EngineSelection, ServiceConfig};
use clap::{Parser, Subcommand};
use engine_cpu::{EngineRange, MinerEngine};
use miner_service::{run, ServiceConfig};
use primitive_types::U512;
use rand::RngCore;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};
#[derive(Subcommand, Debug)]
#[allow(clippy::large_enum_variant)]
enum Command {
/// Run the mining service (default behavior)
Serve {
/// Port number to listen on for the miner HTTP API
#[arg(short, long, env = "MINER_PORT", default_value_t = 9833)]
port: u16,
/// Number of CPU worker threads to use for mining (None = auto-detect)
#[arg(long = "cpu-workers", env = "MINER_CPU_WORKERS")]
cpu_workers: Option<usize>,
/// Number of GPU worker threads to use for mining (None = auto-detect)
#[arg(long = "gpu-workers", env = "MINER_GPU_WORKERS")]
gpu_workers: Option<usize>,
/// Optional Prometheus metrics exporter port; if omitted, metrics are disabled
#[arg(long, env = "MINER_METRICS_PORT")]
metrics_port: Option<u16>,
/// Enable verbose logging (shows debug info, progress details, etc.)
#[arg(short, long, env = "MINER_VERBOSE")]
verbose: bool,
/// How often to report mining progress (in milliseconds).
/// Smaller values give more frequent updates but slightly reduce performance.
#[arg(long = "progress-interval-ms", env = "MINER_PROGRESS_INTERVAL_MS")]
progress_interval_ms: Option<u64>,
/// Size of work chunks to process before reporting progress (number of hashes).
/// If omitted, uses engine-specific defaults (200K for CPU, 100M for GPU).
#[arg(long = "chunk-size", env = "MINER_CHUNK_SIZE")]
chunk_size: Option<u64>,
/// For cpu-chain-manipulator: start throttle index at this many solved blocks
/// to "pick up where we left off" after restarts.
#[arg(long = "manip-solved-blocks", env = "MINER_MANIP_SOLVED_BLOCKS")]
manip_solved_blocks: Option<u64>,
/// For cpu-chain-manipulator: base sleep per batch in nanoseconds (default 500_000 ns)
#[arg(long = "manip-base-delay-ns", env = "MINER_MANIP_BASE_DELAY_NS")]
manip_base_delay_ns: Option<u64>,
/// For cpu-chain-manipulator: number of nonce attempts between sleeps (default 10_000)
#[arg(long = "manip-step-batch", env = "MINER_MANIP_STEP_BATCH")]
manip_step_batch: Option<u64>,
/// For cpu-chain-manipulator: optional cap on solved-blocks throttle index
#[arg(long = "manip-throttle-cap", env = "MINER_MANIP_THROTTLE_CAP")]
manip_throttle_cap: Option<u64>,
/// Telemetry endpoints (repeat --telemetry-endpoint or comma-separated)
#[arg(long = "telemetry-endpoint", env = "MINER_TELEMETRY_ENDPOINTS", value_delimiter = ',', num_args = 0.., value_name = "URL")]
telemetry_endpoints: Option<Vec<String>>,
/// Enable or disable telemetry explicitly
#[arg(long = "telemetry-enabled", env = "MINER_TELEMETRY_ENABLED")]
telemetry_enabled: Option<bool>,
/// Telemetry verbosity level (0..=4 typical)
#[arg(long = "telemetry-verbosity", env = "MINER_TELEMETRY_VERBOSITY")]
telemetry_verbosity: Option<u8>,
/// Interval seconds for system.interval messages
#[arg(
long = "telemetry-interval-secs",
env = "MINER_TELEMETRY_INTERVAL_SECS"
)]
telemetry_interval_secs: Option<u64>,
/// Default association: chain name
#[arg(long = "telemetry-chain", env = "MINER_TELEMETRY_CHAIN")]
telemetry_chain: Option<String>,
/// Default association: genesis hash (hex)
#[arg(long = "telemetry-genesis", env = "MINER_TELEMETRY_GENESIS")]
telemetry_genesis: Option<String>,
/// Default association: node telemetry id
#[arg(long = "telemetry-node-id", env = "MINER_TELEMETRY_NODE_ID")]
telemetry_node_id: Option<String>,
/// Default association: node libp2p peer id
#[arg(long = "telemetry-node-peer-id", env = "MINER_TELEMETRY_NODE_PEER_ID")]
telemetry_node_peer_id: Option<String>,
/// Default association: node name
#[arg(long = "telemetry-node-name", env = "MINER_TELEMETRY_NODE_NAME")]
telemetry_node_name: Option<String>,
/// Default association: node version
#[arg(long = "telemetry-node-version", env = "MINER_TELEMETRY_NODE_VERSION")]
telemetry_node_version: Option<String>,
},
/// Run a quick benchmark of the mining engines
Benchmark {
/// Number of CPU workers to use for benchmark
#[arg(long = "cpu-workers", env = "MINER_CPU_WORKERS")]
cpu_workers: Option<usize>,
/// Number of GPU workers to use for benchmark
#[arg(long = "gpu-workers", env = "MINER_GPU_WORKERS")]
gpu_workers: Option<usize>,
/// Benchmark duration in seconds (default: 10)
#[arg(short, long, default_value_t = 10)]
duration: u64,
/// Enable verbose logging during benchmark
#[arg(short, long, env = "MINER_VERBOSE")]
verbose: bool,
},
}
/// Quantus External Miner CLI
#[derive(Parser, Debug)]
#[command(author, version = option_env!("MINER_VERSION").unwrap_or(env!("CARGO_PKG_VERSION")), about, long_about = None)]
struct Args {
/// Port number to listen on for the miner HTTP API
#[arg(short, long, env = "MINER_PORT", default_value_t = 9833)]
port: u16,
/// Number of worker threads (logical CPUs) to use for mining (defaults to all available)
#[arg(long = "workers", env = "MINER_WORKERS")]
workers: Option<usize>,
/// Optional Prometheus metrics exporter port; if omitted, metrics are disabled
#[arg(long, env = "MINER_METRICS_PORT")]
metrics_port: Option<u16>,
/// Target milliseconds for per-thread progress updates (chunking).
/// Smaller values increase metrics freshness but add a bit of overhead.
#[arg(long = "progress-chunk-ms", env = "MINER_PROGRESS_CHUNK_MS")]
progress_chunk_ms: Option<u64>,
/// For cpu-chain-manipulator: start throttle index at this many solved blocks
/// to "pick up where we left off" after restarts.
#[arg(long = "manip-solved-blocks", env = "MINER_MANIP_SOLVED_BLOCKS")]
manip_solved_blocks: Option<u64>,
/// For cpu-chain-manipulator: base sleep per batch in nanoseconds (default 500_000 ns)
#[arg(long = "manip-base-delay-ns", env = "MINER_MANIP_BASE_DELAY_NS")]
manip_base_delay_ns: Option<u64>,
/// For cpu-chain-manipulator: number of nonce attempts between sleeps (default 10_000)
#[arg(long = "manip-step-batch", env = "MINER_MANIP_STEP_BATCH")]
manip_step_batch: Option<u64>,
/// For cpu-chain-manipulator: optional cap on solved-blocks throttle index
#[arg(long = "manip-throttle-cap", env = "MINER_MANIP_THROTTLE_CAP")]
manip_throttle_cap: Option<u64>,
/// Mining engine to use (default: cpu-fast).
/// Options: cpu-baseline, cpu-fast, cpu-chain-manipulator, gpu-cuda, gpu-opencl
/// Note: GPU engines are currently unimplemented and will return a clear error at runtime.
#[arg(long, env = "MINER_ENGINE", value_enum, default_value_t = EngineCli::CpuFast)]
engine: EngineCli,
/// Telemetry endpoints (repeat --telemetry-endpoint or comma-separated)
#[arg(long = "telemetry-endpoint", env = "MINER_TELEMETRY_ENDPOINTS", value_delimiter = ',', num_args = 0.., value_name = "URL")]
telemetry_endpoints: Option<Vec<String>>,
/// Enable or disable telemetry explicitly
#[arg(long = "telemetry-enabled", env = "MINER_TELEMETRY_ENABLED")]
telemetry_enabled: Option<bool>,
/// Telemetry verbosity level (0..=4 typical)
#[arg(long = "telemetry-verbosity", env = "MINER_TELEMETRY_VERBOSITY")]
telemetry_verbosity: Option<u8>,
/// Interval seconds for system.interval messages
#[arg(
long = "telemetry-interval-secs",
env = "MINER_TELEMETRY_INTERVAL_SECS"
)]
telemetry_interval_secs: Option<u64>,
/// Default association: chain name
#[arg(long = "telemetry-chain", env = "MINER_TELEMETRY_CHAIN")]
telemetry_chain: Option<String>,
/// Default association: genesis hash (hex)
#[arg(long = "telemetry-genesis", env = "MINER_TELEMETRY_GENESIS")]
telemetry_genesis: Option<String>,
/// Default association: node telemetry id
#[arg(long = "telemetry-node-id", env = "MINER_TELEMETRY_NODE_ID")]
telemetry_node_id: Option<String>,
/// Default association: node libp2p peer id
#[arg(long = "telemetry-node-peer-id", env = "MINER_TELEMETRY_NODE_PEER_ID")]
telemetry_node_peer_id: Option<String>,
/// Default association: node name
#[arg(long = "telemetry-node-name", env = "MINER_TELEMETRY_NODE_NAME")]
telemetry_node_name: Option<String>,
/// Default association: node version
#[arg(long = "telemetry-node-version", env = "MINER_TELEMETRY_NODE_VERSION")]
telemetry_node_version: Option<String>,
#[command(subcommand)]
command: Option<Command>,
}
#[allow(clippy::enum_variant_names)]
#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
enum EngineCli {
/// Baseline CPU engine (reference implementation)
CpuBaseline,
/// Optimized CPU engine (incremental precompute + step_mul)
CpuFast,
/// Throttling CPU engine that slows per block to help reduce difficulty
CpuChainManipulator,
/// CUDA GPU engine (unimplemented; selecting will return an error)
GpuCuda,
/// OpenCL GPU engine (unimplemented; selecting will return an error)
GpuOpencl,
}
impl From<EngineCli> for EngineSelection {
fn from(value: EngineCli) -> Self {
match value {
EngineCli::CpuBaseline => EngineSelection::CpuBaseline,
EngineCli::CpuFast => EngineSelection::CpuFast,
EngineCli::CpuChainManipulator => EngineSelection::CpuChainManipulator,
EngineCli::GpuCuda => EngineSelection::GpuCuda,
EngineCli::GpuOpencl => EngineSelection::GpuOpenCl,
}
}
}
#[tokio::main]
async fn main() {
let args = Args::parse();
match args.command.unwrap_or(Command::Serve {
port: 9833,
cpu_workers: None,
gpu_workers: None,
metrics_port: None,
verbose: false,
progress_interval_ms: None,
chunk_size: None,
manip_solved_blocks: None,
manip_base_delay_ns: None,
manip_step_batch: None,
manip_throttle_cap: None,
telemetry_endpoints: None,
telemetry_enabled: None,
telemetry_verbosity: None,
telemetry_interval_secs: None,
telemetry_chain: None,
telemetry_genesis: None,
telemetry_node_id: None,
telemetry_node_peer_id: None,
telemetry_node_name: None,
telemetry_node_version: None,
}) {
Command::Serve {
port,
cpu_workers,
gpu_workers,
metrics_port,
verbose,
progress_interval_ms,
chunk_size,
manip_solved_blocks,
manip_base_delay_ns,
manip_step_batch,
manip_throttle_cap,
telemetry_endpoints,
telemetry_enabled,
telemetry_verbosity,
telemetry_interval_secs,
telemetry_chain,
telemetry_genesis,
telemetry_node_id,
telemetry_node_peer_id,
telemetry_node_name,
telemetry_node_version,
} => {
run_serve_command(
port,
cpu_workers,
gpu_workers,
metrics_port,
verbose,
progress_interval_ms,
chunk_size,
manip_solved_blocks,
manip_base_delay_ns,
manip_step_batch,
manip_throttle_cap,
telemetry_endpoints,
telemetry_enabled,
telemetry_verbosity,
telemetry_interval_secs,
telemetry_chain,
telemetry_genesis,
telemetry_node_id,
telemetry_node_peer_id,
telemetry_node_name,
telemetry_node_version,
)
.await;
}
Command::Benchmark {
cpu_workers,
gpu_workers,
duration,
verbose,
} => {
run_benchmark_command(cpu_workers, gpu_workers, duration, verbose).await;
}
}
}
#[allow(clippy::too_many_arguments)]
async fn run_serve_command(
port: u16,
cpu_workers: Option<usize>,
gpu_workers: Option<usize>,
metrics_port: Option<u16>,
verbose: bool,
progress_interval_ms: Option<u64>,
chunk_size: Option<u64>,
manip_solved_blocks: Option<u64>,
manip_base_delay_ns: Option<u64>,
manip_step_batch: Option<u64>,
manip_throttle_cap: Option<u64>,
telemetry_endpoints: Option<Vec<String>>,
telemetry_enabled: Option<bool>,
telemetry_verbosity: Option<u8>,
telemetry_interval_secs: Option<u64>,
telemetry_chain: Option<String>,
telemetry_genesis: Option<String>,
telemetry_node_id: Option<String>,
telemetry_node_peer_id: Option<String>,
telemetry_node_name: Option<String>,
telemetry_node_version: Option<String>,
) {
// Initialize logger early to capture startup messages.
// If RUST_LOG is not set, default to info level for our app.
// If RUST_LOG is not set, default to appropriate level based on verbose flag
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "info");
let log_level = if verbose {
"debug,miner=debug,gpu_engine=debug,engine_cpu=debug"
} else {
"info,miner=info,gpu_engine=info"
};
std::env::set_var("RUST_LOG", log_level);
}
env_logger::init();
// Telemetry CLI passthrough to env for miner-service bootstrap
if let Some(eps) = args.telemetry_endpoints.as_ref() {
if let Some(eps) = telemetry_endpoints.as_ref() {
if !eps.is_empty() {
std::env::set_var("MINER_TELEMETRY_ENDPOINTS", eps.join(","));
}
}
if let Some(v) = args.telemetry_enabled {
if let Some(v) = telemetry_enabled {
std::env::set_var("MINER_TELEMETRY_ENABLED", if v { "1" } else { "0" });
}
if let Some(v) = args.telemetry_verbosity {
if let Some(v) = telemetry_verbosity {
std::env::set_var("MINER_TELEMETRY_VERBOSITY", v.to_string());
}
if let Some(v) = args.telemetry_interval_secs {
if let Some(v) = telemetry_interval_secs {
std::env::set_var("MINER_TELEMETRY_INTERVAL_SECS", v.to_string());
}
if let Some(v) = args.telemetry_chain.as_ref() {
if let Some(v) = telemetry_chain.as_ref() {
std::env::set_var("MINER_TELEMETRY_CHAIN", v);
}
if let Some(v) = args.telemetry_genesis.as_ref() {
if let Some(v) = telemetry_genesis.as_ref() {
std::env::set_var("MINER_TELEMETRY_GENESIS", v);
}
if let Some(v) = args.telemetry_node_id.as_ref() {
if let Some(v) = telemetry_node_id.as_ref() {
std::env::set_var("MINER_TELEMETRY_NODE_ID", v);
}
if let Some(v) = args.telemetry_node_peer_id.as_ref() {
if let Some(v) = telemetry_node_peer_id.as_ref() {
std::env::set_var("MINER_TELEMETRY_NODE_PEER_ID", v);
}
if let Some(v) = args.telemetry_node_name.as_ref() {
if let Some(v) = telemetry_node_name.as_ref() {
std::env::set_var("MINER_TELEMETRY_NODE_NAME", v);
}
if let Some(v) = args.telemetry_node_version.as_ref() {
if let Some(v) = telemetry_node_version.as_ref() {
std::env::set_var("MINER_TELEMETRY_NODE_VERSION", v);
}
@@ -165,15 +292,16 @@ async fn main() {
log::info!("Starting external miner service...");
let config = ServiceConfig {
port: args.port,
workers: args.workers,
metrics_port: args.metrics_port,
progress_chunk_ms: args.progress_chunk_ms,
manip_solved_blocks: args.manip_solved_blocks,
manip_base_delay_ns: args.manip_base_delay_ns,
manip_step_batch: args.manip_step_batch,
manip_throttle_cap: args.manip_throttle_cap,
engine: args.engine.into(),
port,
cpu_workers,
gpu_workers,
metrics_port,
progress_interval_ms,
chunk_size,
manip_solved_blocks,
manip_base_delay_ns,
manip_step_batch,
manip_throttle_cap,
};
log::info!("Effective config: {config}");
@@ -182,3 +310,225 @@ async fn main() {
std::process::exit(1);
}
}
async fn run_benchmark_command(
cpu_workers: Option<usize>,
gpu_workers: Option<usize>,
duration: u64,
verbose: bool,
) {
// Initialize logger early to capture startup messages.
if std::env::var("RUST_LOG").is_err() {
let log_level = if verbose {
"debug,miner=debug,gpu_engine=debug,engine_cpu=debug"
} else {
"info,miner=info,gpu_engine=warn,engine_cpu=info"
};
std::env::set_var("RUST_LOG", log_level);
}
env_logger::init();
let effective_cpu_workers = cpu_workers.unwrap_or_else(num_cpus::get);
let effective_gpu_workers = gpu_workers.unwrap_or(0);
let total_workers = effective_cpu_workers + effective_gpu_workers;
println!("🚀 Quantus Miner Benchmark");
println!("==========================");
println!(
"Configuration: {} CPU workers, {} GPU workers",
effective_cpu_workers, effective_gpu_workers
);
println!("Duration: {} seconds", duration);
println!("Total Workers: {}", total_workers);
println!("Available CPUs: {}", num_cpus::get());
// Create engines based on configuration
let cpu_engine = if effective_cpu_workers > 0 {
Some(Arc::new(engine_cpu::FastCpuEngine::new()) as Arc<dyn MinerEngine>)
} else {
None
};
let gpu_engine = if effective_gpu_workers > 0 {
#[cfg(feature = "gpu")]
{
Some(Arc::new(engine_gpu::GpuEngine::new()) as Arc<dyn MinerEngine>)
}
#[cfg(not(feature = "gpu"))]
{
eprintln!("Error: GPU workers requested but this binary was built without GPU support");
std::process::exit(1);
}
} else {
None
};
// For benchmark, we'll use CPU engine as primary for simplicity
let engine = cpu_engine
.or(gpu_engine)
.expect("At least one engine must be available");
if total_workers == 0 {
eprintln!("Error: No workers specified");
std::process::exit(1);
}
println!("Workers: {}", total_workers);
println!();
// Run benchmark
let cancel_flag = Arc::new(AtomicBool::new(false));
let benchmark_start = Instant::now();
// Create a large range that should take the full duration
let benchmark_range = EngineRange {
start: U512::from(0u64),
end: U512::from(100_000_000u64), // 100M nonces - should be plenty
};
let mut header = [0u8; 32];
rand::thread_rng().fill_bytes(&mut header);
let difficulty = U512::MAX; // High difficulty - no solutions expected
let ctx = engine.prepare_context(header, difficulty);
println!("⛏️ Starting benchmark...");
// Spawn worker threads
let mut handles = Vec::new();
let total_hashes_arc = Arc::new(std::sync::Mutex::new(0u64));
// Use larger ranges for GPU (1M) vs CPU (10K)
let nonces_per_worker = if effective_gpu_workers > 0 {
1_000_000u64 // 1M nonces per GPU worker
} else {
10_000u64 // 10K nonces per CPU worker
};
for worker_id in 0..total_workers {
let engine = engine.clone();
let ctx = ctx.clone();
let cancel_flag = cancel_flag.clone();
let total_hashes = total_hashes_arc.clone();
let handle = thread::spawn(move || {
let worker_range = EngineRange {
start: benchmark_range.start.saturating_add(
U512::from(worker_id as u64).saturating_mul(U512::from(nonces_per_worker)),
),
end: benchmark_range
.start
.saturating_add(
U512::from((worker_id + 1) as u64)
.saturating_mul(U512::from(nonces_per_worker)),
)
.saturating_sub(U512::from(1u64)),
};
let mut worker_hashes = 0u64;
loop {
if cancel_flag.load(std::sync::atomic::Ordering::Relaxed) {
break;
}
let result = engine.search_range(&ctx, worker_range.clone(), &cancel_flag);
match result {
engine_cpu::EngineStatus::Found { hash_count, .. }
| engine_cpu::EngineStatus::Exhausted { hash_count }
| engine_cpu::EngineStatus::Cancelled { hash_count } => {
worker_hashes += hash_count;
*total_hashes.lock().unwrap() += hash_count;
}
engine_cpu::EngineStatus::Running { .. } => {}
}
// Check if we've exceeded the time limit
if benchmark_start.elapsed() >= Duration::from_secs(duration) {
break;
}
}
worker_hashes
});
handles.push(handle);
}
// Wait for duration or interrupt
let mut last_update = Instant::now();
loop {
tokio::time::sleep(Duration::from_millis(100)).await;
if benchmark_start.elapsed() >= Duration::from_secs(duration) {
cancel_flag.store(true, std::sync::atomic::Ordering::Relaxed);
break;
}
// Update progress every second
if last_update.elapsed() >= Duration::from_secs(1) {
let current_hashes = *total_hashes_arc.lock().unwrap();
let elapsed = benchmark_start.elapsed().as_secs_f64();
if current_hashes > 0 {
let hash_rate = current_hashes as f64 / elapsed;
let hash_rate_str = if hash_rate >= 1_000_000.0 {
format!("{:.1}M", hash_rate / 1_000_000.0)
} else if hash_rate >= 1_000.0 {
format!("{:.1}K", hash_rate / 1_000.0)
} else {
format!("{:.0}", hash_rate)
};
println!("⏱️ {:.1}s - {} H/s", elapsed, hash_rate_str);
} else if effective_gpu_workers > 0 {
println!("⏱️ {:.1}s - processing...", elapsed);
} else {
println!("⏱️ {:.1}s - starting...", elapsed);
}
last_update = Instant::now();
}
}
// Wait for all threads to finish
for handle in handles {
let _ = handle.join();
}
let total_elapsed = benchmark_start.elapsed();
let final_hashes = *total_hashes_arc.lock().unwrap();
let avg_hash_rate = final_hashes as f64 / total_elapsed.as_secs_f64();
println!();
println!("📊 Benchmark Results");
println!("===================");
println!("Total time: {:.2} seconds", total_elapsed.as_secs_f64());
println!("Total hashes: {}", final_hashes);
let hash_rate_str = if avg_hash_rate >= 1_000_000.0 {
format!("{:.2}M H/s", avg_hash_rate / 1_000_000.0)
} else if avg_hash_rate >= 1_000.0 {
format!("{:.2}K H/s", avg_hash_rate / 1_000.0)
} else {
format!("{:.0} H/s", avg_hash_rate)
};
println!("Average hash rate: {}", hash_rate_str);
if total_workers > 1 {
let per_worker_rate = avg_hash_rate / total_workers as f64;
let per_worker_str = if per_worker_rate >= 1_000_000.0 {
format!("{:.2}M H/s", per_worker_rate / 1_000_000.0)
} else if per_worker_rate >= 1_000.0 {
format!("{:.2}K H/s", per_worker_rate / 1_000.0)
} else {
format!("{:.0} H/s", per_worker_rate)
};
println!(
"Per-worker rate: {} (across {} workers)",
per_worker_str, total_workers
);
}
println!("✅ Benchmark completed successfully!");
}

View File

@@ -9,15 +9,13 @@ description = "Service layer: HTTP API, job orchestration, and engine abstractio
default = ["cpu", "metrics"]
# Enable CPU engine by default.
cpu = ["engine-cpu"]
# Enable GPU engine.
gpu = ["engine-gpu"]
# Optional metrics/observability (Prometheus endpoint).
metrics = [
"dep:metrics",
"engine-gpu-cuda?/metrics",
"metrics/http-exporter",
]
# Optional GPU backends (off by default).
cuda = ["dep:engine-gpu-cuda", "engine-gpu-cuda/cuda"]
opencl = ["engine-gpu-opencl"]
[dependencies]
# Workspace-shared deps
@@ -32,6 +30,9 @@ crossbeam-channel = { workspace = true }
num_cpus = { workspace = true }
thiserror = { workspace = true }
anyhow = { workspace = true }
reqwest = { version = "0.11", features = ["json"] }
quinn = "0.10"
rustls = { version = "0.21", default-features = false, features = ["dangerous_configuration", "quic"] }
# Protocol types from the node repo (keep API compatibility)
quantus-miner-api = { workspace = true }
@@ -39,7 +40,6 @@ quantus-miner-api = { workspace = true }
# Local crates
pow-core = { path = "../pow-core" }
engine-cpu = { path = "../engine-cpu", optional = true }
engine-gpu-cuda = { path = "../engine-gpu-cuda", optional = true, features = ["cuda"] }
engine-gpu-opencl = { path = "../engine-gpu-opencl", optional = true }
engine-gpu = { path = "../engine-gpu", optional = true }
metrics = { path = "../metrics", optional = true }
miner-telemetry = { path = "../miner-telemetry" }

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ This directory contains a production-friendly systemd unit and drop-in overrides
Contents
- quantus-miner.service
- A baseline unit file that runs the miner with journald logging and sensible security hardening.
- A service unit file that runs the miner with journald logging and sensible security hardening.
- Uses environment variables for configuration (preferred for stable ExecStart).
- overrides/
- 10-shared-hardware.conf
@@ -31,7 +31,7 @@ Install (unit)
- RHEL/CentOS/Fed: sudoedit /etc/sysconfig/quantus-miner
Common variables (examples):
MINER_ENGINE=cpu-fast
MINER_ENGINE=cpu
MINER_PORT=9833
MINER_METRICS_PORT=9900 # enable Prometheus exporter
MINER_WORKERS=4 # leave unset to use default (50% of effective CPUs)
@@ -66,8 +66,8 @@ Dedicated hardware (miner only)
Configuration reference (environment variables)
- MINER_ENGINE
- cpu-fast (default), cpu-baseline, cpu-chain-manipulator
- gpu-cuda, gpu-opencl are placeholders; selecting them fails with a clear error.
- cpu (default), cpu-chain-manipulator
- gpu for high-performance GPU mining
- MINER_PORT
- HTTP API port (default 9833)
- MINER_METRICS_PORT