From aba3ef96f11e6193791080dc6f95864ddecbd868 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Wed, 15 Jan 2025 10:08:03 -0800 Subject: [PATCH] Revert to workgroup size 64. Create NanoPowOptions type to expand available parameters for executing searches and validation. Add a logging feature to gpu class. Expand options available on test page. --- src/classes/gl.ts | 26 ++++++++++++----- src/classes/gpu.ts | 63 ++++++++++++++++++++++++++++++++-------- src/shaders/compute.wgsl | 4 +-- test.html | 30 +++++++++++++------ types.d.ts | 13 +++++++++ 5 files changed, 105 insertions(+), 31 deletions(-) diff --git a/src/classes/gl.ts b/src/classes/gl.ts index f4e16fa..a6e376b 100644 --- a/src/classes/gl.ts +++ b/src/classes/gl.ts @@ -3,6 +3,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later AND MIT import { NanoPowGlFragmentShader, NanoPowGlVertexShader } from '../shaders' +import type { NanoPowOptions } from '../../types.d.ts' export class NanoPowGl { /** Used to set canvas size. Must be a multiple of 256. */ @@ -165,11 +166,17 @@ export class NanoPowGl { * @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts * @param {number} [threshold=0xfffffff8] - Difficulty of proof-of-work calculation */ - static async search (hash: string, threshold: number = 0xfffffff8): Promise { + static async search (hash: string, options?: NanoPowOptions): Promise { if (NanoPowGl.#gl == null) throw new Error('WebGL 2 is required') - if (!/^[A-Fa-f0-9]{64}$/.test(hash)) throw new Error(`Invalid hash ${hash}`) - if (typeof threshold !== 'number') throw new TypeError(`Invalid threshold ${threshold}`) if (this.#gl == null) throw new Error('WebGL 2 is required') + if (!/^[A-Fa-f0-9]{64}$/.test(hash)) throw new Error(`Invalid hash ${hash}`) + const threshold = (typeof options?.threshold !== 'number' || options.threshold < 0x0 || options.threshold > 0xffffffff) + ? 0xfffffff8 + : options.threshold + const effort = (typeof options?.effort !== 'number' || options.effort < 0x1 || options.effort > 0x10) + ? 0x8 + : options.effort + const debug = !!(options?.debug) /** Set up uniform buffer object */ const uboView = new DataView(new ArrayBuffer(144)) @@ -178,7 +185,7 @@ export class NanoPowGl { uboView.setUint32(i * 2, parseInt(uint32, 16)) } uboView.setUint32(128, threshold, true) - uboView.setFloat32(132, NanoPowGl.#WORKLOAD - 1, true) + uboView.setFloat32(132, 256 * effort, true) NanoPowGl.#gl.bindBuffer(NanoPowGl.#gl.UNIFORM_BUFFER, NanoPowGl.#uboBuffer) NanoPowGl.#gl.bufferSubData(NanoPowGl.#gl.UNIFORM_BUFFER, 0, uboView) NanoPowGl.#gl.bindBuffer(NanoPowGl.#gl.UNIFORM_BUFFER, null) @@ -204,12 +211,15 @@ export class NanoPowGl { * @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts * @param {number} [threshold=0xfffffff8] - Difficulty of proof-of-work calculation */ - static async validate (work: string, hash: string, threshold: number = 0xfffffff8): Promise { + static async validate (work: string, hash: string, options?: NanoPowOptions): Promise { if (NanoPowGl.#gl == null) throw new Error('WebGL 2 is required') + if (this.#gl == null) throw new Error('WebGL 2 is required') if (!/^[A-Fa-f0-9]{16}$/.test(work)) throw new Error(`Invalid work ${work}`) if (!/^[A-Fa-f0-9]{64}$/.test(hash)) throw new Error(`Invalid hash ${hash}`) - if (typeof threshold !== 'number') throw new TypeError(`Invalid threshold ${threshold}`) - if (this.#gl == null) throw new Error('WebGL 2 is required') + const threshold = (typeof options?.threshold !== 'number' || options.threshold < 0x0 || options.threshold > 0xffffffff) + ? 0xfffffff8 + : options.threshold + const debug = !!(options?.debug) /** Set up uniform buffer object */ const uboView = new DataView(new ArrayBuffer(144)) @@ -218,7 +228,7 @@ export class NanoPowGl { uboView.setUint32(i * 2, parseInt(uint32, 16)) } uboView.setUint32(128, threshold, true) - uboView.setFloat32(132, NanoPowGl.#WORKLOAD - 1, true) + uboView.setFloat32(132, 1, true) NanoPowGl.#gl.bindBuffer(NanoPowGl.#gl.UNIFORM_BUFFER, NanoPowGl.#uboBuffer) NanoPowGl.#gl.bufferSubData(NanoPowGl.#gl.UNIFORM_BUFFER, 0, uboView) NanoPowGl.#gl.bindBuffer(NanoPowGl.#gl.UNIFORM_BUFFER, null) diff --git a/src/classes/gpu.ts b/src/classes/gpu.ts index 6d497ff..f4eba87 100644 --- a/src/classes/gpu.ts +++ b/src/classes/gpu.ts @@ -3,7 +3,7 @@ /// import { NanoPowGpuComputeShader } from '../shaders' - +import type { NanoPowOptions } from '../../types.d.ts' /** * Nano proof-of-work using WebGPU. */ @@ -92,6 +92,31 @@ export class NanoPowGpu { NanoPowGpu.init() } + static logAverages (times: number[]): void { + let count = times.length, sum = 0, reciprocals = 0, logarithms = 0, truncated = 0, min = 0xffff, max = 0, rate = 0 + times.sort() + for (let i = 0; i < count; i++) { + sum += times[i] + reciprocals += 1 / times[i] + logarithms += Math.log(times[i]) + min = Math.min(min, times[i]) + max = Math.max(max, times[i]) + if (count > 2 && i > (count * 0.1) && i < (count * 0.9)) truncated += times[i] + } + const averages = { + Count: `${count} dispatches`, + Total: `${sum} ms`, + Rate: `${count / sum * 1000} dispatches/second`, + Minimum: `${min} ms`, + Maximum: `${max} ms`, + Arithmetic: `${sum / count} ms`, + Truncated: `${truncated / count} ms`, + Harmonic: `${count / reciprocals} ms`, + Geometric: `${Math.exp(logarithms / count)} ms` + } + console.table(averages) + } + static async #dispatch (seed: bigint, hash: string, threshold: number, passes: number): Promise { if (this.#device == null) throw new Error(`WebGPU device failed to load.`) // Set up uniform buffer object @@ -162,20 +187,26 @@ export class NanoPowGpu { * Finds a nonce that satisfies the Nano proof-of-work requirements. * * @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts - * @param {number} [threshold=0xfffffff8] - Difficulty of proof-of-work calculation + * @param {NanoPowOptions} options - Used to configure search execution */ - static async search (hash: string, threshold: number = 0xfffffff8): Promise { + static async search (hash: string, options?: NanoPowOptions): Promise { if (this.#busy) { return new Promise(resolve => { - setTimeout(async () => { - const result = this.search(hash, threshold) + setTimeout(async (): Promise => { + const result = this.search(hash, options) resolve(result) }, 100) }) } this.#busy = true if (!/^[A-Fa-f0-9]{64}$/.test(hash)) throw new TypeError(`Invalid hash ${hash}`) - if (typeof threshold !== 'number') throw new TypeError(`Invalid threshold ${threshold}`) + const threshold = (typeof options?.threshold !== 'number' || options.threshold < 0x0 || options.threshold > 0xffffffff) + ? 0xfffffff8 + : options.threshold + const effort = (typeof options?.effort !== 'number' || options.effort < 0x1 || options.effort > 0x10) + ? 0x8 + : options.effort + const debug = !!(options?.debug) // Ensure WebGPU is initialized before calculating let loads = 0 @@ -186,14 +217,19 @@ export class NanoPowGpu { } if (this.#device == null) throw new Error(`WebGPU device failed to load.`) + let times = [] + let start = performance.now() let nonce = 0n do { + start = performance.now() const random = Math.floor(Math.random() * 0xffffffff) const seed = (BigInt(random) << 32n) | BigInt(random) - const data = await this.#dispatch(seed, hash, threshold, 0x400) + const data = await this.#dispatch(seed, hash, threshold, effort * 0x100) nonce = data.getBigUint64(0, true) this.#busy = !data.getUint32(8) + times.push(performance.now() - start) } while (this.#busy) + if (debug) this.logAverages(times) return nonce.toString(16).padStart(16, '0') } @@ -202,13 +238,13 @@ export class NanoPowGpu { * * @param {string} work - Hexadecimal proof-of-work value to validate * @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts - * @param {number} [threshold=0xfffffff8] - Difficulty of proof-of-work calculation + * @param {NanoPowOptions} options - Options used to configure search execution */ - static async validate (work: string, hash: string, threshold: number = 0xfffffff8): Promise { + static async validate (work: string, hash: string, options?: NanoPowOptions): Promise { if (this.#busy) { return new Promise(resolve => { - setTimeout(async () => { - const result = this.validate(work, hash, threshold) + setTimeout(async (): Promise => { + const result = this.validate(work, hash, options) resolve(result) }, 100) }) @@ -216,7 +252,10 @@ export class NanoPowGpu { this.#busy = true if (!/^[A-Fa-f0-9]{16}$/.test(work)) throw new TypeError(`Invalid work ${work}`) if (!/^[A-Fa-f0-9]{64}$/.test(hash)) throw new TypeError(`Invalid hash ${hash}`) - if (typeof threshold !== 'number') throw new TypeError(`Invalid threshold ${threshold}`) + const debug = !!(options?.debug) + const threshold = (typeof options?.threshold !== 'number' || options.threshold < 0x0 || options.threshold > 0xffffffff) + ? 0xfffffff8 + : options.threshold // Ensure WebGPU is initialized before calculating let loads = 0 diff --git a/src/shaders/compute.wgsl b/src/shaders/compute.wgsl index 75dc6e2..01125aa 100644 --- a/src/shaders/compute.wgsl +++ b/src/shaders/compute.wgsl @@ -26,7 +26,7 @@ const BLAKE2B_IV32_1: u32 = 0x6A09E667u; * this 8-byte value is then XOR'd with a different dimensional index from * the thread identifier. */ -@compute @workgroup_size(256) +@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) id: vec3) { if (atomicLoad(&work.found) != 0u) { return; } @@ -83,7 +83,7 @@ fn main(@builtin(global_invocation_id) id: vec3) { /** * Twelve rounds of G mixing as part of BLAKE2b compression step. -* + * * Each sigma r index correlates with the reference implementation, but each * sigma i index is doubled due to using two u32 array elements to represent * one uint64_t. diff --git a/test.html b/test.html index 9310bea..6039989 100644 --- a/test.html +++ b/test.html @@ -65,7 +65,8 @@ SPDX-License-Identifier: GPL-3.0-or-later console.log(`Geometric Mean: ${geometric} ms`) } - export async function run (COUNT) { + export async function run (size, effort, gl, debug) { + if (gl) NanoPow = NanoPowGl document.getElementById('status').innerHTML = `TESTING IN PROGRESS` console.log(`%cNanoPow`, 'color:green', 'Checking validate()') const expectFalse = await NanoPow.validate('0000000000000000', '0000000000000000000000000000000000000000000000000000000000000000') @@ -82,16 +83,16 @@ SPDX-License-Identifier: GPL-3.0-or-later return } - console.log(`%cNanoPow (${type})`, 'color:green', `Calculate proof-of-work for ${COUNT} unique send block hashes`) + console.log(`%cNanoPow (${type})`, 'color:green', `Calculate proof-of-work for ${size} unique send block hashes`) const times = [] - document.getElementById('output').innerHTML += `Now testing: NanoPow (${type})
` - for (let i = 0; i < COUNT; i++) { + document.getElementById('output').innerHTML += `Now testing: NanoPow (${type}) | Dispatch: ${(effort * 0x100) ** 2} | Threads/Dispatch: ${64 * ((effort * 0x100) ** 2)}
` + for (let i = 0; i < size; i++) { const hash = random() let work = null let isValid = null const start = performance.now() try { - work = await NanoPow.search(hash) + work = await NanoPow.search(hash, { effort, debug }) isValid = (await NanoPow.validate(work, hash)) ? 'VALID' : 'INVALID' } catch (err) { document.getElementById('output').innerHTML += `Error: ${err.message}
` @@ -111,11 +112,14 @@ SPDX-License-Identifier: GPL-3.0-or-later } document.getElementById('go').addEventListener('click', e => { - const count = document.getElementById('count') - run(count.value) + const size = document.getElementById('size') + const effort = document.getElementById('effort') + const gl = document.getElementById('gl') + const debug = document.getElementById('debug') + run(size.value, effort.value, gl.checked, debug.checked) }) - + @@ -125,8 +129,16 @@ SPDX-License-Identifier: GPL-3.0-or-later

NanoPow uses cutting edge WebGPU technology. Not all browsers are supported.

NanoPow uses WebGL 2.0 as a fallback option if WebGPU is not detected.

Times below are in milliseconds and summarized by various averaging methods.

+

Level of Effort depends on hardware and does not guarantee faster results.


- + + + + + + + +

WAITING


diff --git a/types.d.ts b/types.d.ts index 29be660..f81da16 100644 --- a/types.d.ts +++ b/types.d.ts @@ -6,6 +6,19 @@ export declare const NanoPowGpuComputeShader: any declare const NanoPow: typeof NanoPowGl | typeof NanoPowGpu | null +/** +* Used to configure NanoPow. +* +* @param {boolean} [debug=false] - Enables additional debug logging to the console. Default: false +* @param {number} [effort=0x8] - Multiplier for dispatching work search. Larger values are not necessarily better since they can quickly overwhelm the GPU. Ignored when validating. Default: 0x8 +* @param {number} [threshold=0xfffffff8] - Minimum value result of `BLAKE2b(nonce||blockhash) << 0x32`. Default: 0xFFFFFFF8 +*/ +export type NanoPowOptions = { + debug?: boolean + effort?: 0x1 | 0x2 | 0x3 | 0x4 | 0x5 | 0x6 | 0x7 | 0x8 | 0x9 | 0xa | 0xb | 0xc | 0xd | 0xe | 0xf | 0x10 + threshold?: number +} + export declare class NanoPowGl { #private /** Compile */ -- 2.34.1