Compare commits
128 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a60d95c825 | ||
|
|
417a3efd86 | ||
|
|
443f0f9f89 | ||
|
|
0a07bb2de5 | ||
|
|
35df5a3554 | ||
|
|
bf8721f41d | ||
|
|
590ce82440 | ||
|
|
689e5f9bfd | ||
|
|
059fa8322c | ||
|
|
8295d27d25 | ||
|
|
e817c1a5e3 | ||
|
|
a8ecc6f03c | ||
|
|
91170c6aea | ||
|
|
4131b60ebb | ||
|
|
de3ffa9fb9 | ||
|
|
ad46f200c0 | ||
|
|
d14ebdf65c | ||
|
|
6834dd2dff | ||
|
|
af5564796b | ||
|
|
ffc5b8f83c | ||
|
|
3a694ecee1 | ||
|
|
b9e080e20c | ||
|
|
3705fa96e7 | ||
|
|
c3e7d70a75 | ||
|
|
0b0731b869 | ||
|
|
3b5f759fbd | ||
|
|
06933d5745 | ||
|
|
96853cad23 | ||
|
|
010eff6e71 | ||
|
|
4e7280cb0f | ||
|
|
c2773cb80d | ||
|
|
b79a9368e0 | ||
|
|
f0aa971326 | ||
|
|
23c617bda9 | ||
|
|
2a36eeb3c3 | ||
|
|
a548f74f7d | ||
|
|
97ec1350de | ||
|
|
ac87758996 | ||
|
|
bdcd939bc9 | ||
|
|
a51df35ca1 | ||
|
|
41579e8223 | ||
|
|
516286836a | ||
|
|
63a1d1f2d4 | ||
|
|
ad2d2c90ba | ||
|
|
b3cef7bab8 | ||
|
|
a97d79632e | ||
|
|
51ba298a68 | ||
|
|
3ae66edf13 | ||
|
|
5a6de314ab | ||
|
|
3ca29b6d8a | ||
|
|
c276a1173e | ||
|
|
6ae4658209 | ||
|
|
f7209be81b | ||
|
|
b4999ab448 | ||
|
|
1eaea91909 | ||
|
|
52318aeaad | ||
|
|
22fecf183e | ||
|
|
0978d3bebd | ||
|
|
8e073c110a | ||
|
|
c13b0027f3 | ||
|
|
3cb91c4798 | ||
|
|
43e857dfba | ||
|
|
83e0c9edce | ||
|
|
1509e2283b | ||
|
|
d423b5c1b1 | ||
|
|
6c7835f348 | ||
|
|
46202989df | ||
|
|
09b63783e0 | ||
|
|
e8e0d6acf4 | ||
|
|
14022a367f | ||
|
|
587303b480 | ||
|
|
9c88ac7aff | ||
|
|
ffe58596d4 | ||
|
|
0ab3049703 | ||
|
|
30c3b3f1a7 | ||
|
|
ccca629c49 | ||
|
|
5705d7fb1a | ||
|
|
9bb8c3cb3c | ||
|
|
44e62ce2f4 | ||
|
|
ded099d8bd | ||
|
|
0ebb6c4058 | ||
|
|
d8ab215b94 | ||
|
|
0850cb9c92 | ||
|
|
0bbeee332d | ||
|
|
57814d50df | ||
|
|
aa1cf2bb46 | ||
|
|
66d75b9d9f | ||
|
|
1a34013d0b | ||
|
|
765a51553e | ||
|
|
9f35010ce4 | ||
|
|
09be940f14 | ||
|
|
aec1e76dfc | ||
|
|
d0adeb5cf6 | ||
|
|
da19865de8 | ||
|
|
16c400dfaa | ||
|
|
7d4f126bc7 | ||
|
|
a91eaf6f32 | ||
|
|
675e8564da | ||
|
|
8d18ad1fb1 | ||
|
|
cf50bc6cb8 | ||
|
|
9d70218fa9 | ||
|
|
4a6c8936b3 | ||
|
|
021683b20e | ||
|
|
2d3c940432 | ||
|
|
79e30ce9a2 | ||
|
|
82793591a6 | ||
|
|
a83d5e32a4 | ||
|
|
b89cd1af72 | ||
|
|
ed89a1f1d2 | ||
|
|
12a2c94717 | ||
|
|
a8e7bc669f | ||
|
|
067abe7c18 | ||
|
|
46282c3852 | ||
|
|
7aba4eac70 | ||
|
|
4d0a8c425a | ||
|
|
45e0d7d384 | ||
|
|
36cb0efb8a | ||
|
|
93696a5f4b | ||
|
|
e7156dd890 | ||
|
|
9635482098 | ||
|
|
c214aa9d47 | ||
|
|
6fc2e9a209 | ||
|
|
4578e076ce | ||
|
|
50958ef4da | ||
|
|
029cf6640a | ||
|
|
6715169630 | ||
|
|
8c0938333d | ||
|
|
6831d6a493 |
42
.dockerignore
Normal file
42
.dockerignore
Normal 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/
|
||||
|
||||
78
.github/workflows/ci.yml
vendored
78
.github/workflows/ci.yml
vendored
@@ -49,11 +49,13 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: setup rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
- name: compile
|
||||
run: cargo build --locked --workspace
|
||||
- name: compile (gpu)
|
||||
run: |
|
||||
cargo build --locked --workspace
|
||||
timeout-minutes: 90
|
||||
- name: test
|
||||
run: cargo test --locked --workspace
|
||||
- name: test (gpu)
|
||||
run: |
|
||||
cargo test --locked --workspace
|
||||
timeout-minutes: 15
|
||||
|
||||
analysis:
|
||||
@@ -66,64 +68,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
|
||||
timeout-minutes: 60
|
||||
- name: run cpu benchmark
|
||||
run: ./target/release/quantus-miner benchmark --cpu-workers 2 --duration 5
|
||||
timeout-minutes: 10
|
||||
|
||||
83
.github/workflows/cuda-builder.yml
vendored
83
.github/workflows/cuda-builder.yml
vendored
@@ -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
145
.github/workflows/docker-image.yml
vendored
Normal 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"
|
||||
|
||||
27
.github/workflows/release-proposal.yml
vendored
27
.github/workflows/release-proposal.yml
vendored
@@ -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"
|
||||
|
||||
40
.github/workflows/release-publish.yml
vendored
40
.github/workflows/release-publish.yml
vendored
@@ -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,14 +66,22 @@ 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
|
||||
# Windows builds
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
binary_name: quantus-miner.exe
|
||||
asset_name: quantus-miner-windows-x86_64.exe
|
||||
# macOS builds (Intel)
|
||||
- os: macos-15-intel
|
||||
target: x86_64-apple-darwin
|
||||
binary_name: quantus-miner
|
||||
asset_name: quantus-miner-macos-x86_64
|
||||
# macOS builds (Apple Silicon)
|
||||
- os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
binary_name: quantus-miner
|
||||
@@ -83,15 +91,21 @@ jobs:
|
||||
- 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 +116,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 +130,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 +148,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-*
|
||||
|
||||
2520
Cargo.lock
generated
2520
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
14
Cargo.toml
@@ -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 = "3.0.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1"
|
||||
@@ -30,18 +29,15 @@ num-bigint = { version = "0.4", features = ["rand"] }
|
||||
num-traits = "0.2"
|
||||
num_cpus = "1.16"
|
||||
primitive-types = { version = "0.13.1", default-features = false }
|
||||
qp-poseidon-core = { version = "1.0.1", default-features = false }
|
||||
qpow-math = { git = "https://github.com/Quantus-Network/chain.git", package = "qpow-math", default-features = false }
|
||||
quantus-miner-api = { version = "0.0.3", default-features = false }
|
||||
qp-poseidon-core = { version = "1.4.0", default-features = false }
|
||||
qpow-math = { git = "https://github.com/Quantus-Network/chain.git", tag = "v0.4.11-star-dust", package = "qpow-math", default-features = false }
|
||||
quantus-miner-api = { git = "https://github.com/Quantus-Network/chain.git", tag = "v0.4.11-star-dust" }
|
||||
rand = { version = "0.8.5", default-features = false }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = { version = "1.0.132", default-features = false }
|
||||
thiserror = "1"
|
||||
tokio = { version = "1.36", features = ["full"] }
|
||||
warp = "0.3"
|
||||
# pow-core now delegates to qpow-math for core functionality while providing optimized mining scaffolding.
|
||||
|
||||
# GPU backend dependencies are declared in individual engine crates (no optional deps in workspace)
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
|
||||
55
Dockerfile
Normal file
55
Dockerfile
Normal 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"]
|
||||
|
||||
@@ -1,256 +1,235 @@
|
||||
# External Miner Protocol Specification
|
||||
|
||||
This document defines the JSON-based HTTP protocol for communication between the Resonance Network node and an external QPoW miner service.
|
||||
This document defines the QUIC-based protocol for communication between the Quantus Network node and external QPoW miner services.
|
||||
|
||||
## Overview
|
||||
|
||||
The node delegates the mining task (finding a valid nonce) to an external service. The node provides the necessary parameters (header hash, difficulty, nonce range) and the external miner searches for a valid nonce according to the QPoW rules defined in the `qpow-math` crate. The miner returns the result, including the winning nonce, when found.
|
||||
The node delegates the mining task (finding a valid nonce) to external miner services over persistent QUIC connections. The node provides the necessary parameters (header hash, difficulty) and each external miner independently searches for a valid nonce according to the QPoW rules defined in the `qpow-math` crate. Miners push results back when found.
|
||||
|
||||
### Key Benefits of QUIC
|
||||
|
||||
- **Lower latency**: Results are pushed immediately when found (no polling)
|
||||
- **Connection resilience**: Built-in connection migration and recovery
|
||||
- **Multiplexed streams**: Multiple operations on single connection
|
||||
- **Built-in TLS**: Encrypted by default
|
||||
|
||||
## Architecture
|
||||
|
||||
### Connection Model
|
||||
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ Node │
|
||||
│ (QUIC Server on port 9833) │
|
||||
│ │
|
||||
┌──────────┐ │ Broadcasts: NewJob │
|
||||
│ Miner 1 │ ──connect───► │ Receives: JobResult │
|
||||
└──────────┘ │ │
|
||||
│ Supports multiple miners │
|
||||
┌──────────┐ │ First valid result wins │
|
||||
│ Miner 2 │ ──connect───► │ │
|
||||
└──────────┘ └─────────────────────────────────┘
|
||||
|
||||
┌──────────┐
|
||||
│ Miner 3 │ ──connect───►
|
||||
└──────────┘
|
||||
```
|
||||
|
||||
- **Node** acts as the QUIC server, listening on port 9833 (default)
|
||||
- **Miners** act as QUIC clients, connecting to the node
|
||||
- Single bidirectional stream per miner connection
|
||||
- Connection persists across multiple mining jobs
|
||||
- Multiple miners can connect simultaneously
|
||||
|
||||
### Multi-Miner Operation
|
||||
|
||||
When multiple miners are connected:
|
||||
1. Node broadcasts the same `NewJob` to all connected miners
|
||||
2. Each miner independently selects a random starting nonce
|
||||
3. First miner to find a valid solution sends `JobResult`
|
||||
4. Node uses the first valid result, ignores subsequent results for same job
|
||||
5. New job broadcast implicitly cancels work on all miners
|
||||
|
||||
### Message Types
|
||||
|
||||
The protocol uses only **two message types**:
|
||||
|
||||
| Direction | Message | Description |
|
||||
|-----------|---------|-------------|
|
||||
| Node → Miner | `NewJob` | Submit a mining job (implicitly cancels any previous job) |
|
||||
| Miner → Node | `JobResult` | Mining result (completed, failed, or cancelled) |
|
||||
|
||||
### Wire Format
|
||||
|
||||
Messages are length-prefixed JSON:
|
||||
|
||||
```
|
||||
┌─────────────────┬─────────────────────────────────┐
|
||||
│ Length (4 bytes)│ JSON payload (MinerMessage) │
|
||||
│ big-endian u32 │ │
|
||||
└─────────────────┴─────────────────────────────────┘
|
||||
```
|
||||
|
||||
Maximum message size: 16 MB
|
||||
|
||||
## Data Types
|
||||
|
||||
See the `resonance-miner-api` crate for the canonical Rust definitions of these structures.
|
||||
See the `quantus-miner-api` crate for the canonical Rust definitions.
|
||||
|
||||
- `job_id`: String (UUID recommended) - Unique identifier for a specific mining task, generated by the node.
|
||||
- `mining_hash`: String (64 hex chars, no 0x) - The header hash for which to find a nonce.
|
||||
- `difficulty`: String (u64 as string) - The target difficulty for the mining job.
|
||||
- `nonce_start`: String (128 hex chars, no 0x) - The starting nonce value (inclusive) for the search range.
|
||||
- `nonce_end`: String (128 hex chars, no 0x) - The ending nonce value (inclusive) for the search range.
|
||||
- `status`: Enum (`ApiResponseStatus`) - Indicates the state or result of an API call.
|
||||
- `message`: String (optional) - Provides details for `Error` status responses.
|
||||
- `nonce`: String (Hex, no 0x) - Represents the `U512` value of the current or winning nonce.
|
||||
- `work`: String (128 hex chars, no 0x) - Represents the winning nonce as `[u8; 64]`. This is the value the node needs for verification.
|
||||
- `hash_count`: Number (u64) - Number of nonces checked by the miner for the job.
|
||||
- `elapsed_time`: Number (f64) - Time in seconds the miner spent on the job.
|
||||
### MinerMessage (Enum)
|
||||
|
||||
## Endpoints
|
||||
```rust
|
||||
pub enum MinerMessage {
|
||||
NewJob(MiningRequest),
|
||||
JobResult(MiningResult),
|
||||
}
|
||||
```
|
||||
|
||||
### 1. Submit Mining Job
|
||||
### MiningRequest
|
||||
|
||||
- **Endpoint:** `POST /mine`
|
||||
- **Description:** The node requests the external miner to start searching for a valid nonce.
|
||||
- **Request Body (`MiningRequest`):**
|
||||
```json
|
||||
{
|
||||
"job_id": "...",
|
||||
"mining_hash": "...",
|
||||
"difficulty": "...",
|
||||
"nonce_start": "...",
|
||||
"nonce_end": "..."
|
||||
}
|
||||
```
|
||||
- **Response Body (`MiningResponse`):**
|
||||
- Success (200 OK):
|
||||
```json
|
||||
{
|
||||
"status": "accepted",
|
||||
"job_id": "..."
|
||||
}
|
||||
```
|
||||
- Error (400 Bad Request - Invalid Input / 409 Conflict - Duplicate Job ID):
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"job_id": "...",
|
||||
"message": "..." // e.g., "Job already exists", "Invalid mining_hash (...)"
|
||||
}
|
||||
```
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `job_id` | String | Unique identifier (UUID recommended) |
|
||||
| `mining_hash` | String | Header hash (64 hex chars, no 0x prefix) |
|
||||
| `distance_threshold` | String | Difficulty (U512 as decimal string) |
|
||||
|
||||
### 2. Get Job Result
|
||||
Note: Nonce range is not specified - each miner independently selects a random starting point.
|
||||
|
||||
- **Endpoint:** `GET /result/{job_id}`
|
||||
- **Description:** The node polls the external miner to check the status and retrieve the result.
|
||||
- **Path Parameter:**
|
||||
- `job_id`: String (UUID) - The ID of the job to query.
|
||||
- **Response Body (`MiningResult`):**
|
||||
- Job Completed (200 OK):
|
||||
```json
|
||||
{
|
||||
"status": "completed",
|
||||
"job_id": "...",
|
||||
"nonce": "...", // U512 hex value of winning nonce
|
||||
"work": "...", // [u8; 64] hex value of winning nonce
|
||||
"hash_count": ..., // u64
|
||||
"elapsed_time": ... // f64 seconds
|
||||
}
|
||||
```
|
||||
- Job Still Running (200 OK):
|
||||
```json
|
||||
{
|
||||
"status": "running",
|
||||
"job_id": "...",
|
||||
"nonce": "...", // Current nonce being checked (U512 hex)
|
||||
"work": null,
|
||||
"hash_count": ..., // u64
|
||||
"elapsed_time": ... // f64 seconds
|
||||
}
|
||||
```
|
||||
- Job Failed (e.g., nonce range exhausted) (200 OK):
|
||||
```json
|
||||
{
|
||||
"status": "failed",
|
||||
"job_id": "...",
|
||||
"nonce": "...", // Final nonce checked (U512 hex)
|
||||
"work": null,
|
||||
"hash_count": ..., // u64
|
||||
"elapsed_time": ... // f64 seconds
|
||||
}
|
||||
```
|
||||
- Job Not Found (404 Not Found):
|
||||
```json
|
||||
{
|
||||
"status": "not_found",
|
||||
"job_id": "...",
|
||||
"nonce": null,
|
||||
"work": null,
|
||||
"hash_count": 0,
|
||||
"elapsed_time": 0.0
|
||||
}
|
||||
```
|
||||
### MiningResult
|
||||
|
||||
### 3. Cancel Mining Job
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `status` | ApiResponseStatus | Result status (see below) |
|
||||
| `job_id` | String | Job identifier |
|
||||
| `nonce` | Option<String> | Winning nonce (U512 hex, no 0x prefix) |
|
||||
| `work` | Option<String> | Winning nonce as bytes (128 hex chars) |
|
||||
| `hash_count` | u64 | Number of nonces checked |
|
||||
| `elapsed_time` | f64 | Time spent mining (seconds) |
|
||||
|
||||
- **Endpoint:** `POST /cancel/{job_id}`
|
||||
- **Description:** The node requests the external miner to stop working on a specific job.
|
||||
- **Path Parameter:**
|
||||
- `job_id`: String (UUID) - The ID of the job to cancel.
|
||||
- **Request Body:** (Empty)
|
||||
- **Response Body (`MiningResponse`):
|
||||
- Success (200 OK):
|
||||
```json
|
||||
{
|
||||
"status": "cancelled",
|
||||
"job_id": "..."
|
||||
}
|
||||
```
|
||||
- Job Not Found (404 Not Found):
|
||||
```json
|
||||
{
|
||||
"status": "not_found",
|
||||
"job_id": "..."
|
||||
}
|
||||
```
|
||||
### ApiResponseStatus (Enum)
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `completed` | Valid nonce found |
|
||||
| `failed` | Nonce range exhausted without finding solution |
|
||||
| `cancelled` | Job was cancelled (new job received) |
|
||||
| `running` | Job still in progress (not typically sent) |
|
||||
|
||||
## Protocol Flow
|
||||
|
||||
### Normal Mining Flow
|
||||
|
||||
```
|
||||
Miner Node
|
||||
│ │
|
||||
│──── QUIC Connect ─────────────────────────►│
|
||||
│◄─── Connection Established ────────────────│
|
||||
│ │
|
||||
│◄─── NewJob { job_id: "abc", ... } ─────────│
|
||||
│ │
|
||||
│ (picks random nonce, starts mining) │
|
||||
│ │
|
||||
│──── JobResult { job_id: "abc", ... } ─────►│ (found solution!)
|
||||
│ │
|
||||
│ (node submits block, gets new work) │
|
||||
│ │
|
||||
│◄─── NewJob { job_id: "def", ... } ─────────│
|
||||
│ │
|
||||
```
|
||||
|
||||
### Job Cancellation (Implicit)
|
||||
|
||||
When a new block arrives before the miner finds a solution, the node simply sends a new `NewJob`. The miner automatically cancels the previous job:
|
||||
|
||||
```
|
||||
Miner Node
|
||||
│ │
|
||||
│◄─── NewJob { job_id: "abc", ... } ─────────│
|
||||
│ │
|
||||
│ (mining "abc") │
|
||||
│ │
|
||||
│ (new block arrives at node!) │
|
||||
│ │
|
||||
│◄─── NewJob { job_id: "def", ... } ─────────│
|
||||
│ │
|
||||
│ (cancels "abc", starts "def") │
|
||||
│ │
|
||||
│──── JobResult { job_id: "def", ... } ─────►│
|
||||
```
|
||||
|
||||
### Miner Connect During Active Job
|
||||
|
||||
When a miner connects while a job is active, it immediately receives the current job:
|
||||
|
||||
```
|
||||
Miner (new) Node
|
||||
│ │ (already mining job "abc")
|
||||
│──── QUIC Connect ─────────────────────────►│
|
||||
│◄─── Connection Established ────────────────│
|
||||
│◄─── NewJob { job_id: "abc", ... } ─────────│ (current job sent immediately)
|
||||
│ │
|
||||
│ (joins mining effort) │
|
||||
```
|
||||
|
||||
### Stale Result Handling
|
||||
|
||||
If a result arrives for an old job, the node discards it:
|
||||
|
||||
```
|
||||
Miner Node
|
||||
│ │
|
||||
│◄─── NewJob { job_id: "abc", ... } ─────────│
|
||||
│ │
|
||||
│◄─── NewJob { job_id: "def", ... } ─────────│ (almost simultaneous)
|
||||
│ │
|
||||
│──── JobResult { job_id: "abc", ... } ─────►│ (stale, node ignores)
|
||||
│ │
|
||||
│──── JobResult { job_id: "def", ... } ─────►│ (current, node uses)
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Node
|
||||
|
||||
```bash
|
||||
# Listen for external miner connections on port 9833
|
||||
quantus-node --miner-listen-port 9833
|
||||
```
|
||||
|
||||
### Miner
|
||||
|
||||
```bash
|
||||
# Connect to node
|
||||
quantus-miner serve --node-addr 127.0.0.1:9833
|
||||
```
|
||||
|
||||
## TLS Configuration
|
||||
|
||||
The node generates a self-signed TLS certificate at startup. The miner skips certificate verification by default (insecure mode). For production deployments, consider:
|
||||
|
||||
1. **Certificate pinning**: Configure the miner to accept only specific certificate fingerprints
|
||||
2. **Proper CA**: Use certificates signed by a trusted CA
|
||||
3. **Network isolation**: Run node and miner on a private network
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Connection Loss
|
||||
|
||||
The miner automatically reconnects with exponential backoff:
|
||||
- Initial delay: 1 second
|
||||
- Maximum delay: 30 seconds
|
||||
|
||||
The node continues operating with remaining connected miners.
|
||||
|
||||
### Validation Errors
|
||||
|
||||
If the miner receives an invalid `MiningRequest`, it sends a `JobResult` with status `failed`.
|
||||
|
||||
## Notes
|
||||
|
||||
- All hex values (`mining_hash`, `nonce_start`, `nonce_end`, `nonce`, `work`) should be sent **without** the `0x` prefix.
|
||||
- The miner must implement the validation logic defined in `qpow_math::is_valid_nonce`.
|
||||
- The node relies primarily on the `work` field in the `MiningResult` (when status is `completed`) for constructing the `QPoWSeal`.
|
||||
|
||||
# External Miner Protocol Specification
|
||||
|
||||
This document defines the JSON-based HTTP protocol for communication between the node and an external QPoW miner.
|
||||
|
||||
## Overview
|
||||
|
||||
The node delegates the mining task to an external service. The node provides the necessary parameters (mining hash, difficulty, and a nonce range) and the external miner searches for a valid nonce within that range. The miner returns the nonce and the resulting work hash when a solution is found.
|
||||
|
||||
## Data Types
|
||||
|
||||
- `job_id`: String (UUID recommended) - Identifier for a specific mining task.
|
||||
- `mining_hash`: String (Hex-encoded, 32-byte hash, H256) - The hash derived from the block header data that the miner needs to solve.
|
||||
- `difficulty`: String (Decimal representation of u64) - The target difficulty for the block.
|
||||
- `nonce_start`: String (Hex-encoded, 64-byte value, U512) - The starting nonce value (inclusive).
|
||||
- `nonce_end`: String (Hex-encoded, 64-byte value, U512) - The ending nonce value (inclusive).
|
||||
- `nonce`: String (Hex-encoded, 64-byte value, U512) - The solution found by the miner.
|
||||
- `work`: String (Hex-encoded, 32-byte hash, H256) - The hash resulting from the combination of `mining_hash` and `nonce`, meeting the difficulty requirement.
|
||||
- `status`: String Enum - Indicates the state or result of an API call.
|
||||
|
||||
## Endpoints
|
||||
|
||||
### 1. Start Mining Job
|
||||
|
||||
- **Endpoint:** `POST /mine`
|
||||
- **Description:** The node requests the external miner to start searching for a valid nonce within the specified range for the given parameters.
|
||||
- **Request Body (application/json):**
|
||||
```json
|
||||
{
|
||||
"job_id": "...", // String (UUID), generated by the node
|
||||
"mining_hash": "...", // Hex String (H256)
|
||||
"difficulty": "...", // String (u64 decimal)
|
||||
"nonce_start": "...", // Hex String (U512 hex)
|
||||
"nonce_end": "..." // Hex String (U512 hex)
|
||||
}
|
||||
```
|
||||
- **Response Body (application/json):**
|
||||
- Success (200 OK):
|
||||
```json
|
||||
{
|
||||
"status": "accepted",
|
||||
"job_id": "..." // String (UUID), confirming the job ID received
|
||||
}
|
||||
```
|
||||
- Error (e.g., 400 Bad Request, 500 Internal Server Error):
|
||||
```json
|
||||
{
|
||||
"status": "rejected",
|
||||
"reason": "..." // String (Description of error)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Get Job Result
|
||||
|
||||
- **Endpoint:** `GET /result/{job_id}`
|
||||
- **Description:** The node polls the external miner to check the status and retrieve the result of a previously submitted job.
|
||||
- **Path Parameter:**
|
||||
- `job_id`: String (UUID) - The ID of the job to query.
|
||||
- **Response Body (application/json):**
|
||||
- Solution Found (200 OK):
|
||||
```json
|
||||
{
|
||||
"status": "found",
|
||||
"job_id": "...", // String (UUID)
|
||||
"nonce": "...", // Hex String (U512 hex)
|
||||
"work": "CAFEBABE01.." // Hex String (H256 hex)
|
||||
}
|
||||
```
|
||||
- Still Working (200 OK):
|
||||
```json
|
||||
{
|
||||
"status": "working",
|
||||
"job_id": "..." // String (UUID)
|
||||
}
|
||||
```
|
||||
- Job Stale/Cancelled (200 OK): Indicates the job is no longer valid (e.g., the node requested cancellation or submitted work for a newer block).
|
||||
```json
|
||||
{
|
||||
"status": "stale",
|
||||
"job_id": "..." // String (UUID)
|
||||
}
|
||||
```
|
||||
- Job Not Found (404 Not Found):
|
||||
```json
|
||||
{
|
||||
"status": "not_found",
|
||||
"job_id": "..." // String (UUID)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Cancel Mining Job
|
||||
|
||||
- **Endpoint:** `POST /cancel/{job_id}`
|
||||
- **Description:** The node requests the external miner to stop working on a specific job. This is typically used when the node receives a new block or its mining parameters change, making the old job obsolete.
|
||||
- **Path Parameter:**
|
||||
- `job_id`: String (UUID) - The ID of the job to cancel.
|
||||
- **Request Body:** (Empty)
|
||||
- **Response Body (application/json):**
|
||||
- Success (200 OK):
|
||||
```json
|
||||
{
|
||||
"status": "cancelled",
|
||||
"job_id": "..." // String (UUID)
|
||||
}
|
||||
```
|
||||
- Job Not Found (404 Not Found):
|
||||
```json
|
||||
{
|
||||
"status": "not_found",
|
||||
"job_id": "..." // String (UUID)
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- The external miner should iterate from `nonce_start` up to and including `nonce_end` when searching for a valid nonce.
|
||||
- The miner should return the `nonce` and the calculated `work` hash when a solution is found.
|
||||
- The node uses the returned `nonce` and `work` (along with the fetched `difficulty`) to construct the `QPoWSeal` and submit it.
|
||||
- The external miner should not need to know anything about the runtime or the code; it only needs to perform the nonce search and return the results.
|
||||
- All hex values should be sent **without** the `0x` prefix
|
||||
- The miner implements validation logic from `qpow_math::is_valid_nonce`
|
||||
- The node uses the `work` field from `MiningResult` to construct `QPoWSeal`
|
||||
- ALPN protocol identifier: `quantus-miner`
|
||||
- Each miner independently generates a random nonce starting point using cryptographically secure randomness
|
||||
- With a 512-bit nonce space, collision between miners is statistically impossible
|
||||
|
||||
320
README.md
320
README.md
@@ -1,244 +1,126 @@
|
||||
# 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 --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 isn’t 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 NVIDIA’s 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 doesn’t 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 driver’s 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 serve --cpu-workers 4
|
||||
|
||||
# GPU-only mining
|
||||
./target/release/quantus-miner serve --gpu-devices 1
|
||||
|
||||
# Hybrid CPU+GPU mining
|
||||
./target/release/quantus-miner serve --cpu-workers 4 --gpu-devices 1
|
||||
|
||||
# Custom port and metrics
|
||||
./target/release/quantus-miner serve --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-devices <N>` | `MINER_GPU_DEVICES` | Number of GPU devices | 0 |
|
||||
| `--port <PORT>` | `MINER_PORT` | QUIC server 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 --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 serve --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 serve --gpu-devices 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 serve --cpu-workers 4 --gpu-devices 1
|
||||
|
||||
# With verbose logging
|
||||
RUST_LOG=debug ./target/release/quantus-miner serve --cpu-workers 2 --gpu-devices 1
|
||||
|
||||
# Production setup with metrics
|
||||
./target/release/quantus-miner serve \
|
||||
--cpu-workers 6 \
|
||||
--gpu-devices 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.
|
||||
## Protocol
|
||||
|
||||
Example output:
|
||||
The miner uses a QUIC-based protocol for communication with the node:
|
||||
|
||||
```
|
||||
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
|
||||
- **Transport**: QUIC with TLS 1.3 (self-signed certificates)
|
||||
- **Port**: 9833 (default)
|
||||
- **Messages**: `NewJob` (from node) and `JobResult` (from miner)
|
||||
|
||||
For full protocol specification, see `EXTERNAL_MINER_PROTOCOL.md`.
|
||||
|
||||
## 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 serve --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-devices 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-devices 1 --duration 30
|
||||
```
|
||||
|
||||
15
agents.md
15
agents.md
@@ -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
|
||||
|
||||
---
|
||||
|
||||
|
||||
242
api/openapi.yaml
242
api/openapi.yaml
@@ -1,242 +0,0 @@
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
title: Quantus External Miner API
|
||||
version: 0.1.0
|
||||
description: API for managing QPoW mining jobs for the Resonance Network external miner service.
|
||||
|
||||
paths:
|
||||
/mine:
|
||||
post:
|
||||
summary: Submit a new mining job
|
||||
description: Accepts a mining job request and adds it to the queue if valid.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MiningRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Job accepted successfully.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MiningResponseAccepted'
|
||||
'400':
|
||||
description: Invalid request (e.g., missing fields, invalid format).
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MiningResponseError'
|
||||
'409':
|
||||
description: Conflict (e.g., duplicate job ID).
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MiningResponseError'
|
||||
|
||||
/result/{job_id}:
|
||||
get:
|
||||
summary: Get mining job status and result
|
||||
description: Retrieves the current status and potentially the found nonce for a given job ID.
|
||||
parameters:
|
||||
- name: job_id
|
||||
in: path
|
||||
required: true
|
||||
description: The ID of the mining job to retrieve.
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
'200':
|
||||
description: Job status retrieved successfully (could be running, completed, or failed).
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MiningResult'
|
||||
'404':
|
||||
description: Job ID not found.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MiningResultNotFound'
|
||||
|
||||
/cancel/{job_id}:
|
||||
post:
|
||||
summary: Cancel a mining job
|
||||
description: Attempts to cancel an ongoing mining job.
|
||||
parameters:
|
||||
- name: job_id
|
||||
in: path
|
||||
required: true
|
||||
description: The ID of the mining job to cancel.
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
'200':
|
||||
description: Job cancelled successfully.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MiningResponseCancelled'
|
||||
'404':
|
||||
description: Job ID not found.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MiningResponseNotFound'
|
||||
|
||||
components:
|
||||
schemas:
|
||||
ApiResponseStatus:
|
||||
type: string
|
||||
description: Status code indicating the result of an API call or the state of a mining job.
|
||||
enum:
|
||||
- accepted
|
||||
- running
|
||||
- completed
|
||||
- failed
|
||||
- cancelled
|
||||
- not_found
|
||||
- error
|
||||
example: running
|
||||
|
||||
MiningRequest:
|
||||
type: object
|
||||
required:
|
||||
- job_id
|
||||
- mining_hash
|
||||
- difficulty
|
||||
- nonce_start
|
||||
- nonce_end
|
||||
properties:
|
||||
job_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Unique identifier for the mining job (UUID format recommended).
|
||||
example: "123e4567-e89b-12d3-a456-426614174000"
|
||||
mining_hash:
|
||||
type: string
|
||||
description: The header hash to mine on (hex encoded, 64 characters, no 0x).
|
||||
example: "a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0"
|
||||
pattern: '^[a-fA-F0-9]{64}$'
|
||||
difficulty:
|
||||
type: string
|
||||
description: Target difficulty for the mining job (string representation of a u64).
|
||||
example: "10000000000"
|
||||
nonce_start:
|
||||
type: string
|
||||
description: Starting nonce value (hex encoded, 128 characters, no 0x).
|
||||
example: "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
pattern: '^[a-fA-F0-9]{128}$'
|
||||
nonce_end:
|
||||
type: string
|
||||
description: Ending nonce value (hex encoded, 128 characters, no 0x).
|
||||
example: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
|
||||
pattern: '^[a-fA-F0-9]{128}$'
|
||||
|
||||
MiningResponseBase:
|
||||
type: object
|
||||
required:
|
||||
- status
|
||||
- job_id
|
||||
properties:
|
||||
status:
|
||||
$ref: '#/components/schemas/ApiResponseStatus'
|
||||
job_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: The ID of the job associated with the response.
|
||||
example: "123e4567-e89b-12d3-a456-426614174000"
|
||||
|
||||
MiningResponseAccepted:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/MiningResponseBase'
|
||||
- type: object
|
||||
properties:
|
||||
status:
|
||||
example: accepted
|
||||
|
||||
MiningResponseCancelled:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/MiningResponseBase'
|
||||
- type: object
|
||||
properties:
|
||||
status:
|
||||
example: cancelled
|
||||
|
||||
MiningResponseError:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/MiningResponseBase'
|
||||
- type: object
|
||||
properties:
|
||||
status:
|
||||
example: error
|
||||
message:
|
||||
type: string
|
||||
description: Details about the error that occurred.
|
||||
example: "Job already exists"
|
||||
|
||||
MiningResponseNotFound:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/MiningResponseBase'
|
||||
- type: object
|
||||
properties:
|
||||
status:
|
||||
example: not_found
|
||||
|
||||
MiningResult:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/MiningResponseBase'
|
||||
- type: object
|
||||
required:
|
||||
- hash_count
|
||||
- elapsed_time
|
||||
properties:
|
||||
nonce:
|
||||
type: string
|
||||
nullable: true
|
||||
description: The current or winning nonce (U512 hex, no 0x).
|
||||
example: "1a2b3c"
|
||||
pattern: '^[a-fA-F0-9]+$'
|
||||
work:
|
||||
type: string
|
||||
nullable: true
|
||||
description: The winning nonce ([u8; 64] hex, 128 chars, no 0x). Only present if status is 'completed'.
|
||||
example: "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a2b3c"
|
||||
pattern: '^[a-fA-F0-9]{128}$'
|
||||
hash_count:
|
||||
type: integer
|
||||
format: uint64
|
||||
description: Number of nonces checked so far for this job.
|
||||
example: 123456
|
||||
elapsed_time:
|
||||
type: number
|
||||
format: double
|
||||
description: Time elapsed (in seconds) since the job started.
|
||||
example: 15.732
|
||||
|
||||
MiningResultNotFound:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/MiningResponseBase'
|
||||
- type: object
|
||||
properties:
|
||||
status:
|
||||
example: not_found
|
||||
nonce:
|
||||
type: string
|
||||
nullable: true
|
||||
example: null
|
||||
work:
|
||||
type: string
|
||||
nullable: true
|
||||
example: null
|
||||
hash_count:
|
||||
type: integer
|
||||
format: uint64
|
||||
example: 0
|
||||
elapsed_time:
|
||||
type: number
|
||||
format: double
|
||||
example: 0.0
|
||||
@@ -15,15 +15,11 @@ 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]
|
||||
pow-core = { path = "../pow-core" }
|
||||
primitive-types = { workspace = true }
|
||||
log = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
hex = { workspace = true }
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,23 +1,14 @@
|
||||
#![deny(rust_2018_idioms)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! CPU mining engine scaffolding and trait definition.
|
||||
//! CPU mining engine for Quantus.
|
||||
//!
|
||||
//! This crate defines a minimal `MinerEngine` trait so the service layer can
|
||||
//! orchestrate mining without knowing about specific math/device details,
|
||||
//! plus a baseline CPU implementation that uses the reference path in `pow-core`.
|
||||
//!
|
||||
//! The baseline engine performs a straightforward linear scan across an inclusive
|
||||
//! nonce range, computing distance per nonce using the context (header-derived)
|
||||
//! constants. It is intended as a correctness reference and may be replaced at
|
||||
//! runtime by faster engines (e.g., incremental CPU path, CUDA/OpenCL engines).
|
||||
//! This crate defines the `MinerEngine` trait for mining orchestration,
|
||||
//! plus a fast CPU implementation using optimized paths from `pow-core`.
|
||||
|
||||
use core::cmp::Ordering;
|
||||
|
||||
use pow_core::{is_valid_nonce, JobContext};
|
||||
use pow_core::JobContext;
|
||||
use primitive_types::U512;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering as AtomicOrdering};
|
||||
use std::time::Duration;
|
||||
use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
|
||||
|
||||
/// An inclusive nonce range to search.
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -44,13 +35,6 @@ pub enum FoundOrigin {
|
||||
}
|
||||
|
||||
/// Status from an engine search.
|
||||
///
|
||||
/// For synchronous `search_range` calls, the final outcome will be one of:
|
||||
/// - `Found`
|
||||
/// - `Exhausted`
|
||||
/// - `Cancelled`
|
||||
///
|
||||
/// `Running` is included for potential future async/streaming engines.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum EngineStatus {
|
||||
Running {
|
||||
@@ -72,99 +56,22 @@ pub enum EngineStatus {
|
||||
/// Abstract mining engine interface.
|
||||
///
|
||||
/// The service layer depends only on this trait to manage jobs.
|
||||
/// Different engines (baseline CPU, optimized CPU, CUDA, OpenCL) can implement
|
||||
/// this trait and be selected at runtime via configuration.
|
||||
/// Different engines (CPU, CUDA, OpenCL) can implement this trait.
|
||||
pub trait MinerEngine: Send + Sync {
|
||||
/// Human-readable engine name (for logs/metrics).
|
||||
/// Human-readable engine name (for logs).
|
||||
fn name(&self) -> &'static str;
|
||||
|
||||
/// Prepare a precomputed context for a job (header + difficulty).
|
||||
fn prepare_context(&self, header_hash: [u8; 32], difficulty: U512) -> JobContext;
|
||||
|
||||
/// Search an inclusive nonce range with cancellation support.
|
||||
///
|
||||
/// Implementations should:
|
||||
/// - Respect `cancel` promptly to minimize wasted work after a solution is found.
|
||||
/// - Return `Found` with a `Candidate` on success.
|
||||
/// - 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.
|
||||
///
|
||||
/// This implementation uses the reference path from `pow-core` for distance
|
||||
/// computation and scans the range linearly, one nonce at a time.
|
||||
#[derive(Default)]
|
||||
pub struct BaselineCpuEngine;
|
||||
|
||||
impl BaselineCpuEngine {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl MinerEngine for BaselineCpuEngine {
|
||||
fn name(&self) -> &'static str {
|
||||
"cpu-baseline"
|
||||
}
|
||||
|
||||
fn prepare_context(&self, header_hash: [u8; 32], difficulty: U512) -> JobContext {
|
||||
JobContext::new(header_hash, difficulty)
|
||||
}
|
||||
|
||||
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 {
|
||||
return EngineStatus::Exhausted { hash_count: 0 };
|
||||
}
|
||||
|
||||
let mut current = range.start;
|
||||
let mut hash_count: u64 = 0;
|
||||
|
||||
loop {
|
||||
// Cancellation check
|
||||
if cancel.load(AtomicOrdering::Relaxed) {
|
||||
return EngineStatus::Cancelled { hash_count };
|
||||
}
|
||||
|
||||
// Compute hash for this nonce using Bitcoin-style double Poseidon2
|
||||
let (is_valid, hash) =
|
||||
is_valid_nonce(ctx.header, current.to_big_endian(), ctx.difficulty);
|
||||
hash_count = hash_count.saturating_add(1);
|
||||
|
||||
// Check if it meets difficulty target
|
||||
if is_valid {
|
||||
let work = current.to_big_endian();
|
||||
let candidate = Candidate {
|
||||
nonce: current,
|
||||
work,
|
||||
hash,
|
||||
};
|
||||
return EngineStatus::Found {
|
||||
candidate,
|
||||
hash_count,
|
||||
origin: FoundOrigin::Cpu,
|
||||
};
|
||||
}
|
||||
|
||||
// Advance or finish
|
||||
match current.cmp(&range.end) {
|
||||
Ordering::Less => {
|
||||
current = current.saturating_add(U512::one());
|
||||
}
|
||||
_ => {
|
||||
// End of inclusive range reached
|
||||
break EngineStatus::Exhausted { hash_count };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-export commonly used items for convenience by consumers.
|
||||
|
||||
// Fast CPU engine using incremental pow-core helpers (init_worker_y0 + step_mul)
|
||||
/// Fast CPU engine using optimized pow-core helpers.
|
||||
#[derive(Default)]
|
||||
pub struct FastCpuEngine;
|
||||
|
||||
@@ -176,17 +83,20 @@ 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};
|
||||
|
||||
// Ensure start <= end (inclusive range). If not, treat as exhausted.
|
||||
if range.start > range.end {
|
||||
return EngineStatus::Exhausted { hash_count: 0 };
|
||||
}
|
||||
@@ -195,12 +105,10 @@ impl MinerEngine for FastCpuEngine {
|
||||
let mut hash_count: u64 = 0;
|
||||
|
||||
loop {
|
||||
// Cancellation check
|
||||
if cancel.load(AtomicOrdering::Relaxed) {
|
||||
return EngineStatus::Cancelled { hash_count };
|
||||
}
|
||||
|
||||
// Compute hash using Bitcoin-style double Poseidon2
|
||||
let hash = hash_from_nonce(ctx, current);
|
||||
hash_count = hash_count.saturating_add(1);
|
||||
|
||||
@@ -221,7 +129,6 @@ impl MinerEngine for FastCpuEngine {
|
||||
break;
|
||||
}
|
||||
|
||||
// Advance to next nonce
|
||||
current = step_nonce(current);
|
||||
}
|
||||
|
||||
@@ -229,136 +136,8 @@ impl MinerEngine for FastCpuEngine {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ChainManipulatorEngine {
|
||||
/// Base sleep per batch in nanoseconds; actual sleep increases linearly with job count.
|
||||
pub base_delay_ns: u64,
|
||||
/// Number of nonce attempts between sleeps.
|
||||
pub step_batch: u64,
|
||||
/// Optional cap for solved-block throttling (sleep index will not exceed this).
|
||||
pub throttle_cap: Option<u64>,
|
||||
/// Monotonically increasing solved-block counter used to scale throttling.
|
||||
/// Public so the service can initialize from CLI (pick up where we left off).
|
||||
pub job_index: AtomicU64,
|
||||
}
|
||||
|
||||
impl ChainManipulatorEngine {
|
||||
pub fn new() -> Self {
|
||||
// Start fast: first block has no throttle (job_index = 0 -> 0ns sleep),
|
||||
// then 0.5ms per batch at block 1, 1.0ms at block 2, etc.
|
||||
Self {
|
||||
base_delay_ns: 500_000, // 0.5 ms
|
||||
step_batch: 10_000, // sleep every 10k nonce checks
|
||||
throttle_cap: None, // unlimited by default
|
||||
job_index: AtomicU64::new(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MinerEngine for ChainManipulatorEngine {
|
||||
fn name(&self) -> &'static str {
|
||||
"cpu-chain-manipulator"
|
||||
}
|
||||
|
||||
fn prepare_context(&self, header_hash: [u8; 32], difficulty: U512) -> JobContext {
|
||||
// Per-block throttling: do NOT increment here. We increment on Found (i.e., when a block is solved).
|
||||
let ctx = JobContext::new(header_hash, difficulty);
|
||||
// Debug: log current throttle state at job start
|
||||
log::debug!(
|
||||
target: "miner",
|
||||
"manipulator throttle start: solved_blocks={}, sleep_ns_per_batch={}, step_batch={}",
|
||||
self.job_index.load(AtomicOrdering::Relaxed),
|
||||
self.base_delay_ns.saturating_mul(self.job_index.load(AtomicOrdering::Relaxed)),
|
||||
self.step_batch
|
||||
);
|
||||
ctx
|
||||
}
|
||||
|
||||
fn search_range(&self, ctx: &JobContext, range: Range, cancel: &AtomicBool) -> EngineStatus {
|
||||
use pow_core::{hash_from_nonce, is_valid_hash, step_nonce};
|
||||
|
||||
if range.start > range.end {
|
||||
return EngineStatus::Exhausted { hash_count: 0 };
|
||||
}
|
||||
|
||||
// Bitcoin-style hashing path
|
||||
let mut current = range.start;
|
||||
let mut hash_count: u64 = 0;
|
||||
let mut batch_counter: u64 = 0;
|
||||
|
||||
// Per-block throttling: derived from blocks solved so far (apply cap if configured).
|
||||
let mut solved_blocks = self.job_index.load(AtomicOrdering::Relaxed);
|
||||
if let Some(cap) = self.throttle_cap {
|
||||
if solved_blocks > cap {
|
||||
solved_blocks = cap;
|
||||
}
|
||||
}
|
||||
let sleep_ns = self.base_delay_ns.saturating_mul(solved_blocks);
|
||||
let do_sleep = sleep_ns > 0;
|
||||
|
||||
loop {
|
||||
if cancel.load(AtomicOrdering::Relaxed) {
|
||||
return EngineStatus::Cancelled { hash_count };
|
||||
}
|
||||
|
||||
// Compute hash using Bitcoin-style double Poseidon2
|
||||
let hash = hash_from_nonce(ctx, current);
|
||||
hash_count = hash_count.saturating_add(1);
|
||||
batch_counter = batch_counter.saturating_add(1);
|
||||
|
||||
// Optional detailed debug of throttle progression within a job
|
||||
#[allow(unused_variables)]
|
||||
let _dbg_batch = batch_counter;
|
||||
|
||||
if is_valid_hash(ctx, hash) {
|
||||
let work = current.to_big_endian();
|
||||
// Increment solved-block counter so the NEXT block throttles more.
|
||||
let _new_idx = self.job_index.fetch_add(1, AtomicOrdering::Relaxed) + 1;
|
||||
{
|
||||
let capped = if let Some(cap) = self.throttle_cap {
|
||||
std::cmp::min(_new_idx, cap)
|
||||
} else {
|
||||
_new_idx
|
||||
};
|
||||
log::debug!(
|
||||
target: "miner",
|
||||
"manipulator throttle increment: solved_blocks={} (next sleep_ns_per_batch={}, cap={:?})",
|
||||
_new_idx,
|
||||
self.base_delay_ns.saturating_mul(capped),
|
||||
self.throttle_cap
|
||||
);
|
||||
}
|
||||
return EngineStatus::Found {
|
||||
candidate: Candidate {
|
||||
nonce: current,
|
||||
work,
|
||||
hash,
|
||||
},
|
||||
hash_count,
|
||||
origin: FoundOrigin::Cpu,
|
||||
};
|
||||
}
|
||||
|
||||
// Throttle after each batch to artificially slow down based on blocks solved so far.
|
||||
if do_sleep && batch_counter >= self.step_batch {
|
||||
std::thread::sleep(Duration::from_nanos(sleep_ns));
|
||||
batch_counter = 0;
|
||||
}
|
||||
|
||||
// Advance
|
||||
if current < range.end {
|
||||
current = step_nonce(current);
|
||||
} else {
|
||||
break EngineStatus::Exhausted { hash_count };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub use {
|
||||
BaselineCpuEngine as DefaultEngine, Candidate as EngineCandidate,
|
||||
ChainManipulatorEngine as ChainEngine, FastCpuEngine as FastEngine, Range as EngineRange,
|
||||
};
|
||||
// Re-exports for convenience
|
||||
pub use {Candidate as EngineCandidate, Range as EngineRange};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
@@ -366,33 +145,25 @@ mod tests {
|
||||
use primitive_types::U512;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
fn make_ctx() -> JobContext {
|
||||
let header = [1u8; 32];
|
||||
let difficulty = U512::from(1u64); // easy difficulty for "found" parity test
|
||||
JobContext::new(header, difficulty)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn engine_returns_exhausted_when_no_solution_in_range() {
|
||||
// Use a very hard difficulty to make solutions effectively impossible in a tiny range.
|
||||
let header = [2u8; 32];
|
||||
let difficulty = U512::MAX;
|
||||
let difficulty = U512::MAX; // impossible difficulty
|
||||
let ctx = JobContext::new(header, difficulty);
|
||||
|
||||
let range = Range {
|
||||
start: U512::from(1u64),
|
||||
end: U512::from(1000u64), // small range; probability of accidental match is negligible
|
||||
end: U512::from(1000u64),
|
||||
};
|
||||
|
||||
let cancel = AtomicBool::new(false);
|
||||
let baseline = BaselineCpuEngine::new();
|
||||
let engine = FastCpuEngine::new();
|
||||
|
||||
let status = baseline.search_range(&ctx, range.clone(), &cancel);
|
||||
let status = engine.search_range(&ctx, range.clone(), &cancel);
|
||||
match status {
|
||||
EngineStatus::Exhausted { hash_count } => {
|
||||
// Inclusive range length = end - start + 1
|
||||
let expected = (range.end - range.start + U512::one()).as_u64();
|
||||
assert_eq!(hash_count, expected, "hash_count should equal range length");
|
||||
assert_eq!(hash_count, expected);
|
||||
}
|
||||
other => panic!("expected Exhausted, got {other:?}"),
|
||||
}
|
||||
@@ -400,19 +171,22 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn engine_respects_immediate_cancellation() {
|
||||
let ctx = make_ctx();
|
||||
let header = [1u8; 32];
|
||||
let difficulty = U512::from(1u64);
|
||||
let ctx = JobContext::new(header, difficulty);
|
||||
|
||||
let range = Range {
|
||||
start: U512::from(0u64),
|
||||
end: U512::from(1_000_000u64),
|
||||
};
|
||||
let cancel = AtomicBool::new(true); // cancelled before starting
|
||||
let baseline = BaselineCpuEngine::new();
|
||||
|
||||
let status = baseline.search_range(&ctx, range, &cancel);
|
||||
let cancel = AtomicBool::new(true); // pre-cancelled
|
||||
let engine = FastCpuEngine::new();
|
||||
|
||||
let status = engine.search_range(&ctx, range, &cancel);
|
||||
match status {
|
||||
EngineStatus::Cancelled { hash_count } => {
|
||||
// Cancellation was pre-set; allow zero or near-zero work depending on timing.
|
||||
assert_eq!(hash_count, 0, "expected no work when cancelled immediately");
|
||||
assert_eq!(hash_count, 0);
|
||||
}
|
||||
other => panic!("expected Cancelled, got {other:?}"),
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
@@ -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()
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
# engine-gpu-cuda (CUDA backend) – G1 bring‑up
|
||||
|
||||
This crate provides the CUDA GPU backend for the Quantus External Miner. It currently implements "G1" bring‑up: the per‑nonce modular multiply loop runs on the GPU (512‑bit Montgomery CIOS), while Poseidon2‑512 and the threshold check run on the host CPU. This allows correctness and plumbing to be validated before we move Poseidon2 and early‑exit onto the device in G2.
|
||||
|
||||
The backend is feature‑gated. When built with `--features cuda`, the crate’s 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: 512‑bit Montgomery multiply (CIOS using 64×64→128 via `__umul64hi`), maintaining `ŷ` in Montgomery domain and converting to normal domain for output.
|
||||
- Host: Poseidon2‑512 and threshold compare (and orchestration).
|
||||
- Correctness: parity against CPU small‑range tests.
|
||||
- Performance: primarily limited by PCIe copy‑back and host Poseidon2. See "Tuning" below.
|
||||
|
||||
- G2 (next):
|
||||
- Device: Poseidon2‑512 optimized for 64‑byte input.
|
||||
- Device: threshold compare + global early‑exit flag (atomic) + tiny candidate write.
|
||||
- Device: move constants to `__constant__` memory.
|
||||
- Host: poll early‑exit; no large copy‑backs (only candidate or counters).
|
||||
- Result: removes PCIe and host‑Poseidon2 bottlenecks; enables real GPU‑bound 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 (forward‑compatible 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 GPU‑broken 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 NVIDIA’s 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 – build‑time”).
|
||||
- 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 per‑launch 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 × 1–2 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 64–128 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 – build‑time (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 64–128 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 Poseidon2‑512 tuned for 64B input.
|
||||
- On‑device threshold compare and early‑exit flag (atomic).
|
||||
- Host polling and tiny candidate copy‑back.
|
||||
- Constants in `__constant__` memory.
|
||||
- With G2, copy‑backs and host hashing disappear from the steady‑state path, enabling real GPU‑bound 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.
|
||||
@@ -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
@@ -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 }
|
||||
@@ -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);
|
||||
// }
|
||||
// }
|
||||
46
crates/engine-gpu/Cargo.toml
Normal file
46
crates/engine-gpu/Cargo.toml
Normal file
@@ -0,0 +1,46 @@
|
||||
[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"]
|
||||
|
||||
# Map engine features (pow-core dependency removed for now)
|
||||
baseline = []
|
||||
simd-poseidon2 = []
|
||||
|
||||
[dependencies]
|
||||
engine-cpu = { path = "../engine-cpu" }
|
||||
pow-core = { path = "../pow-core" }
|
||||
primitive-types = { workspace = true }
|
||||
log = { workspace = true }
|
||||
qp-poseidon-core = { workspace = true }
|
||||
qp-poseidon-constants = "1.1"
|
||||
qp-plonky2-field = { version = "1.1.1" }
|
||||
plonky2 = { package = "qp-plonky2", version = "1.1.3" }
|
||||
p3-goldilocks = "0.3"
|
||||
p3-field = "0.3"
|
||||
|
||||
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
|
||||
343
crates/engine-gpu/benches/gpu_engine_bench.rs
Normal file
343
crates/engine-gpu/benches/gpu_engine_bench.rs
Normal 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);
|
||||
62
crates/engine-gpu/examples/verify_nonce.rs
Normal file
62
crates/engine-gpu/examples/verify_nonce.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use engine_cpu::{EngineStatus, FastCpuEngine, 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 = FastCpuEngine::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!");
|
||||
}
|
||||
}
|
||||
}
|
||||
245
crates/engine-gpu/src/end_to_end_tests.rs
Normal file
245
crates/engine-gpu/src/end_to_end_tests.rs
Normal file
@@ -0,0 +1,245 @@
|
||||
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, total_nonces, cancel_check_interval]
|
||||
let dispatch_config_data: [u32; 4] = [256, 1, 256, 10000];
|
||||
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),
|
||||
);
|
||||
|
||||
// Cancel flag buffer: 0 = running, 1 = cancel requested
|
||||
let cancel_flag_data: [u32; 1] = [0];
|
||||
let cancel_flag_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Cancel Flag Buffer"),
|
||||
size: 4,
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
queue.write_buffer(
|
||||
&cancel_flag_buffer,
|
||||
0,
|
||||
bytemuck::cast_slice(&cancel_flag_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(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 5,
|
||||
resource: cancel_flag_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);
|
||||
|
||||
// Parse hash from GPU result
|
||||
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 gpu_hash = U512::from_little_endian(&hash_bytes);
|
||||
println!("GPU Hash: {:x}", gpu_hash);
|
||||
|
||||
// Verify: compute hash on CPU for the found nonce and compare
|
||||
let cpu_verified_hash = hash_from_nonce(&ctx, found_nonce);
|
||||
println!("CPU verified hash: {:x}", cpu_verified_hash);
|
||||
|
||||
assert_eq!(
|
||||
gpu_hash, cpu_verified_hash,
|
||||
"GPU hash doesn't match CPU verification for the found nonce!"
|
||||
);
|
||||
|
||||
// Verify hash is below target (should always pass with difficulty 1)
|
||||
assert!(gpu_hash <= ctx.target, "Hash is not below target!");
|
||||
|
||||
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(())
|
||||
}
|
||||
720
crates/engine-gpu/src/lib.rs
Normal file
720
crates/engine-gpu/src/lib.rs
Normal file
@@ -0,0 +1,720 @@
|
||||
#![deny(rust_2018_idioms)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use engine_cpu::{Candidate, EngineStatus, FoundOrigin, MinerEngine, Range};
|
||||
use futures::executor::block_on;
|
||||
use pow_core::{format_hashrate, format_u512, JobContext};
|
||||
use primitive_types::U512;
|
||||
use std::cell::RefCell;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
/// Default interval for checking cancel flag in shader (in nonces)
|
||||
const DEFAULT_CANCEL_CHECK_INTERVAL: u32 = 10_000;
|
||||
|
||||
/// Represents a single GPU device context.
|
||||
struct GpuContext {
|
||||
device: wgpu::Device,
|
||||
queue: wgpu::Queue,
|
||||
pipeline: wgpu::ComputePipeline,
|
||||
|
||||
// Cached vendor configuration
|
||||
optimal_workgroups: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct GpuResources {
|
||||
header_buffer: wgpu::Buffer,
|
||||
target_buffer: wgpu::Buffer,
|
||||
start_nonce_buffer: wgpu::Buffer,
|
||||
results_buffer: wgpu::Buffer,
|
||||
dispatch_config_buffer: wgpu::Buffer,
|
||||
cancel_buffer: wgpu::Buffer,
|
||||
staging_buffer: wgpu::Buffer,
|
||||
bind_group: wgpu::BindGroup,
|
||||
}
|
||||
|
||||
pub struct GpuEngine {
|
||||
contexts: Vec<Arc<GpuContext>>,
|
||||
device_counter: AtomicUsize,
|
||||
cancel_check_interval: u32,
|
||||
}
|
||||
|
||||
// Thread-local storage for consistent GPU device assignment per worker thread
|
||||
thread_local! {
|
||||
static ASSIGNED_GPU_DEVICE: RefCell<Option<usize>> = const { RefCell::new(None) };
|
||||
static WORKER_RESOURCES: RefCell<Option<GpuResources>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
impl GpuContext {
|
||||
fn create_resources(&self) -> GpuResources {
|
||||
let bind_group_layout = self.pipeline.get_bind_group_layout(0);
|
||||
|
||||
// Header: 8 u32s
|
||||
let header_buffer = self.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 = self.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 = self.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 = self.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, total_nonces, cancel_check_interval] = 4 u32s
|
||||
let dispatch_config_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Dispatch Config Buffer"),
|
||||
size: 16,
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
// Cancel flag: single u32 (0 = running, 1 = cancel requested)
|
||||
let cancel_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Cancel Buffer"),
|
||||
size: 4,
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
let staging_buffer = self.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 = self.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(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 5,
|
||||
resource: cancel_buffer.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
GpuResources {
|
||||
header_buffer,
|
||||
target_buffer,
|
||||
start_nonce_buffer,
|
||||
results_buffer,
|
||||
dispatch_config_buffer,
|
||||
cancel_buffer,
|
||||
staging_buffer,
|
||||
bind_group,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GpuEngine {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl GpuEngine {
|
||||
pub fn new() -> Self {
|
||||
block_on(Self::init(DEFAULT_CANCEL_CHECK_INTERVAL))
|
||||
.expect("Failed to initialize GPU engine")
|
||||
}
|
||||
|
||||
/// Create a new GPU engine with a custom cancel check interval.
|
||||
pub fn with_cancel_interval(cancel_check_interval: u32) -> Self {
|
||||
block_on(Self::init(cancel_check_interval)).expect("Failed to initialize GPU engine")
|
||||
}
|
||||
|
||||
/// Try to initialize the GPU engine, returning an error if initialization fails.
|
||||
pub fn try_new() -> Result<Self, Box<dyn std::error::Error>> {
|
||||
block_on(Self::init(DEFAULT_CANCEL_CHECK_INTERVAL))
|
||||
}
|
||||
|
||||
/// Try to initialize the GPU engine with a custom cancel check interval.
|
||||
pub fn try_with_cancel_interval(
|
||||
cancel_check_interval: u32,
|
||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
block_on(Self::init(cancel_check_interval))
|
||||
}
|
||||
|
||||
async fn init(cancel_check_interval: u32) -> 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,
|
||||
});
|
||||
|
||||
log::debug!(target: "gpu_engine", "Pipeline 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,
|
||||
optimal_workgroups,
|
||||
}));
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"GPU engine initialized with {} devices (cancel check interval: {} nonces)",
|
||||
contexts.len(),
|
||||
cancel_check_interval
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
contexts,
|
||||
device_counter: AtomicUsize::new(0),
|
||||
cancel_check_interval,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the number of GPU devices available
|
||||
pub fn device_count(&self) -> usize {
|
||||
self.contexts.len()
|
||||
}
|
||||
|
||||
/// Explicitly clear thread-local GPU resources.
|
||||
/// Call this before thread exit to avoid TLS destruction order issues with wgpu.
|
||||
pub fn clear_worker_resources() {
|
||||
WORKER_RESOURCES.with(|resources| {
|
||||
*resources.borrow_mut() = None;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
// Check for pre-cancellation
|
||||
if cancel.load(Ordering::Relaxed) {
|
||||
return EngineStatus::Cancelled { 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 {
|
||||
index
|
||||
} else {
|
||||
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];
|
||||
|
||||
// Calculate range size (capped at u32::MAX for dispatch config)
|
||||
let range_size_u512 = range
|
||||
.end
|
||||
.saturating_sub(range.start)
|
||||
.saturating_add(U512::one());
|
||||
let range_size = if range_size_u512 > U512::from(u32::MAX) {
|
||||
u32::MAX as u64
|
||||
} else {
|
||||
range_size_u512.as_u64()
|
||||
};
|
||||
|
||||
log::info!(
|
||||
target: "gpu_engine",
|
||||
"GPU {} search started: range {}..{}, nonces: {}, cancel check interval: {}",
|
||||
device_index,
|
||||
format_u512(range.start),
|
||||
format_u512(range.end),
|
||||
range_size,
|
||||
self.cancel_check_interval
|
||||
);
|
||||
|
||||
// Ensure resources are initialized for this thread
|
||||
WORKER_RESOURCES.with(|resources_cell| {
|
||||
let mut resources = resources_cell.borrow_mut();
|
||||
if resources.is_none() {
|
||||
*resources = Some(gpu_ctx.create_resources());
|
||||
}
|
||||
});
|
||||
|
||||
let resources = WORKER_RESOURCES
|
||||
.with(|resources_cell| resources_cell.borrow().as_ref().unwrap().clone());
|
||||
|
||||
// Pre-convert header and target
|
||||
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(
|
||||
&resources.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(
|
||||
&resources.target_buffer,
|
||||
0,
|
||||
bytemuck::cast_slice(&target_u32s),
|
||||
);
|
||||
|
||||
// Calculate dispatch configuration
|
||||
let threads_per_workgroup = 256u32;
|
||||
let limits = gpu_ctx.device.limits();
|
||||
let max_workgroups = limits.max_compute_workgroups_per_dimension;
|
||||
|
||||
let hinted_workgroups = gpu_ctx.optimal_workgroups.max(1).min(max_workgroups);
|
||||
let hinted_threads = hinted_workgroups as u64 * threads_per_workgroup as u64;
|
||||
|
||||
let logical_threads = range_size.min(hinted_threads).max(1);
|
||||
let num_workgroups = ((logical_threads as u32).div_ceil(threads_per_workgroup)).max(1);
|
||||
let total_threads = (num_workgroups * threads_per_workgroup) as u64;
|
||||
let nonces_per_thread = (range_size.div_ceil(total_threads)).max(1) as u32;
|
||||
|
||||
log::debug!(
|
||||
target: "gpu_engine",
|
||||
"GPU {} dispatch config: {} workgroups × {} threads, {} nonces/thread",
|
||||
device_index,
|
||||
num_workgroups,
|
||||
threads_per_workgroup,
|
||||
nonces_per_thread
|
||||
);
|
||||
|
||||
// Dispatch config: [total_threads, nonces_per_thread, total_nonces, cancel_check_interval]
|
||||
let dispatch_config = [
|
||||
total_threads as u32,
|
||||
nonces_per_thread,
|
||||
range_size as u32,
|
||||
self.cancel_check_interval,
|
||||
];
|
||||
|
||||
// Write dispatch config
|
||||
gpu_ctx.queue.write_buffer(
|
||||
&resources.dispatch_config_buffer,
|
||||
0,
|
||||
bytemuck::cast_slice(&dispatch_config),
|
||||
);
|
||||
|
||||
// Write start nonce
|
||||
let start_nonce_bytes = range.start.to_little_endian();
|
||||
gpu_ctx
|
||||
.queue
|
||||
.write_buffer(&resources.start_nonce_buffer, 0, &start_nonce_bytes);
|
||||
|
||||
// Reset results buffer
|
||||
const RESULTS_SIZE: usize = (1 + 16 + 16) * 4;
|
||||
const ZEROS: [u8; RESULTS_SIZE] = [0; RESULTS_SIZE];
|
||||
gpu_ctx
|
||||
.queue
|
||||
.write_buffer(&resources.results_buffer, 0, &ZEROS);
|
||||
|
||||
// Reset cancel buffer to 0 (not cancelled)
|
||||
gpu_ctx
|
||||
.queue
|
||||
.write_buffer(&resources.cancel_buffer, 0, &[0u8; 4]);
|
||||
|
||||
let search_start = std::time::Instant::now();
|
||||
|
||||
// Create and submit command buffer
|
||||
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, &resources.bind_group, &[]);
|
||||
cpass.dispatch_workgroups(num_workgroups, 1, 1);
|
||||
}
|
||||
encoder.copy_buffer_to_buffer(
|
||||
&resources.results_buffer,
|
||||
0,
|
||||
&resources.staging_buffer,
|
||||
0,
|
||||
RESULTS_SIZE as u64,
|
||||
);
|
||||
|
||||
gpu_ctx.queue.submit(Some(encoder.finish()));
|
||||
|
||||
// Poll GPU with periodic cancel checks
|
||||
// We use a shared flag to know when the mapping is complete
|
||||
let buffer_slice = resources.staging_buffer.slice(..);
|
||||
let mapped = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let mapped_clone = mapped.clone();
|
||||
buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
|
||||
if result.is_ok() {
|
||||
mapped_clone.store(true, Ordering::Release);
|
||||
}
|
||||
});
|
||||
|
||||
loop {
|
||||
// Check if cancelled
|
||||
if cancel.load(Ordering::Relaxed) {
|
||||
// Write cancel flag to GPU buffer
|
||||
gpu_ctx
|
||||
.queue
|
||||
.write_buffer(&resources.cancel_buffer, 0, &1u32.to_le_bytes());
|
||||
log::debug!(target: "gpu_engine", "GPU {} cancel flag propagated to GPU buffer", device_index);
|
||||
}
|
||||
|
||||
// Poll with short timeout
|
||||
let _ = gpu_ctx.device.poll(wgpu::PollType::Wait {
|
||||
submission_index: None,
|
||||
timeout: Some(std::time::Duration::from_millis(10)),
|
||||
});
|
||||
|
||||
// Check if buffer mapping is complete
|
||||
if mapped.load(Ordering::Acquire) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let search_elapsed = search_start.elapsed();
|
||||
|
||||
// Read results
|
||||
let data = buffer_slice.get_mapped_range();
|
||||
let result_u32s: &[u32] = bytemuck::cast_slice(&data);
|
||||
|
||||
let was_cancelled = cancel.load(Ordering::Relaxed);
|
||||
|
||||
// Calculate the actual number of nonces dispatched to the GPU
|
||||
// This is total_threads * nonces_per_thread, capped at range_size
|
||||
let dispatched_nonces = (total_threads * nonces_per_thread as u64).min(range_size);
|
||||
|
||||
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();
|
||||
|
||||
// Calculate hashes computed based on GPU parallel execution model.
|
||||
//
|
||||
// GPU threads process nonces in parallel, not sequentially:
|
||||
// - Thread T processes nonces: start + T*nonces_per_thread + 0, +1, +2, ...
|
||||
// - All threads run approximately in lockstep (SIMT execution)
|
||||
//
|
||||
// When thread T finds a solution at its iteration J:
|
||||
// - logical_index = nonce - start = T * nonces_per_thread + J
|
||||
// - winning_iteration = logical_index % nonces_per_thread = J
|
||||
// - All threads have progressed to approximately iteration J
|
||||
// - Total hashes ≈ total_threads * (J + 1)
|
||||
//
|
||||
// This gives a consistent hash rate regardless of which thread finds the solution.
|
||||
let hashes_computed = if nonce >= range.start {
|
||||
let logical_index = (nonce - range.start).as_u64();
|
||||
let winning_iteration = logical_index % (nonces_per_thread as u64);
|
||||
// All threads processed approximately (winning_iteration + 1) nonces each
|
||||
(total_threads * (winning_iteration + 1)).min(dispatched_nonces)
|
||||
} else {
|
||||
// Shouldn't happen, but fall back to dispatched count
|
||||
dispatched_nonces
|
||||
};
|
||||
|
||||
drop(data);
|
||||
resources.staging_buffer.unmap();
|
||||
|
||||
let hash_rate = hashes_computed as f64 / search_elapsed.as_secs_f64();
|
||||
|
||||
log::debug!(
|
||||
target: "gpu_engine",
|
||||
"GPU {} found solution! Nonce: {}, Hash: {} ({} hashes in {:.2}s, {})",
|
||||
device_index,
|
||||
format_u512(nonce),
|
||||
format_u512(hash),
|
||||
hashes_computed,
|
||||
search_elapsed.as_secs_f64(),
|
||||
format_hashrate(hash_rate)
|
||||
);
|
||||
|
||||
return EngineStatus::Found {
|
||||
candidate: Candidate { nonce, work, hash },
|
||||
hash_count: hashes_computed,
|
||||
origin: FoundOrigin::GpuG1,
|
||||
};
|
||||
}
|
||||
|
||||
drop(data);
|
||||
resources.staging_buffer.unmap();
|
||||
|
||||
if was_cancelled {
|
||||
// For cancelled jobs, estimate hashes based on elapsed time and dispatched work.
|
||||
// The shader checks cancel flag periodically, so we estimate based on how much
|
||||
// of the dispatch likely completed. This is approximate but better than 0.
|
||||
// We use the ratio of elapsed time to expected completion time.
|
||||
// As a simple heuristic, if the GPU was running, it was doing work.
|
||||
// We report dispatched_nonces as upper bound since the dispatch was submitted.
|
||||
// In practice, cancellation happens quickly so this is often close to 0 useful hashes.
|
||||
let estimated_hashes = dispatched_nonces;
|
||||
log::info!(
|
||||
target: "gpu_engine",
|
||||
"GPU {} search cancelled after {:.2}s (~{} hashes dispatched)",
|
||||
device_index,
|
||||
search_elapsed.as_secs_f64(),
|
||||
estimated_hashes
|
||||
);
|
||||
return EngineStatus::Cancelled {
|
||||
hash_count: estimated_hashes,
|
||||
};
|
||||
}
|
||||
|
||||
// Range exhausted without finding solution - all dispatched nonces were processed
|
||||
let hash_rate = dispatched_nonces as f64 / search_elapsed.as_secs_f64();
|
||||
log::info!(
|
||||
target: "gpu_engine",
|
||||
"GPU {} search exhausted: {} hashes in {:.2}s ({})",
|
||||
device_index,
|
||||
dispatched_nonces,
|
||||
search_elapsed.as_secs_f64(),
|
||||
format_hashrate(hash_rate)
|
||||
);
|
||||
|
||||
EngineStatus::Exhausted {
|
||||
hash_count: dispatched_nonces,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
177
crates/engine-gpu/src/main.rs
Normal file
177
crates/engine-gpu/src/main.rs
Normal 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_hash_squeeze_twice(&device, &queue).await {
|
||||
eprintln!("❌ Hash squeeze twice 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!(");");
|
||||
}
|
||||
914
crates/engine-gpu/src/mining.wgsl
Normal file
914
crates/engine-gpu/src/mining.wgsl
Normal file
@@ -0,0 +1,914 @@
|
||||
// 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 = 8u;
|
||||
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, nonces_per_thread, total_nonces, cancel_check_interval]
|
||||
@group(0) @binding(5) var<storage, read> cancel_flag: array<u32, 1>; // 0 = running, 1 = cancel requested
|
||||
|
||||
// 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: 24 from input + 1 terminator)
|
||||
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=8
|
||||
|
||||
// Process first 24 elements (3 complete chunks of 8)
|
||||
for (var chunk = 0u; chunk < 3u; chunk++) {
|
||||
// Absorb 8 elements for this chunk
|
||||
for (var i = 0u; i < 8u; i++) {
|
||||
let felt_idx = chunk * 8u + 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 terminator = 1) remaining
|
||||
state[0] = gf_add(state[0], input_felts[24u]); // Add the terminator (value 1)
|
||||
|
||||
// Add sponge padding marker (ONE) to the next position
|
||||
state[1] = gf_add(state[1], gf_one());
|
||||
|
||||
// Final permutation
|
||||
poseidon2_permute(&state);
|
||||
|
||||
// First squeeze - get first 4 field elements (POSEIDON2_OUTPUT=4 -> 32 bytes)
|
||||
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;
|
||||
}
|
||||
|
||||
// Single Poseidon2 hash with double squeeze (512-bit output)
|
||||
fn hash_squeeze_twice(input: array<u32, 24>) -> array<u32, 16> {
|
||||
return poseidon2_hash_squeeze_twice(input);
|
||||
}
|
||||
|
||||
// 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 or cancelled, exit early
|
||||
if (atomicLoad(&results[0]) != 0u) {
|
||||
return;
|
||||
}
|
||||
if (cancel_flag[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
|
||||
let cancel_check_interval = dispatch_config[3]; // How often to check cancel flag
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Periodic checks for early exit
|
||||
// Check cancel flag every cancel_check_interval iterations
|
||||
if (cancel_check_interval > 0u && (j % cancel_check_interval) == 0u) {
|
||||
if (cancel_flag[0] != 0u) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 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 = hash_squeeze_twice(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
|
||||
}
|
||||
}
|
||||
}
|
||||
4521
crates/engine-gpu/src/tests.rs
Normal file
4521
crates/engine-gpu/src/tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -6,11 +6,9 @@ publish = false
|
||||
description = "Metrics and Prometheus exporter utilities for the Quantus External Miner"
|
||||
|
||||
[features]
|
||||
# Provide a Registry by default; HTTP exporter is opt-in.
|
||||
default = ["registry"]
|
||||
registry = []
|
||||
default = []
|
||||
# Enable a Warp-based HTTP exporter (used when --metrics-port is provided).
|
||||
http-exporter = ["dep:serde", "dep:serde_json", "dep:tokio", "dep:warp"]
|
||||
http-exporter = ["dep:tokio", "dep:warp"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
@@ -21,5 +19,3 @@ prometheus = { version = "0.13", default-features = false }
|
||||
# Optional HTTP exporter stack (enabled via `http-exporter` feature)
|
||||
warp = { workspace = true, optional = true }
|
||||
tokio = { workspace = true, features = ["full"], optional = true }
|
||||
serde = { workspace = true, features = ["derive"], optional = true }
|
||||
serde_json = { workspace = true, optional = true }
|
||||
|
||||
@@ -1,59 +1,91 @@
|
||||
#![deny(rust_2018_idioms)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! Minimal metrics scaffolding for the Quantus External Miner.
|
||||
//! Minimal metrics for the Quantus External Miner.
|
||||
//!
|
||||
//! - Provides a global Prometheus registry and a handful of default metrics.
|
||||
//! - Exposes helper functions to update metrics from the service layer.
|
||||
//! - Optionally runs a Warp-based HTTP endpoint (/metrics) when the
|
||||
//! `http-exporter` feature is enabled. This is gated at runtime by
|
||||
//! the presence of a metrics port in the CLI (`--metrics-port`).
|
||||
//! Exposes a small set of Prometheus metrics for monitoring mining performance:
|
||||
//! - `miner_hash_rate`: Total hash rate (CPU + GPU combined)
|
||||
//! - `miner_cpu_hash_rate`: CPU-only hash rate
|
||||
//! - `miner_gpu_hash_rate`: GPU-only hash rate
|
||||
//! - `miner_hashes_total`: Total hashes computed (all time)
|
||||
//! - `miner_active_jobs`: Currently running jobs (0 or 1)
|
||||
//! - `miner_workers`: Total worker count
|
||||
//! - `miner_cpu_workers`: Number of CPU workers
|
||||
//! - `miner_gpu_devices`: Number of GPU devices
|
||||
//! - `miner_effective_cpus`: Detected CPU cores
|
||||
//!
|
||||
//! Default metrics:
|
||||
//! - miner_jobs_total{status} : number of jobs by terminal state
|
||||
//! - miner_hashes_total : total nonces tested
|
||||
//! - miner_hash_rate : current estimated hash rate (nonces/sec)
|
||||
//! - miner_http_requests_total{code,endpoint} : HTTP request counts for miner API
|
||||
//!
|
||||
//! Notes:
|
||||
//! - The HTTP exporter is spawned as a background task and does not block.
|
||||
//! - If the `http-exporter` feature is disabled, `start_http_exporter` becomes
|
||||
//! a no-op that returns immediately.
|
||||
//! - When jobs/threads end, prefer removing gauge label children (series) rather than
|
||||
//! writing a zero; this avoids scrape-timing artifacts and produces cleaner rollups.
|
||||
//! - Service-level observability: expose an `active_jobs` gauge and a `mine_requests_total`
|
||||
//! counter (labeled by result) to disambiguate "idle because no jobs" vs "actively mining".
|
||||
//! Optionally runs a Warp-based HTTP endpoint (`/metrics`) when the
|
||||
//! `http-exporter` feature is enabled.
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use prometheus::{
|
||||
opts, Encoder, Gauge, GaugeVec, IntCounter, IntCounterVec, IntGauge, IntGaugeVec, Registry,
|
||||
TextEncoder,
|
||||
};
|
||||
use prometheus::{IntCounter, IntGauge, Registry};
|
||||
use std::sync::Mutex;
|
||||
use std::time::Instant;
|
||||
|
||||
#[cfg(feature = "http-exporter")]
|
||||
use {
|
||||
anyhow::Result,
|
||||
prometheus::{Encoder, TextEncoder},
|
||||
std::net::SocketAddr,
|
||||
warp::{http::Response, Filter},
|
||||
warp::Filter,
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "http-exporter"))]
|
||||
use anyhow::Result;
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Mutex;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
// -------------------------------------------------------------------------------------
|
||||
// Global Registry and Default Metrics
|
||||
// -------------------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// Global Registry
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static REGISTRY: Lazy<Registry> = Lazy::new(Registry::new);
|
||||
|
||||
// -------------------------------------------------------------------------------------
|
||||
// High-level service metrics
|
||||
// -------------------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hash Rate Metrics
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static HASH_RATE: Lazy<IntGauge> = Lazy::new(|| {
|
||||
let g = IntGauge::new("miner_hash_rate", "Total hash rate in hashes per second")
|
||||
.expect("create miner_hash_rate");
|
||||
REGISTRY
|
||||
.register(Box::new(g.clone()))
|
||||
.expect("register miner_hash_rate");
|
||||
g
|
||||
});
|
||||
|
||||
static CPU_HASH_RATE: Lazy<IntGauge> = Lazy::new(|| {
|
||||
let g = IntGauge::new("miner_cpu_hash_rate", "CPU hash rate in hashes per second")
|
||||
.expect("create miner_cpu_hash_rate");
|
||||
REGISTRY
|
||||
.register(Box::new(g.clone()))
|
||||
.expect("register miner_cpu_hash_rate");
|
||||
g
|
||||
});
|
||||
|
||||
static GPU_HASH_RATE: Lazy<IntGauge> = Lazy::new(|| {
|
||||
let g = IntGauge::new("miner_gpu_hash_rate", "GPU hash rate in hashes per second")
|
||||
.expect("create miner_gpu_hash_rate");
|
||||
REGISTRY
|
||||
.register(Box::new(g.clone()))
|
||||
.expect("register miner_gpu_hash_rate");
|
||||
g
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Counter Metrics
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static HASHES_TOTAL: Lazy<IntCounter> = Lazy::new(|| {
|
||||
let c = IntCounter::new("miner_hashes_total", "Total hashes computed")
|
||||
.expect("create miner_hashes_total");
|
||||
REGISTRY
|
||||
.register(Box::new(c.clone()))
|
||||
.expect("register miner_hashes_total");
|
||||
c
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Gauge Metrics
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static ACTIVE_JOBS: Lazy<IntGauge> = Lazy::new(|| {
|
||||
let g = IntGauge::new(
|
||||
@@ -67,25 +99,37 @@ static ACTIVE_JOBS: Lazy<IntGauge> = Lazy::new(|| {
|
||||
g
|
||||
});
|
||||
|
||||
static ENGINE_BACKEND_INFO: Lazy<GaugeVec> = Lazy::new(|| {
|
||||
let g = GaugeVec::new(
|
||||
opts!(
|
||||
"miner_engine_backend",
|
||||
"Engine backend info (label-only gauge set to 1). Labels: engine, backend"
|
||||
),
|
||||
&["engine", "backend"],
|
||||
)
|
||||
.expect("create miner_engine_backend");
|
||||
static WORKERS: Lazy<IntGauge> = Lazy::new(|| {
|
||||
let g = IntGauge::new("miner_workers", "Total number of worker threads")
|
||||
.expect("create miner_workers");
|
||||
REGISTRY
|
||||
.register(Box::new(g.clone()))
|
||||
.expect("register miner_engine_backend");
|
||||
.expect("register miner_workers");
|
||||
g
|
||||
});
|
||||
|
||||
static CPU_WORKERS: Lazy<IntGauge> = Lazy::new(|| {
|
||||
let g = IntGauge::new("miner_cpu_workers", "Number of CPU worker threads")
|
||||
.expect("create miner_cpu_workers");
|
||||
REGISTRY
|
||||
.register(Box::new(g.clone()))
|
||||
.expect("register miner_cpu_workers");
|
||||
g
|
||||
});
|
||||
|
||||
static GPU_DEVICES: Lazy<IntGauge> = Lazy::new(|| {
|
||||
let g = IntGauge::new("miner_gpu_devices", "Number of GPU devices")
|
||||
.expect("create miner_gpu_devices");
|
||||
REGISTRY
|
||||
.register(Box::new(g.clone()))
|
||||
.expect("register miner_gpu_devices");
|
||||
g
|
||||
});
|
||||
|
||||
static EFFECTIVE_CPUS: Lazy<IntGauge> = Lazy::new(|| {
|
||||
let g = IntGauge::new(
|
||||
"miner_effective_cpus",
|
||||
"Detected logical CPU capacity available to this process (cpuset-aware)",
|
||||
"Detected logical CPU cores available to this process",
|
||||
)
|
||||
.expect("create miner_effective_cpus");
|
||||
REGISTRY
|
||||
@@ -94,583 +138,172 @@ static EFFECTIVE_CPUS: Lazy<IntGauge> = Lazy::new(|| {
|
||||
g
|
||||
});
|
||||
|
||||
static MINE_REQUESTS_TOTAL: Lazy<IntCounterVec> = Lazy::new(|| {
|
||||
let c = IntCounterVec::new(
|
||||
opts!(
|
||||
"miner_mine_requests_total",
|
||||
"Count of /mine requests by result"
|
||||
),
|
||||
&["result"], // accepted, duplicate, invalid, error
|
||||
)
|
||||
.expect("create miner_mine_requests_total");
|
||||
REGISTRY
|
||||
.register(Box::new(c.clone()))
|
||||
.expect("register miner_mine_requests_total");
|
||||
c
|
||||
});
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hash Rate Tracking
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static JOBS_TOTAL: Lazy<IntCounterVec> = Lazy::new(|| {
|
||||
let c = IntCounterVec::new(
|
||||
opts!("miner_jobs_total", "Number of jobs by status"),
|
||||
&["status"],
|
||||
)
|
||||
.expect("create miner_jobs_total");
|
||||
REGISTRY
|
||||
.register(Box::new(c.clone()))
|
||||
.expect("register miner_jobs_total");
|
||||
c
|
||||
});
|
||||
|
||||
static HASHES_TOTAL: Lazy<IntCounter> = Lazy::new(|| {
|
||||
let c = IntCounter::new("miner_hashes_total", "Total nonces tested")
|
||||
.expect("create miner_hashes_total");
|
||||
REGISTRY
|
||||
.register(Box::new(c.clone()))
|
||||
.expect("register miner_hashes_total");
|
||||
c
|
||||
});
|
||||
|
||||
static HASH_RATE: Lazy<Gauge> = Lazy::new(|| {
|
||||
let g =
|
||||
Gauge::new("miner_hash_rate", "Estimated hash rate (nonces per second)").expect("create");
|
||||
REGISTRY
|
||||
.register(Box::new(g.clone()))
|
||||
.expect("register miner_hash_rate");
|
||||
g
|
||||
});
|
||||
|
||||
static HTTP_REQUESTS_TOTAL: Lazy<IntCounterVec> = Lazy::new(|| {
|
||||
let c = IntCounterVec::new(
|
||||
opts!(
|
||||
"miner_http_requests_total",
|
||||
"HTTP requests count by endpoint and status code"
|
||||
),
|
||||
&["endpoint", "code"],
|
||||
)
|
||||
.expect("create miner_http_requests_total");
|
||||
REGISTRY
|
||||
.register(Box::new(c.clone()))
|
||||
.expect("register miner_http_requests_total");
|
||||
c
|
||||
});
|
||||
|
||||
/// Access the global Prometheus registry used by this crate.
|
||||
// Engine-aware, per-job, and per-thread labeled metrics
|
||||
pub fn set_effective_cpus(n: i64) {
|
||||
EFFECTIVE_CPUS.set(n);
|
||||
/// Tracks cumulative hashes to compute rolling hash rates.
|
||||
///
|
||||
/// The hash rate is computed as total_hashes / elapsed_time since mining started.
|
||||
/// Call `reset()` when a new mining session begins to get accurate rates.
|
||||
struct HashRateTracker {
|
||||
/// Cumulative CPU hashes since last reset
|
||||
cpu_total: u64,
|
||||
/// Cumulative GPU hashes since last reset
|
||||
gpu_total: u64,
|
||||
/// When tracking started - set via reset() when mining begins
|
||||
start_time: Option<Instant>,
|
||||
}
|
||||
|
||||
static JOB_HASHES_TOTAL: Lazy<IntCounterVec> = Lazy::new(|| {
|
||||
let c = IntCounterVec::new(
|
||||
opts!(
|
||||
"miner_job_hashes_total",
|
||||
"Total nonces tested per job and engine"
|
||||
),
|
||||
&["engine", "job_id"],
|
||||
)
|
||||
.expect("create miner_job_hashes_total");
|
||||
REGISTRY
|
||||
.register(Box::new(c.clone()))
|
||||
.expect("register miner_job_hashes_total");
|
||||
c
|
||||
});
|
||||
impl HashRateTracker {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
cpu_total: 0,
|
||||
gpu_total: 0,
|
||||
start_time: None,
|
||||
}
|
||||
}
|
||||
|
||||
static JOB_HASH_RATE: Lazy<GaugeVec> = Lazy::new(|| {
|
||||
let g = GaugeVec::new(
|
||||
opts!(
|
||||
"miner_job_hash_rate",
|
||||
"Estimated hash rate (nonces per second) per job and engine"
|
||||
),
|
||||
&["engine", "job_id"],
|
||||
)
|
||||
.expect("create miner_job_hash_rate");
|
||||
REGISTRY
|
||||
.register(Box::new(g.clone()))
|
||||
.expect("register miner_job_hash_rate");
|
||||
g
|
||||
});
|
||||
/// Reset the tracker - call this when a new mining session starts.
|
||||
fn reset(&mut self) {
|
||||
self.cpu_total = 0;
|
||||
self.gpu_total = 0;
|
||||
self.start_time = Some(Instant::now());
|
||||
}
|
||||
|
||||
static JOB_ESTIMATED_RATE: Lazy<GaugeVec> = Lazy::new(|| {
|
||||
let g = GaugeVec::new(
|
||||
opts!(
|
||||
"miner_job_estimated_rate",
|
||||
"Estimated work rate (nonces per second) per engine and backend"
|
||||
),
|
||||
&["engine", "backend"],
|
||||
)
|
||||
.expect("create miner_job_estimated_rate");
|
||||
REGISTRY
|
||||
.register(Box::new(g.clone()))
|
||||
.expect("register miner_job_estimated_rate");
|
||||
g
|
||||
});
|
||||
fn record_cpu(&mut self, hashes: u64) {
|
||||
self.cpu_total += hashes;
|
||||
self.update_rates();
|
||||
}
|
||||
|
||||
static CANDIDATES_FOUND_TOTAL: Lazy<IntCounterVec> = Lazy::new(|| {
|
||||
let c = IntCounterVec::new(
|
||||
opts!(
|
||||
"miner_candidates_found_total",
|
||||
"Total candidates found per job and engine"
|
||||
),
|
||||
&["engine", "job_id"],
|
||||
)
|
||||
.expect("create miner_candidates_found_total");
|
||||
REGISTRY
|
||||
.register(Box::new(c.clone()))
|
||||
.expect("register miner_candidates_found_total");
|
||||
c
|
||||
});
|
||||
fn record_gpu(&mut self, hashes: u64) {
|
||||
self.gpu_total += hashes;
|
||||
self.update_rates();
|
||||
}
|
||||
|
||||
static CANDIDATES_FALSE_POSITIVE_TOTAL: Lazy<IntCounterVec> = Lazy::new(|| {
|
||||
let c = IntCounterVec::new(
|
||||
opts!(
|
||||
"miner_candidates_false_positive_total",
|
||||
"Total false-positive candidates rejected by host re-verification per engine"
|
||||
),
|
||||
&["engine"],
|
||||
)
|
||||
.expect("create miner_candidates_false_positive_total");
|
||||
REGISTRY
|
||||
.register(Box::new(c.clone()))
|
||||
.expect("register miner_candidates_false_positive_total");
|
||||
c
|
||||
});
|
||||
|
||||
static SAMPLE_MISMATCH_TOTAL: Lazy<IntCounterVec> = Lazy::new(|| {
|
||||
let c = IntCounterVec::new(
|
||||
opts!(
|
||||
"miner_sample_mismatch_total",
|
||||
"Total decision parity mismatches between engine and host per engine"
|
||||
),
|
||||
&["engine"],
|
||||
)
|
||||
.expect("create miner_sample_mismatch_total");
|
||||
REGISTRY
|
||||
.register(Box::new(c.clone()))
|
||||
.expect("register miner_sample_mismatch_total");
|
||||
c
|
||||
});
|
||||
|
||||
static MISSED_WINNER_TOTAL: Lazy<IntCounter> = Lazy::new(|| {
|
||||
let c = IntCounter::new(
|
||||
"miner_gpu_g2_missed_winner_total",
|
||||
"Host detected a winner in G2 batch sample but device did not flag early-exit",
|
||||
)
|
||||
.expect("create miner_gpu_g2_missed_winner_total");
|
||||
REGISTRY
|
||||
.register(Box::new(c.clone()))
|
||||
.expect("register miner_gpu_g2_missed_winner_total");
|
||||
c
|
||||
});
|
||||
|
||||
static FOUND_BY_ORIGIN_TOTAL: Lazy<IntCounterVec> = Lazy::new(|| {
|
||||
let c = IntCounterVec::new(
|
||||
opts!(
|
||||
"miner_found_by_origin_total",
|
||||
"Count of candidates found by origin per engine"
|
||||
),
|
||||
&["engine", "origin"],
|
||||
)
|
||||
.expect("create miner_found_by_origin_total");
|
||||
REGISTRY
|
||||
.register(Box::new(c.clone()))
|
||||
.expect("register miner_found_by_origin_total");
|
||||
c
|
||||
});
|
||||
|
||||
static THREAD_HASHES_TOTAL: Lazy<IntCounterVec> = Lazy::new(|| {
|
||||
let c = IntCounterVec::new(
|
||||
opts!(
|
||||
"miner_thread_hashes_total",
|
||||
"Total nonces tested per thread, job, and engine"
|
||||
),
|
||||
&["engine", "job_id", "thread_id"],
|
||||
)
|
||||
.expect("create miner_thread_hashes_total");
|
||||
REGISTRY
|
||||
.register(Box::new(c.clone()))
|
||||
.expect("register miner_thread_hashes_total");
|
||||
c
|
||||
});
|
||||
|
||||
static THREAD_HASH_RATE: Lazy<GaugeVec> = Lazy::new(|| {
|
||||
let g = GaugeVec::new(
|
||||
opts!(
|
||||
"miner_thread_hash_rate",
|
||||
"Estimated hash rate (nonces per second) per thread, job, and engine"
|
||||
),
|
||||
&["engine", "job_id", "thread_id"],
|
||||
)
|
||||
.expect("create miner_thread_hash_rate");
|
||||
REGISTRY
|
||||
.register(Box::new(g.clone()))
|
||||
.expect("register miner_thread_hash_rate");
|
||||
g
|
||||
});
|
||||
|
||||
static JOBS_BY_ENGINE_TOTAL: Lazy<IntCounterVec> = Lazy::new(|| {
|
||||
let c = IntCounterVec::new(
|
||||
opts!(
|
||||
"miner_jobs_by_engine_total",
|
||||
"Number of jobs by engine and terminal status"
|
||||
),
|
||||
&["engine", "status"],
|
||||
)
|
||||
.expect("create miner_jobs_by_engine_total");
|
||||
REGISTRY
|
||||
.register(Box::new(c.clone()))
|
||||
.expect("register miner_jobs_by_engine_total");
|
||||
c
|
||||
});
|
||||
|
||||
static JOB_STATUS_GAUGE: Lazy<IntGaugeVec> = Lazy::new(|| {
|
||||
let g = IntGaugeVec::new(
|
||||
opts!(
|
||||
"miner_job_status",
|
||||
"Job status gauge (set to 1 for current status)"
|
||||
),
|
||||
&["engine", "job_id", "status"],
|
||||
)
|
||||
.expect("create miner_job_status");
|
||||
REGISTRY
|
||||
.register(Box::new(g.clone()))
|
||||
.expect("register miner_job_status");
|
||||
g
|
||||
});
|
||||
|
||||
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)"
|
||||
),
|
||||
&["engine", "job_id", "origin"],
|
||||
)
|
||||
.expect("create miner_job_found_origin");
|
||||
REGISTRY
|
||||
.register(Box::new(g.clone()))
|
||||
.expect("register miner_job_found_origin");
|
||||
g
|
||||
});
|
||||
|
||||
pub fn default_registry() -> &'static Registry {
|
||||
®ISTRY
|
||||
fn update_rates(&self) {
|
||||
if let Some(start) = self.start_time {
|
||||
let elapsed = start.elapsed().as_secs_f64();
|
||||
// Require at least 0.1 seconds of data for meaningful rate
|
||||
if elapsed >= 0.1 {
|
||||
let cpu_rate = (self.cpu_total as f64 / elapsed) as i64;
|
||||
let gpu_rate = (self.gpu_total as f64 / elapsed) as i64;
|
||||
CPU_HASH_RATE.set(cpu_rate);
|
||||
GPU_HASH_RATE.set(gpu_rate);
|
||||
HASH_RATE.set(cpu_rate + gpu_rate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------
|
||||
// Update Helpers (to be called from the service layer)
|
||||
// -------------------------------------------------------------------------------------
|
||||
static HASH_TRACKER: Lazy<Mutex<HashRateTracker>> =
|
||||
Lazy::new(|| Mutex::new(HashRateTracker::new()));
|
||||
|
||||
/// Increment the total hashes counter by `n` (number of nonces tested).
|
||||
// Labeled helpers for engine/job/thread metrics
|
||||
//
|
||||
// Service-level helpers
|
||||
pub fn set_active_jobs(n: i64) {
|
||||
ACTIVE_JOBS.set(n);
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API - Hash Recording
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Reset the hash rate tracker - call this when mining starts.
|
||||
/// This resets the cumulative hash counts and starts a fresh timing window.
|
||||
pub fn reset_hash_tracker() {
|
||||
if let Ok(mut tracker) = HASH_TRACKER.lock() {
|
||||
tracker.reset();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_engine_backend(engine: &str, backend: &str) {
|
||||
ENGINE_BACKEND_INFO
|
||||
.with_label_values(&[engine, backend])
|
||||
.set(1.0);
|
||||
/// Record CPU hashes and update hash rate metrics.
|
||||
pub fn record_cpu_hashes(n: u64) {
|
||||
HASHES_TOTAL.inc_by(n);
|
||||
if let Ok(mut tracker) = HASH_TRACKER.lock() {
|
||||
tracker.record_cpu(n);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inc_mine_requests(result: &str) {
|
||||
MINE_REQUESTS_TOTAL.with_label_values(&[result]).inc();
|
||||
/// Record GPU hashes and update hash rate metrics.
|
||||
pub fn record_gpu_hashes(n: u64) {
|
||||
HASHES_TOTAL.inc_by(n);
|
||||
if let Ok(mut tracker) = HASH_TRACKER.lock() {
|
||||
tracker.record_gpu(n);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inc_job_hashes(engine: &str, job_id: &str, n: u64) {
|
||||
touch_job(engine, job_id);
|
||||
JOB_HASHES_TOTAL
|
||||
.with_label_values(&[engine, job_id])
|
||||
.inc_by(n);
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API - Hash Rates (for direct setting, kept for compatibility)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Set the total hash rate (CPU + GPU combined).
|
||||
pub fn set_hash_rate(rate: i64) {
|
||||
HASH_RATE.set(rate);
|
||||
}
|
||||
|
||||
pub fn set_job_hash_rate(engine: &str, job_id: &str, rate: f64) {
|
||||
touch_job(engine, job_id);
|
||||
JOB_HASH_RATE.with_label_values(&[engine, job_id]).set(rate);
|
||||
/// Set the CPU-only hash rate.
|
||||
pub fn set_cpu_hash_rate(rate: i64) {
|
||||
CPU_HASH_RATE.set(rate);
|
||||
}
|
||||
|
||||
pub fn set_job_estimated_rate_backend(engine: &str, backend: &str, rate: f64) {
|
||||
JOB_ESTIMATED_RATE
|
||||
.with_label_values(&[engine, backend])
|
||||
.set(rate);
|
||||
/// Set the GPU-only hash rate.
|
||||
pub fn set_gpu_hash_rate(rate: i64) {
|
||||
GPU_HASH_RATE.set(rate);
|
||||
}
|
||||
|
||||
pub fn set_job_estimated_rate(engine: &str, _job_id: &str, rate: f64) {
|
||||
// Compatibility shim: map per-job series to backend="unknown" to avoid cardinality growth
|
||||
JOB_ESTIMATED_RATE
|
||||
.with_label_values(&[engine, "unknown"])
|
||||
.set(rate);
|
||||
}
|
||||
|
||||
pub fn inc_candidates_found(engine: &str, job_id: &str) {
|
||||
touch_job(engine, job_id);
|
||||
CANDIDATES_FOUND_TOTAL
|
||||
.with_label_values(&[engine, job_id])
|
||||
.inc();
|
||||
}
|
||||
|
||||
pub fn inc_candidates_false_positive(engine: &str) {
|
||||
CANDIDATES_FALSE_POSITIVE_TOTAL
|
||||
.with_label_values(&[engine])
|
||||
.inc();
|
||||
}
|
||||
|
||||
pub fn inc_sample_mismatch(engine: &str) {
|
||||
SAMPLE_MISMATCH_TOTAL.with_label_values(&[engine]).inc();
|
||||
}
|
||||
|
||||
pub fn inc_found_by_origin(engine: &str, origin: &str) {
|
||||
FOUND_BY_ORIGIN_TOTAL
|
||||
.with_label_values(&[engine, origin])
|
||||
.inc();
|
||||
}
|
||||
|
||||
pub fn inc_gpu_g2_missed_winner() {
|
||||
MISSED_WINNER_TOTAL.inc();
|
||||
}
|
||||
|
||||
pub fn set_job_found_origin(engine: &str, job_id: &str, origin: &str) {
|
||||
touch_job(engine, job_id);
|
||||
JOB_FOUND_ORIGIN
|
||||
.with_label_values(&[engine, job_id, origin])
|
||||
.set(1.0);
|
||||
}
|
||||
|
||||
pub fn job_estimated_rate_backend(engine: &str, backend: &str, rate: f64) {
|
||||
JOB_ESTIMATED_RATE
|
||||
.with_label_values(&[engine, backend])
|
||||
.set(rate);
|
||||
}
|
||||
|
||||
pub fn job_estimated_rate(engine: &str, _job_id: &str, rate: f64) {
|
||||
// Compatibility shim: prefer new API using backend; fallback to backend="unknown"
|
||||
JOB_ESTIMATED_RATE
|
||||
.with_label_values(&[engine, "unknown"])
|
||||
.set(rate);
|
||||
}
|
||||
|
||||
pub fn inc_thread_hashes(engine: &str, job_id: &str, thread_id: &str, n: u64) {
|
||||
THREAD_HASHES_TOTAL
|
||||
.with_label_values(&[engine, job_id, thread_id])
|
||||
.inc_by(n);
|
||||
}
|
||||
|
||||
pub fn set_thread_hash_rate(engine: &str, job_id: &str, thread_id: &str, rate: f64) {
|
||||
touch_thread(engine, job_id, thread_id);
|
||||
THREAD_HASH_RATE
|
||||
.with_label_values(&[engine, job_id, thread_id])
|
||||
.set(rate);
|
||||
}
|
||||
|
||||
pub fn inc_jobs_by_engine(engine: &str, status: &str) {
|
||||
JOBS_BY_ENGINE_TOTAL
|
||||
.with_label_values(&[engine, status])
|
||||
.inc();
|
||||
}
|
||||
|
||||
pub fn set_job_status_gauge(engine: &str, job_id: &str, status: &str, value: i64) {
|
||||
touch_job(engine, job_id);
|
||||
JOB_STATUS_GAUGE
|
||||
.with_label_values(&[engine, job_id, status])
|
||||
.set(value);
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API - Counters (kept for compatibility)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Increment the total hashes counter (without updating rates).
|
||||
/// Prefer `record_cpu_hashes` or `record_gpu_hashes` instead.
|
||||
pub fn inc_hashes(n: u64) {
|
||||
HASHES_TOTAL.inc_by(n);
|
||||
}
|
||||
|
||||
/// Set the current estimated hash rate (nonces per second).
|
||||
pub fn set_hash_rate(rate: f64) {
|
||||
HASH_RATE.set(rate);
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API - Gauges
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Set the number of active jobs (0 or 1).
|
||||
pub fn set_active_jobs(n: i64) {
|
||||
ACTIVE_JOBS.set(n);
|
||||
}
|
||||
|
||||
/// Increment the jobs counter for a terminal status: completed | failed | cancelled.
|
||||
pub fn inc_job_status(status: &str) {
|
||||
JOBS_TOTAL.with_label_values(&[status]).inc();
|
||||
/// Set the total number of workers.
|
||||
pub fn set_workers(n: i64) {
|
||||
WORKERS.set(n);
|
||||
}
|
||||
|
||||
/// Increment HTTP request counters for an endpoint with a status code.
|
||||
pub fn inc_http_request(endpoint: &str, code: u16) {
|
||||
HTTP_REQUESTS_TOTAL
|
||||
.with_label_values(&[endpoint, &code.to_string()])
|
||||
.inc();
|
||||
/// Set the number of CPU workers.
|
||||
pub fn set_cpu_workers(n: i64) {
|
||||
CPU_WORKERS.set(n);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------
|
||||
// Removal helpers for end-of-life series
|
||||
// -------------------------------------------------------------------------------------
|
||||
|
||||
const METRICS_TTL_SECS: u64 = 300;
|
||||
const JANITOR_INTERVAL_SECS: u64 = 60;
|
||||
|
||||
type JobKey = (String, String); // (engine, job_id)
|
||||
type ThreadKey = (String, String, String); // (engine, job_id, thread_id)
|
||||
|
||||
static JOB_KEYS: Lazy<Mutex<HashSet<JobKey>>> = Lazy::new(|| Mutex::new(HashSet::new()));
|
||||
static JOB_LAST: Lazy<Mutex<HashMap<JobKey, Instant>>> = Lazy::new(|| Mutex::new(HashMap::new()));
|
||||
static THREAD_KEYS: Lazy<Mutex<HashSet<ThreadKey>>> = Lazy::new(|| Mutex::new(HashSet::new()));
|
||||
static THREAD_LAST: Lazy<Mutex<HashMap<ThreadKey, Instant>>> =
|
||||
Lazy::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
static JANITOR_INIT: Lazy<()> = Lazy::new(|| {
|
||||
thread::spawn(|| loop {
|
||||
thread::sleep(Duration::from_secs(JANITOR_INTERVAL_SECS));
|
||||
prune_stale();
|
||||
});
|
||||
});
|
||||
|
||||
fn prune_stale() {
|
||||
let now = Instant::now();
|
||||
// Jobs
|
||||
let mut to_remove_jobs: Vec<JobKey> = Vec::new();
|
||||
{
|
||||
let last = JOB_LAST.lock().unwrap();
|
||||
for (k, ts) in last.iter() {
|
||||
if now.duration_since(*ts).as_secs() > METRICS_TTL_SECS {
|
||||
to_remove_jobs.push(k.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
for (engine, job_id) in to_remove_jobs {
|
||||
let _ = JOB_HASHES_TOTAL.remove_label_values(&[&engine, &job_id]);
|
||||
let _ = JOB_HASH_RATE.remove_label_values(&[&engine, &job_id]);
|
||||
let _ = CANDIDATES_FOUND_TOTAL.remove_label_values(&[&engine, &job_id]);
|
||||
let _ = JOB_STATUS_GAUGE.remove_label_values(&[&engine, &job_id, "completed"]);
|
||||
let _ = JOB_STATUS_GAUGE.remove_label_values(&[&engine, &job_id, "failed"]);
|
||||
let _ = JOB_STATUS_GAUGE.remove_label_values(&[&engine, &job_id, "cancelled"]);
|
||||
// Remove per-job origin gauge variants (known origins)
|
||||
let _ = JOB_FOUND_ORIGIN.remove_label_values(&[&engine, &job_id, "cpu"]);
|
||||
let _ = JOB_FOUND_ORIGIN.remove_label_values(&[&engine, &job_id, "gpu-g1"]);
|
||||
let _ = JOB_FOUND_ORIGIN.remove_label_values(&[&engine, &job_id, "gpu-g2"]);
|
||||
JOB_KEYS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove(&(engine.clone(), job_id.clone()));
|
||||
JOB_LAST.lock().unwrap().remove(&(engine, job_id));
|
||||
}
|
||||
// Threads
|
||||
let mut to_remove_threads: Vec<ThreadKey> = Vec::new();
|
||||
{
|
||||
let last = THREAD_LAST.lock().unwrap();
|
||||
for (k, ts) in last.iter() {
|
||||
if now.duration_since(*ts).as_secs() > METRICS_TTL_SECS {
|
||||
to_remove_threads.push(k.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
for (engine, job_id, thread_id) in to_remove_threads {
|
||||
let _ = THREAD_HASHES_TOTAL.remove_label_values(&[&engine, &job_id, &thread_id]);
|
||||
let _ = THREAD_HASH_RATE.remove_label_values(&[&engine, &job_id, &thread_id]);
|
||||
THREAD_KEYS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove(&(engine.clone(), job_id.clone(), thread_id.clone()));
|
||||
THREAD_LAST
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove(&(engine, job_id, thread_id));
|
||||
}
|
||||
/// Set the number of GPU devices.
|
||||
pub fn set_gpu_devices(n: i64) {
|
||||
GPU_DEVICES.set(n);
|
||||
}
|
||||
|
||||
fn touch_job(engine: &str, job_id: &str) {
|
||||
*JANITOR_INIT; // ensure janitor starts
|
||||
let key = (engine.to_string(), job_id.to_string());
|
||||
JOB_KEYS.lock().unwrap().insert(key.clone());
|
||||
JOB_LAST.lock().unwrap().insert(key, Instant::now());
|
||||
/// Set the detected CPU core count.
|
||||
pub fn set_effective_cpus(n: i64) {
|
||||
EFFECTIVE_CPUS.set(n);
|
||||
}
|
||||
|
||||
fn touch_thread(engine: &str, job_id: &str, thread_id: &str) {
|
||||
*JANITOR_INIT; // ensure janitor starts
|
||||
let key = (
|
||||
engine.to_string(),
|
||||
job_id.to_string(),
|
||||
thread_id.to_string(),
|
||||
);
|
||||
THREAD_KEYS.lock().unwrap().insert(key.clone());
|
||||
THREAD_LAST.lock().unwrap().insert(key, Instant::now());
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// HTTP Exporter
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Explicitly remove all job-scoped metrics for a job (call on job completion)
|
||||
pub fn remove_job_metrics(engine: &str, job_id: &str) {
|
||||
let _ = JOB_HASHES_TOTAL.remove_label_values(&[engine, job_id]);
|
||||
let _ = JOB_HASH_RATE.remove_label_values(&[engine, job_id]);
|
||||
let _ = CANDIDATES_FOUND_TOTAL.remove_label_values(&[engine, job_id]);
|
||||
let _ = JOB_STATUS_GAUGE.remove_label_values(&[engine, job_id, "completed"]);
|
||||
let _ = JOB_STATUS_GAUGE.remove_label_values(&[engine, job_id, "failed"]);
|
||||
let _ = JOB_STATUS_GAUGE.remove_label_values(&[engine, job_id, "cancelled"]);
|
||||
// Remove per-job origin gauge variants (known origins)
|
||||
let _ = JOB_FOUND_ORIGIN.remove_label_values(&[engine, job_id, "cpu"]);
|
||||
let _ = JOB_FOUND_ORIGIN.remove_label_values(&[engine, job_id, "gpu-g1"]);
|
||||
let _ = JOB_FOUND_ORIGIN.remove_label_values(&[engine, job_id, "gpu-g2"]);
|
||||
JOB_KEYS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove(&(engine.to_string(), job_id.to_string()));
|
||||
JOB_LAST
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove(&(engine.to_string(), job_id.to_string()));
|
||||
}
|
||||
|
||||
/// Explicitly remove all thread-scoped metrics for a job
|
||||
pub fn remove_thread_metrics_for_job(engine: &str, job_id: &str) {
|
||||
// Collect matching thread keys
|
||||
let keys: Vec<ThreadKey> = {
|
||||
let set = THREAD_KEYS.lock().unwrap();
|
||||
set.iter()
|
||||
.filter(|(e, j, _)| e == engine && j == job_id)
|
||||
.cloned()
|
||||
.collect()
|
||||
};
|
||||
for (e, j, t) in keys {
|
||||
let _ = THREAD_HASHES_TOTAL.remove_label_values(&[&e, &j, &t]);
|
||||
let _ = THREAD_HASH_RATE.remove_label_values(&[&e, &j, &t]);
|
||||
THREAD_KEYS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove(&(e.clone(), j.clone(), t.clone()));
|
||||
THREAD_LAST
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove(&(e.clone(), j.clone(), t.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the per-job hash rate series for a finished job.
|
||||
pub fn remove_job_hash_rate(engine: &str, job_id: &str) {
|
||||
let _ = JOB_HASH_RATE.remove_label_values(&[engine, job_id]);
|
||||
}
|
||||
|
||||
/// Remove the per-thread hash rate series for a finished thread.
|
||||
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]);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------
|
||||
// HTTP Exporter (feature: http-exporter)
|
||||
// -------------------------------------------------------------------------------------
|
||||
|
||||
/// Start the Prometheus HTTP exporter on 0.0.0.0:`port`.
|
||||
/// Start the Prometheus HTTP exporter on `0.0.0.0:port`.
|
||||
///
|
||||
/// Behavior:
|
||||
/// - 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).
|
||||
/// Spawns the exporter as a background task and returns immediately.
|
||||
/// Serves plaintext metrics at `GET /metrics`.
|
||||
#[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
|
||||
|
||||
// GET /metrics -> plaintext Prometheus format
|
||||
let metrics_route = warp::path("metrics").and(warp::get()).map(|| {
|
||||
let encoder = TextEncoder::new();
|
||||
let metric_families = REGISTRY.gather();
|
||||
let mut buffer = Vec::with_capacity(16 * 1024);
|
||||
let mut buffer = Vec::with_capacity(4096);
|
||||
encoder
|
||||
.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())
|
||||
});
|
||||
@@ -683,7 +316,7 @@ pub async fn start_http_exporter(port: u16) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// If the exporter feature is not enabled, expose a no-op to keep call sites simple.
|
||||
/// No-op when HTTP exporter feature is disabled.
|
||||
#[cfg(not(feature = "http-exporter"))]
|
||||
pub async fn start_http_exporter(_port: u16) -> Result<()> {
|
||||
log::warn!(
|
||||
|
||||
@@ -5,16 +5,18 @@ edition.workspace = true
|
||||
publish = false
|
||||
description = "CLI binary to run the Quantus External Miner service"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
cuda = ["miner-service/cuda"]
|
||||
|
||||
[dependencies]
|
||||
miner-service = { path = "../miner-service" }
|
||||
metrics = { path = "../metrics", features = ["http-exporter"] }
|
||||
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"
|
||||
|
||||
@@ -1,184 +1,303 @@
|
||||
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)]
|
||||
enum Command {
|
||||
/// Run the mining service
|
||||
Serve {
|
||||
/// Address of the node to connect to (e.g., "127.0.0.1:9833")
|
||||
#[arg(long, env = "MINER_NODE_ADDR")]
|
||||
node_addr: std::net::SocketAddr,
|
||||
|
||||
/// Number of CPU worker threads to use for mining (default: auto-detect)
|
||||
#[arg(long = "cpu-workers", env = "MINER_CPU_WORKERS")]
|
||||
cpu_workers: Option<usize>,
|
||||
|
||||
/// Number of GPU devices to use for mining (default: auto-detect)
|
||||
#[arg(long = "gpu-devices", env = "MINER_GPU_DEVICES")]
|
||||
gpu_devices: Option<usize>,
|
||||
|
||||
/// GPU cancel check interval in nonces (default: 10000)
|
||||
#[arg(long = "gpu-cancel-interval", env = "MINER_GPU_CANCEL_INTERVAL")]
|
||||
gpu_cancel_interval: Option<u32>,
|
||||
|
||||
/// Port for Prometheus metrics HTTP endpoint (default: 9900)
|
||||
#[arg(
|
||||
long = "metrics-port",
|
||||
env = "MINER_METRICS_PORT",
|
||||
default_value_t = 9900
|
||||
)]
|
||||
metrics_port: u16,
|
||||
|
||||
/// Enable verbose logging
|
||||
#[arg(short, long, env = "MINER_VERBOSE")]
|
||||
verbose: bool,
|
||||
},
|
||||
|
||||
/// 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 devices to use for benchmark
|
||||
#[arg(long = "gpu-devices", env = "MINER_GPU_DEVICES")]
|
||||
gpu_devices: Option<usize>,
|
||||
|
||||
/// Benchmark duration in seconds (default: 10)
|
||||
#[arg(short, long, default_value_t = 10)]
|
||||
duration: u64,
|
||||
|
||||
/// Enable verbose logging
|
||||
#[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>,
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
#[command(subcommand)]
|
||||
command: Option<Command>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let args = Args::parse();
|
||||
|
||||
// Initialize logger early to capture startup messages.
|
||||
// If RUST_LOG is not set, default to info level for our app.
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
std::env::set_var("RUST_LOG", "info");
|
||||
}
|
||||
env_logger::init();
|
||||
let Some(command) = args.command else {
|
||||
eprintln!("Error: No command provided. Use 'serve --node-addr <ADDRESS>' to start mining.");
|
||||
eprintln!("Example: quantus-miner serve --node-addr 127.0.0.1:9833");
|
||||
std::process::exit(1);
|
||||
};
|
||||
|
||||
// Telemetry CLI passthrough to env for miner-service bootstrap
|
||||
if let Some(eps) = args.telemetry_endpoints.as_ref() {
|
||||
if !eps.is_empty() {
|
||||
std::env::set_var("MINER_TELEMETRY_ENDPOINTS", eps.join(","));
|
||||
match command {
|
||||
Command::Serve {
|
||||
node_addr,
|
||||
cpu_workers,
|
||||
gpu_devices,
|
||||
gpu_cancel_interval,
|
||||
metrics_port,
|
||||
verbose,
|
||||
} => {
|
||||
init_logger(verbose);
|
||||
|
||||
log::info!("Starting external miner service...");
|
||||
|
||||
// Start metrics HTTP server
|
||||
if let Err(e) = metrics::start_http_exporter(metrics_port).await {
|
||||
log::error!("Failed to start metrics exporter: {e:?}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
log::info!(
|
||||
"Metrics available at http://0.0.0.0:{}/metrics",
|
||||
metrics_port
|
||||
);
|
||||
|
||||
let config = ServiceConfig {
|
||||
node_addr,
|
||||
cpu_workers,
|
||||
gpu_devices,
|
||||
gpu_cancel_interval,
|
||||
};
|
||||
|
||||
if let Err(e) = run(config).await {
|
||||
log::error!("Miner service terminated with error: {e:?}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
Command::Benchmark {
|
||||
cpu_workers,
|
||||
gpu_devices,
|
||||
duration,
|
||||
verbose,
|
||||
} => {
|
||||
init_logger(verbose);
|
||||
run_benchmark(cpu_workers, gpu_devices, duration).await;
|
||||
}
|
||||
}
|
||||
if let Some(v) = args.telemetry_enabled {
|
||||
std::env::set_var("MINER_TELEMETRY_ENABLED", if v { "1" } else { "0" });
|
||||
}
|
||||
if let Some(v) = args.telemetry_verbosity {
|
||||
std::env::set_var("MINER_TELEMETRY_VERBOSITY", v.to_string());
|
||||
}
|
||||
if let Some(v) = args.telemetry_interval_secs {
|
||||
std::env::set_var("MINER_TELEMETRY_INTERVAL_SECS", v.to_string());
|
||||
}
|
||||
if let Some(v) = args.telemetry_chain.as_ref() {
|
||||
std::env::set_var("MINER_TELEMETRY_CHAIN", v);
|
||||
}
|
||||
if let Some(v) = args.telemetry_genesis.as_ref() {
|
||||
std::env::set_var("MINER_TELEMETRY_GENESIS", v);
|
||||
}
|
||||
if let Some(v) = args.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() {
|
||||
std::env::set_var("MINER_TELEMETRY_NODE_PEER_ID", v);
|
||||
}
|
||||
if let Some(v) = args.telemetry_node_name.as_ref() {
|
||||
std::env::set_var("MINER_TELEMETRY_NODE_NAME", v);
|
||||
}
|
||||
if let Some(v) = args.telemetry_node_version.as_ref() {
|
||||
std::env::set_var("MINER_TELEMETRY_NODE_VERSION", v);
|
||||
}
|
||||
}
|
||||
|
||||
// Log effective configuration (concise; see ServiceConfig Display)
|
||||
log::info!("Starting external miner service...");
|
||||
fn init_logger(verbose: bool) {
|
||||
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=info"
|
||||
};
|
||||
std::env::set_var("RUST_LOG", log_level);
|
||||
}
|
||||
env_logger::init();
|
||||
}
|
||||
|
||||
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(),
|
||||
};
|
||||
log::info!("Effective config: {config}");
|
||||
async fn run_benchmark(cpu_workers: Option<usize>, gpu_devices: Option<usize>, duration: u64) {
|
||||
let effective_cpu_workers = cpu_workers.unwrap_or_else(num_cpus::get);
|
||||
|
||||
if let Err(e) = run(config).await {
|
||||
log::error!("Miner service terminated with error: {e:?}");
|
||||
// Initialize GPU engine
|
||||
let (gpu_engine, effective_gpu_devices) =
|
||||
match miner_service::resolve_gpu_configuration(gpu_devices, None) {
|
||||
Ok((engine, count)) => (engine, count),
|
||||
Err(e) => {
|
||||
eprintln!("❌ ERROR: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let total_workers = effective_cpu_workers + effective_gpu_devices;
|
||||
|
||||
println!("🚀 Quantus Miner Benchmark");
|
||||
println!("==========================");
|
||||
println!(
|
||||
"CPU Workers: {} (Available: {})",
|
||||
effective_cpu_workers,
|
||||
num_cpus::get()
|
||||
);
|
||||
println!("GPU Devices: {}", effective_gpu_devices);
|
||||
println!("Duration: {} seconds", duration);
|
||||
println!();
|
||||
|
||||
if total_workers == 0 {
|
||||
eprintln!("Error: No workers specified");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Create CPU engine
|
||||
let cpu_engine: Option<Arc<dyn MinerEngine>> = if effective_cpu_workers > 0 {
|
||||
Some(Arc::new(engine_cpu::FastCpuEngine::new()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let cancel_flag = Arc::new(AtomicBool::new(false));
|
||||
let benchmark_start = Instant::now();
|
||||
|
||||
// Random header hash for benchmark
|
||||
let mut header = [0u8; 32];
|
||||
rand::thread_rng().fill_bytes(&mut header);
|
||||
let difficulty = U512::MAX; // High difficulty - no solutions expected
|
||||
|
||||
let ref_engine = cpu_engine.as_ref().or(gpu_engine.as_ref()).unwrap();
|
||||
let ctx = ref_engine.prepare_context(header, difficulty);
|
||||
|
||||
println!("⛏️ Starting benchmark...");
|
||||
|
||||
// Spawn worker threads
|
||||
let mut handles = Vec::new();
|
||||
let total_hashes = Arc::new(std::sync::Mutex::new(0u64));
|
||||
|
||||
let cpu_chunk = 10_000u64;
|
||||
let gpu_chunk = 1_000_000u64;
|
||||
|
||||
for worker_id in 0..total_workers {
|
||||
let (engine, nonces_per_batch) = if worker_id < effective_cpu_workers {
|
||||
(cpu_engine.as_ref().unwrap().clone(), cpu_chunk)
|
||||
} else {
|
||||
(gpu_engine.as_ref().unwrap().clone(), gpu_chunk)
|
||||
};
|
||||
|
||||
let ctx = ctx.clone();
|
||||
let cancel = cancel_flag.clone();
|
||||
let hashes = total_hashes.clone();
|
||||
let start = benchmark_start;
|
||||
|
||||
let handle = thread::spawn(move || {
|
||||
let stride = U512::from(1_000_000_000_000u64);
|
||||
let worker_start = U512::from(worker_id as u64).saturating_mul(stride);
|
||||
let worker_range = EngineRange {
|
||||
start: worker_start,
|
||||
end: worker_start
|
||||
.saturating_add(U512::from(nonces_per_batch))
|
||||
.saturating_sub(U512::from(1u64)),
|
||||
};
|
||||
|
||||
loop {
|
||||
if cancel.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
let result = engine.search_range(&ctx, worker_range.clone(), &cancel);
|
||||
|
||||
match result {
|
||||
engine_cpu::EngineStatus::Found { hash_count, .. }
|
||||
| engine_cpu::EngineStatus::Exhausted { hash_count }
|
||||
| engine_cpu::EngineStatus::Cancelled { hash_count } => {
|
||||
*hashes.lock().unwrap() += hash_count;
|
||||
}
|
||||
engine_cpu::EngineStatus::Running { .. } => {}
|
||||
}
|
||||
|
||||
if start.elapsed() >= Duration::from_secs(duration) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
engine_gpu::GpuEngine::clear_worker_resources();
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// Progress updates
|
||||
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;
|
||||
}
|
||||
|
||||
if last_update.elapsed() >= Duration::from_secs(1) {
|
||||
let current = *total_hashes.lock().unwrap();
|
||||
let elapsed = benchmark_start.elapsed().as_secs_f64();
|
||||
if current > 0 {
|
||||
let rate = current as f64 / elapsed;
|
||||
println!("⏱️ {:.1}s - {} H/s", elapsed, format_hash_rate(rate));
|
||||
}
|
||||
last_update = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for threads
|
||||
for handle in handles {
|
||||
let _ = handle.join();
|
||||
}
|
||||
|
||||
let total_elapsed = benchmark_start.elapsed();
|
||||
let final_hashes = *total_hashes.lock().unwrap();
|
||||
let avg_rate = final_hashes as f64 / total_elapsed.as_secs_f64();
|
||||
|
||||
println!();
|
||||
println!("📊 Benchmark Results");
|
||||
println!("===================");
|
||||
println!("Total time: {:.2}s", total_elapsed.as_secs_f64());
|
||||
println!("Total hashes: {}", final_hashes);
|
||||
println!("Average rate: {} H/s", format_hash_rate(avg_rate));
|
||||
|
||||
if total_workers > 1 {
|
||||
let per_worker = avg_rate / total_workers as f64;
|
||||
println!("Per-worker: {} H/s", format_hash_rate(per_worker));
|
||||
}
|
||||
|
||||
println!("✅ Benchmark completed!");
|
||||
}
|
||||
|
||||
fn format_hash_rate(rate: f64) -> String {
|
||||
if rate >= 1_000_000.0 {
|
||||
format!("{:.2}M", rate / 1_000_000.0)
|
||||
} else if rate >= 1_000.0 {
|
||||
format!("{:.2}K", rate / 1_000.0)
|
||||
} else {
|
||||
format!("{:.0}", rate)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,43 +3,32 @@ name = "miner-service"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
publish = false
|
||||
description = "Service layer: HTTP API, job orchestration, and engine abstraction for the Quantus External Miner"
|
||||
description = "QUIC-based mining service for the Quantus External Miner"
|
||||
|
||||
[features]
|
||||
default = ["cpu", "metrics"]
|
||||
# Enable CPU engine by default.
|
||||
default = ["cpu"]
|
||||
cpu = ["engine-cpu"]
|
||||
# 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
|
||||
warp = { workspace = true }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
primitive-types = { workspace = true }
|
||||
log = { workspace = true }
|
||||
crossbeam-channel = { workspace = true }
|
||||
num_cpus = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
|
||||
# Protocol types from the node repo (keep API compatibility)
|
||||
# QUIC transport
|
||||
quinn = "0.10"
|
||||
rustls = { version = "0.21", default-features = false, features = ["dangerous_configuration", "quic"] }
|
||||
getrandom = "0.2"
|
||||
|
||||
# Protocol types from the node repo
|
||||
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 }
|
||||
metrics = { path = "../metrics", optional = true }
|
||||
miner-telemetry = { path = "../miner-telemetry" }
|
||||
engine-gpu = { path = "../engine-gpu" }
|
||||
metrics = { path = "../metrics" }
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
324
crates/miner-service/src/quic.rs
Normal file
324
crates/miner-service/src/quic.rs
Normal file
@@ -0,0 +1,324 @@
|
||||
//! QUIC client for connecting to blockchain nodes.
|
||||
//!
|
||||
//! This module provides a QUIC client that connects to a blockchain node
|
||||
//! and handles bidirectional streaming for receiving mining jobs and
|
||||
//! sending results.
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use engine_cpu::MinerEngine;
|
||||
use primitive_types::U512;
|
||||
use quinn::{ClientConfig, Endpoint};
|
||||
use rustls::client::ServerCertVerified;
|
||||
|
||||
use quantus_miner_api::{
|
||||
read_message, write_message, ApiResponseStatus, MinerMessage, MiningResult,
|
||||
};
|
||||
|
||||
use crate::{EngineType, WorkerPool};
|
||||
use pow_core::format_hashrate;
|
||||
|
||||
/// Connect to a node and start mining.
|
||||
///
|
||||
/// This function connects to the node, receives mining jobs, and sends results.
|
||||
/// It automatically reconnects if the connection is lost.
|
||||
///
|
||||
/// Uses a persistent worker pool to avoid thread creation overhead between jobs.
|
||||
pub async fn connect_and_mine(
|
||||
node_addr: SocketAddr,
|
||||
cpu_engine: Option<Arc<dyn MinerEngine>>,
|
||||
gpu_engine: Option<Arc<dyn MinerEngine>>,
|
||||
cpu_workers: usize,
|
||||
gpu_devices: usize,
|
||||
) -> anyhow::Result<()> {
|
||||
// Create persistent worker pool once - it lives for the entire miner lifetime
|
||||
let worker_pool = WorkerPool::new(cpu_engine, gpu_engine, cpu_workers, gpu_devices);
|
||||
|
||||
let mut reconnect_delay = Duration::from_secs(1);
|
||||
const MAX_RECONNECT_DELAY: Duration = Duration::from_secs(30);
|
||||
|
||||
loop {
|
||||
log::info!("⛏️ Connecting to node at {}...", node_addr);
|
||||
|
||||
match establish_connection(node_addr).await {
|
||||
Ok((connection, send, recv)) => {
|
||||
log::info!("⛏️ Connected to node at {}", node_addr);
|
||||
reconnect_delay = Duration::from_secs(1);
|
||||
|
||||
if let Err(e) = handle_connection(connection, send, recv, &worker_pool).await {
|
||||
log::info!("⛏️ Connection lost: {}", e);
|
||||
// Cancel any running job when connection drops
|
||||
worker_pool.cancel();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("⛏️ Failed to connect to node: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("⛏️ Reconnecting in {:?}...", reconnect_delay);
|
||||
tokio::time::sleep(reconnect_delay).await;
|
||||
reconnect_delay = (reconnect_delay * 2).min(MAX_RECONNECT_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
/// Establish a QUIC connection to the node.
|
||||
async fn establish_connection(
|
||||
addr: SocketAddr,
|
||||
) -> anyhow::Result<(quinn::Connection, quinn::SendStream, quinn::RecvStream)> {
|
||||
let mut crypto = rustls::ClientConfig::builder()
|
||||
.with_safe_defaults()
|
||||
.with_custom_certificate_verifier(Arc::new(InsecureCertVerifier))
|
||||
.with_no_client_auth();
|
||||
|
||||
crypto.alpn_protocols = vec![b"quantus-miner".to_vec()];
|
||||
|
||||
let mut client_config = ClientConfig::new(Arc::new(crypto));
|
||||
|
||||
let mut transport_config = quinn::TransportConfig::default();
|
||||
transport_config.keep_alive_interval(Some(Duration::from_secs(5)));
|
||||
transport_config.max_idle_timeout(Some(Duration::from_secs(15).try_into().unwrap()));
|
||||
client_config.transport_config(Arc::new(transport_config));
|
||||
|
||||
let mut endpoint = Endpoint::client("0.0.0.0:0".parse().unwrap())?;
|
||||
endpoint.set_default_client_config(client_config);
|
||||
|
||||
let connection = endpoint.connect(addr, "localhost")?.await?;
|
||||
log::info!("⛏️ QUIC connection established to {}", addr);
|
||||
|
||||
log::info!("⛏️ Opening bidirectional stream to node...");
|
||||
let (mut send, recv) = connection.open_bi().await?;
|
||||
|
||||
// Send Ready message to establish the stream
|
||||
write_message(&mut send, &MinerMessage::Ready).await?;
|
||||
log::info!("⛏️ Bidirectional stream established");
|
||||
|
||||
Ok((connection, send, recv))
|
||||
}
|
||||
|
||||
/// Helper to send a message while monitoring connection health.
|
||||
async fn send_message_checked(
|
||||
connection: &quinn::Connection,
|
||||
send: &mut quinn::SendStream,
|
||||
msg: &MinerMessage,
|
||||
) -> anyhow::Result<()> {
|
||||
tokio::select! {
|
||||
biased;
|
||||
reason = connection.closed() => {
|
||||
Err(anyhow::anyhow!("Connection closed: {}", reason))
|
||||
}
|
||||
result = write_message(send, msg) => {
|
||||
result.map_err(|e| anyhow::anyhow!("Failed to send message: {}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle an established connection, receiving jobs and sending results.
|
||||
async fn handle_connection(
|
||||
connection: quinn::Connection,
|
||||
mut send: quinn::SendStream,
|
||||
mut recv: quinn::RecvStream,
|
||||
worker_pool: &WorkerPool,
|
||||
) -> anyhow::Result<()> {
|
||||
use crossbeam_channel::RecvTimeoutError;
|
||||
|
||||
// Set static metrics once per connection
|
||||
metrics::set_effective_cpus(num_cpus::get() as i64);
|
||||
metrics::set_workers(worker_pool.worker_count() as i64);
|
||||
metrics::set_cpu_workers(worker_pool.cpu_worker_count() as i64);
|
||||
metrics::set_gpu_devices(worker_pool.gpu_worker_count() as i64);
|
||||
metrics::reset_hash_tracker();
|
||||
|
||||
// Current job state
|
||||
// - node_job_id: The string ID from the node (e.g., "27") - used in protocol messages
|
||||
// - internal_job_id: Our internal numeric ID from WorkerPool - used to detect stale results
|
||||
let mut node_job_id: Option<String> = None;
|
||||
let mut internal_job_id: u64 = 0;
|
||||
let mut job_start_time: Option<Instant> = None;
|
||||
let mut cpu_hashes: u64 = 0;
|
||||
let mut gpu_hashes: u64 = 0;
|
||||
let mut result_sent_for_current_job = false;
|
||||
|
||||
log::info!("⛏️ Waiting for mining jobs from node...");
|
||||
|
||||
loop {
|
||||
// Poll for worker results (non-blocking via spawn_blocking)
|
||||
let poll_result = if node_job_id.is_some() && !result_sent_for_current_job {
|
||||
let rx = worker_pool.result_receiver().clone();
|
||||
tokio::task::spawn_blocking(move || rx.recv_timeout(Duration::from_millis(10)))
|
||||
.await
|
||||
.unwrap_or(Err(RecvTimeoutError::Disconnected))
|
||||
} else {
|
||||
Err(RecvTimeoutError::Timeout)
|
||||
};
|
||||
|
||||
// Handle worker result if any
|
||||
if let Ok(worker_result) = poll_result {
|
||||
// Track hashes by engine type (always, even for stale results)
|
||||
match worker_result.engine_type {
|
||||
EngineType::Cpu => {
|
||||
cpu_hashes += worker_result.hash_count;
|
||||
metrics::record_cpu_hashes(worker_result.hash_count);
|
||||
}
|
||||
EngineType::Gpu => {
|
||||
gpu_hashes += worker_result.hash_count;
|
||||
metrics::record_gpu_hashes(worker_result.hash_count);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this result is for the current job (not stale)
|
||||
if worker_result.job_id != internal_job_id {
|
||||
log::debug!(
|
||||
"⏰ Discarding stale result from worker {} (result job_id {} != current {})",
|
||||
worker_result.thread_id,
|
||||
worker_result.job_id,
|
||||
internal_job_id
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only send result for the FIRST solution found for THIS job
|
||||
if let Some(candidate) = worker_result.candidate {
|
||||
if !result_sent_for_current_job {
|
||||
if let Some(ref job_id) = node_job_id {
|
||||
let total_hashes = cpu_hashes + gpu_hashes;
|
||||
let elapsed = job_start_time
|
||||
.map(|t| t.elapsed().as_secs_f64())
|
||||
.unwrap_or(0.0);
|
||||
|
||||
log::info!(
|
||||
"⛏️ Job {job_id} completed: {total_hashes} hashes in {elapsed:.2}s ({})",
|
||||
format_hashrate(total_hashes as f64 / elapsed.max(0.001))
|
||||
);
|
||||
|
||||
// Mark as sent BEFORE sending to prevent duplicates
|
||||
result_sent_for_current_job = true;
|
||||
worker_pool.cancel();
|
||||
metrics::set_active_jobs(0);
|
||||
|
||||
let result = MiningResult {
|
||||
status: ApiResponseStatus::Completed,
|
||||
job_id: job_id.clone(),
|
||||
nonce: Some(format!("{:x}", candidate.nonce)),
|
||||
work: Some(hex::encode(candidate.work)),
|
||||
hash_count: total_hashes,
|
||||
elapsed_time: elapsed,
|
||||
miner_id: None,
|
||||
};
|
||||
|
||||
let msg = MinerMessage::JobResult(result);
|
||||
send_message_checked(&connection, &mut send, &msg).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for incoming messages and connection health
|
||||
tokio::select! {
|
||||
biased;
|
||||
|
||||
reason = connection.closed() => {
|
||||
return Err(anyhow::anyhow!("Connection closed: {}", reason));
|
||||
}
|
||||
|
||||
msg_result = read_message(&mut recv) => {
|
||||
match msg_result {
|
||||
Ok(MinerMessage::NewJob(request)) => {
|
||||
log::info!(
|
||||
"⛏️ Received job: id={}, hash={}...",
|
||||
request.job_id,
|
||||
&request.mining_hash[..8]
|
||||
);
|
||||
|
||||
// Parse header hash
|
||||
let header_hash: [u8; 32] = match hex::decode(&request.mining_hash) {
|
||||
Ok(bytes) if bytes.len() == 32 => bytes.try_into().unwrap(),
|
||||
_ => {
|
||||
log::warn!("Invalid mining_hash in request");
|
||||
let result = MiningResult {
|
||||
status: ApiResponseStatus::Failed,
|
||||
job_id: request.job_id,
|
||||
nonce: None,
|
||||
work: None,
|
||||
hash_count: 0,
|
||||
elapsed_time: 0.0,
|
||||
miner_id: None,
|
||||
};
|
||||
let msg = MinerMessage::JobResult(result);
|
||||
send_message_checked(&connection, &mut send, &msg).await?;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Parse difficulty
|
||||
let difficulty = match U512::from_dec_str(&request.distance_threshold) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
log::warn!("Invalid difficulty in request");
|
||||
let result = MiningResult {
|
||||
status: ApiResponseStatus::Failed,
|
||||
job_id: request.job_id,
|
||||
nonce: None,
|
||||
work: None,
|
||||
hash_count: 0,
|
||||
elapsed_time: 0.0,
|
||||
miner_id: None,
|
||||
};
|
||||
let msg = MinerMessage::JobResult(result);
|
||||
send_message_checked(&connection, &mut send, &msg).await?;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Reset state for new job
|
||||
cpu_hashes = 0;
|
||||
gpu_hashes = 0;
|
||||
job_start_time = Some(Instant::now());
|
||||
node_job_id = Some(request.job_id.clone());
|
||||
result_sent_for_current_job = false;
|
||||
|
||||
log::debug!("Starting job {}", request.job_id);
|
||||
metrics::set_active_jobs(1);
|
||||
|
||||
// start_job returns the internal job ID used to detect stale results
|
||||
internal_job_id = worker_pool.start_job(header_hash, difficulty);
|
||||
}
|
||||
Ok(MinerMessage::JobResult(_)) => {
|
||||
log::warn!("Received unexpected JobResult from node");
|
||||
}
|
||||
Ok(MinerMessage::Ready) => {
|
||||
log::warn!("Received unexpected Ready from node");
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
|
||||
return Err(anyhow::anyhow!("Node disconnected"));
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(anyhow::anyhow!("Read error: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Short sleep to yield when no messages
|
||||
_ = tokio::time::sleep(Duration::from_millis(1)) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Certificate verifier that accepts any certificate (for self-signed certs).
|
||||
struct InsecureCertVerifier;
|
||||
|
||||
impl rustls::client::ServerCertVerifier for InsecureCertVerifier {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_end_entity: &rustls::Certificate,
|
||||
_intermediates: &[rustls::Certificate],
|
||||
_server_name: &rustls::ServerName,
|
||||
_scts: &mut dyn Iterator<Item = &[u8]>,
|
||||
_ocsp_response: &[u8],
|
||||
_now: std::time::SystemTime,
|
||||
) -> Result<ServerCertVerified, rustls::Error> {
|
||||
Ok(ServerCertVerified::assertion())
|
||||
}
|
||||
}
|
||||
@@ -21,5 +21,4 @@ serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
tokio-tungstenite = { version = "0.23", features = ["rustls-tls-webpki-roots"] }
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["fast-rng", "v4"] }
|
||||
|
||||
@@ -18,7 +18,6 @@ default = ["baseline", "std"]
|
||||
# std feature wires through to dependencies that have separate std/no_std builds.
|
||||
std = [
|
||||
"primitive-types/std",
|
||||
"qp-poseidon-core/std",
|
||||
]
|
||||
|
||||
# Baseline/reference implementation using BigUint-backed modular exponentiation.
|
||||
@@ -34,14 +33,8 @@ simd-poseidon2 = []
|
||||
gpu-interop = []
|
||||
|
||||
[dependencies]
|
||||
log = { workspace = true }
|
||||
primitive-types = { workspace = true }
|
||||
num-bigint = { workspace = true }
|
||||
num-traits = { workspace = true }
|
||||
qp-poseidon-core = { workspace = true }
|
||||
qpow-math = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
hex = { workspace = true }
|
||||
|
||||
@@ -2,7 +2,34 @@ use primitive_types::U512;
|
||||
|
||||
pub use qpow_math::{get_nonce_hash, is_valid_nonce, mine_range};
|
||||
|
||||
/// Job context for Bitcoin-style PoW mining with double Poseidon2 hashing
|
||||
/// Format a U512 in a human-readable way (scientific notation for large numbers).
|
||||
pub fn format_u512(n: U512) -> String {
|
||||
if n.is_zero() {
|
||||
return "0".to_string();
|
||||
}
|
||||
let s = format!("{}", n);
|
||||
let len = s.len();
|
||||
if len <= 12 {
|
||||
s
|
||||
} else {
|
||||
format!("{}e{}", &s[..4], len - 1)
|
||||
}
|
||||
}
|
||||
|
||||
/// Format a hash rate with appropriate units (H/s, KH/s, MH/s, GH/s).
|
||||
pub fn format_hashrate(hashes_per_sec: f64) -> String {
|
||||
if hashes_per_sec >= 1_000_000_000.0 {
|
||||
format!("{:.2} GH/s", hashes_per_sec / 1_000_000_000.0)
|
||||
} else if hashes_per_sec >= 1_000_000.0 {
|
||||
format!("{:.2} MH/s", hashes_per_sec / 1_000_000.0)
|
||||
} else if hashes_per_sec >= 1_000.0 {
|
||||
format!("{:.2} KH/s", hashes_per_sec / 1_000.0)
|
||||
} else {
|
||||
format!("{:.2} H/s", hashes_per_sec)
|
||||
}
|
||||
}
|
||||
|
||||
/// Job context for PoW mining with Poseidon2 hash_squeeze_twice
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct JobContext {
|
||||
pub header: [u8; 32],
|
||||
@@ -35,7 +62,7 @@ pub fn step_nonce(nonce: U512) -> U512 {
|
||||
nonce.saturating_add(U512::from(1u64))
|
||||
}
|
||||
|
||||
/// Compute hash for the current nonce using Bitcoin-style double Poseidon2
|
||||
/// Compute hash for the current nonce using Poseidon2 hash_squeeze_twice
|
||||
pub fn hash_from_nonce(ctx: &JobContext, nonce: U512) -> U512 {
|
||||
let nonce_bytes = nonce.to_big_endian();
|
||||
qpow_math::get_nonce_hash(ctx.header, nonce_bytes)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user