]> zoso.dev Git - nano-pow.git/commitdiff
Revert to workgroup size 64. Create NanoPowOptions type to expand available parameter...
authorChris Duncan <chris@zoso.dev>
Wed, 15 Jan 2025 18:08:03 +0000 (10:08 -0800)
committerChris Duncan <chris@zoso.dev>
Wed, 15 Jan 2025 18:08:03 +0000 (10:08 -0800)
src/classes/gl.ts
src/classes/gpu.ts
src/shaders/compute.wgsl
test.html
types.d.ts

index f4e16fa84c6fc202c324fe4571a6b4870561b6a1..a6e376b61ad159ded14554afc30b5a56261269b8 100644 (file)
@@ -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<string> {
+       static async search (hash: string, options?: NanoPowOptions): Promise<string> {
                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<boolean> {
+       static async validate (work: string, hash: string, options?: NanoPowOptions): Promise<boolean> {
                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)
index 6d497ff001b45ef2498cf00ba9a6079ab07ab7c7..f4eba87395349e11fd7d6af9d4d0d2e6419868b1 100644 (file)
@@ -3,7 +3,7 @@
 /// <reference types="@webgpu/types" />
 
 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<DataView> {
                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<string> {
+       static async search (hash: string, options?: NanoPowOptions): Promise<string> {
                if (this.#busy) {
                        return new Promise(resolve => {
-                               setTimeout(async () => {
-                                       const result = this.search(hash, threshold)
+                               setTimeout(async (): Promise<void> => {
+                                       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<boolean> {
+       static async validate (work: string, hash: string, options?: NanoPowOptions): Promise<boolean> {
                if (this.#busy) {
                        return new Promise(resolve => {
-                               setTimeout(async () => {
-                                       const result = this.validate(work, hash, threshold)
+                               setTimeout(async (): Promise<void> => {
+                                       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
index 75dc6e2c608d021bd1d686f135f12c5a91d64d9e..01125aad83a747de9d1a013172bab618471ed6d8 100644 (file)
@@ -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<u32>) {
        if (atomicLoad(&work.found) != 0u) { return; }
 
@@ -83,7 +83,7 @@ fn main(@builtin(global_invocation_id) id: vec3<u32>) {
 
        /**
        * 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.
index 9310bea5b389743f997cdfde54ae55dd4058b10c..60399893977b6eb064b8f326e420c10ff17452c6 100644 (file)
--- 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})<br/>`
-                       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)}<br/>`
+                       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}<br/>`
@@ -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)
                })
        </script>
-       <style>body{background:black;color:white;}a{color:darkcyan;}</style>
+       <style>body{background:black;color:white;}a{color:darkcyan;}input[type=number]{width:5em;}</style>
 </head>
 
 <body>
@@ -125,8 +129,16 @@ SPDX-License-Identifier: GPL-3.0-or-later
        <p>NanoPow uses cutting edge WebGPU technology. Not all browsers are supported.</p>
        <p>NanoPow uses WebGL 2.0 as a fallback option if WebGPU is not detected.</p>
        <p>Times below are in milliseconds and summarized by various averaging methods.</p>
+       <p>Level of Effort depends on hardware and does not guarantee faster results.</p>
        <hr />
-       <input id="count" type="number" value="2" />
+       <label for="size">Test Size</label>
+       <input id="size" type="number" value="64" />
+       <label for="effort">Effort (1-16)</label>
+       <input id="effort" type="number" value="8" />
+       <label for="gl">Force WebGL?</label>
+       <input id="gl" type="checkbox" />
+       <label for="debug">Debug?</label>
+       <input id="debug" type="checkbox" />
        <button id="go">Go</button>
        <h3 id="status">WAITING</h3>
        <hr />
index 29be660062f917d707db7bbac140f95e210bcc4b..f81da16b8bd3b46d8a401f4a0f5c4d8fe7d65131 100644 (file)
@@ -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 */