From: Chris Duncan Date: Thu, 9 Jan 2025 08:08:53 +0000 (-0800) Subject: Implement busy flag to prevent concurrent initializations and searches. Rewrite init... X-Git-Url: https://zoso.dev/?a=commitdiff_plain;h=ff0e89d81e80b0f3facffa395194cbc385188bb0;p=libnemo.git Implement busy flag to prevent concurrent initializations and searches. Rewrite init as async function. Explicitly destroy buffers if device is lost. Setup buffers and pipeline in separate function call. Refactor search into a while loop instead of recursive promises. Unmap CPU buffer as soon as possible. Do not retry search on caught error. --- diff --git a/src/lib/nano-pow/classes/gpu.ts b/src/lib/nano-pow/classes/gpu.ts index 35b6eb4..b4708fb 100644 --- a/src/lib/nano-pow/classes/gpu.ts +++ b/src/lib/nano-pow/classes/gpu.ts @@ -10,6 +10,7 @@ import { NanoPowGpuComputeShader } from '../shaders/index.js' export class NanoPowGpu { // Initialize WebGPU + static #busy: boolean = false static #device: GPUDevice | null = null static #uboBuffer: GPUBuffer static #gpuBuffer: GPUBuffer @@ -22,75 +23,85 @@ export class NanoPowGpu { } // Initialize WebGPU - static init () { + static async init () { + if (this.#busy) return + this.#busy = true // Request device and adapter if (navigator.gpu == null) { throw new Error('WebGPU is not supported in this browser.') } - navigator.gpu.requestAdapter() - .then(adapter => { - if (adapter == null) { - throw new Error('WebGPU adapter refused by browser.') - } - adapter.requestDevice() - .then(device => { - this.#device = device - this.#device.lost.then(loss => { - console.dir(loss) - console.warn(loss.reason, loss.message) - console.warn(`Device lost. Reinitializing...`) - this.init() - }) - - // Create buffers for writing GPU calculations and reading from Javascript - this.#uboBuffer = this.#device.createBuffer({ - size: 48, - usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST - }) - this.#gpuBuffer = this.#device.createBuffer({ - size: 16, - usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC - }) - this.#cpuBuffer = this.#device.createBuffer({ - size: 16, - usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ - }) - - // Create binding group data structure and use it later once UBO is known - this.#bindGroupLayout = this.#device.createBindGroupLayout({ - entries: [ - { - binding: 0, - visibility: GPUShaderStage.COMPUTE, - buffer: { - type: 'uniform' - }, - }, - { - binding: 1, - visibility: GPUShaderStage.COMPUTE, - buffer: { - type: 'storage' - }, - } - ] - }) - - // Create pipeline to connect compute shader to binding layout - this.#pipeline = this.#device.createComputePipeline({ - layout: this.#device.createPipelineLayout({ - bindGroupLayouts: [this.#bindGroupLayout] - }), - compute: { - entryPoint: 'main', - module: this.#device.createShaderModule({ - code: NanoPowGpuComputeShader - }) - } - }) - }) + try { + const adapter = await navigator.gpu.requestAdapter() + if (adapter == null) { + throw new Error('WebGPU adapter refused by browser.') + } + const device = await adapter.requestDevice() + if (!(device instanceof GPUDevice)) { + throw new Error('WebGPU device failed to load.') + } + device.lost.then(loss => { + console.dir(loss) + console.warn(`Device lost. Reinitializing...`) + this.#cpuBuffer?.destroy() + this.#gpuBuffer?.destroy() + this.#uboBuffer?.destroy() + this.init() }) - .catch(err => { throw new Error(err.message) }) + this.#device = device + this.setup() + } catch (err) { + throw new Error(`WebGPU initialization failed. ${err}`) + } finally { + this.#busy = false + } + } + + static setup () { + if (this.#device == null) throw new Error(`WebGPU device failed to load.`) + // Create buffers for writing GPU calculations and reading from Javascript + this.#uboBuffer = this.#device.createBuffer({ + size: 48, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST + }) + this.#gpuBuffer = this.#device.createBuffer({ + size: 16, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC + }) + this.#cpuBuffer = this.#device.createBuffer({ + size: 16, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ + }) + // Create binding group data structure and use it later once UBO is known + this.#bindGroupLayout = this.#device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.COMPUTE, + buffer: { + type: 'uniform' + }, + }, + { + binding: 1, + visibility: GPUShaderStage.COMPUTE, + buffer: { + type: 'storage' + }, + } + ] + }) + // Create pipeline to connect compute shader to binding layout + this.#pipeline = this.#device.createComputePipeline({ + layout: this.#device.createPipelineLayout({ + bindGroupLayouts: [this.#bindGroupLayout] + }), + compute: { + entryPoint: 'main', + module: this.#device.createShaderModule({ + code: NanoPowGpuComputeShader + }) + } + }) } /** @@ -100,91 +111,103 @@ export class NanoPowGpu { * @param {number} [threshold=0xfffffff8] - Difficulty of proof-of-work calculation */ static async search (hash: string, threshold: number = 0xfffffff8): Promise { + if (this.#busy) { + return new Promise(resolve => { + setTimeout(async () => { + const result = this.search(hash, threshold) + 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}`) + if (typeof threshold !== 'number') throw new TypeError(`Invalid threshold ${threshold}`) - // Ensure WebGPU is initialized before calculating, up to a max time frame - while (this.#device == null && performance.now() < 8000) { + // Ensure WebGPU is initialized before calculating + let loads = 0 + while (this.#device == null && loads < 20) { await new Promise(resolve => { setTimeout(resolve, 500) }) } if (this.#device == null) throw new Error(`WebGPU device failed to load.`) - // Set up uniform buffer object - // Note: u32 size is 4, but total alignment must be multiple of 16 - const uboView = new DataView(new ArrayBuffer(48)) - for (let i = 0; i < 64; i += 8) { - const uint32 = hash.slice(i, i + 8) - uboView.setUint32(i / 2, parseInt(uint32, 16)) - } - const random = Math.floor((Math.random() * 0xffffffff)) - uboView.setUint32(32, random, true) - uboView.setUint32(36, threshold, true) - this.#device.queue.writeBuffer(this.#uboBuffer, 0, uboView) - - // Reset `found` flag to 0u in WORK before each calculation - this.#device.queue.writeBuffer(this.#gpuBuffer, 8, new Uint32Array([0])) - - // Bind UBO read and GPU write buffers - const bindGroup = this.#device.createBindGroup({ - layout: this.#bindGroupLayout, - entries: [ - { - binding: 0, - resource: { - buffer: this.#uboBuffer - }, - }, - { - binding: 1, - resource: { - buffer: this.#gpuBuffer - }, - }, - ], - }) - - // Create command encoder to issue commands to GPU and initiate computation - const commandEncoder = this.#device.createCommandEncoder() - const passEncoder = commandEncoder.beginComputePass() - - // Issue commands and end compute pass structure - passEncoder.setPipeline(this.#pipeline) - passEncoder.setBindGroup(0, bindGroup) - passEncoder.dispatchWorkgroups(256, 256, 256) - passEncoder.end() - - // Copy 8-byte nonce and 4-byte found flag from GPU to CPU for reading - commandEncoder.copyBufferToBuffer( - this.#gpuBuffer, - 0, - this.#cpuBuffer, - 0, - 12 - ) - - // End computation by passing array of command buffers to command queue for execution - this.#device.queue.submit([commandEncoder.finish()]) - - // Read results back to Javascript and then unmap buffer after reading - await this.#cpuBuffer.mapAsync(GPUMapMode.READ) - await this.#device.queue.onSubmittedWorkDone() + let nonce = 0n + let found = false try { - const data = new DataView(this.#cpuBuffer.getMappedRange()) - const nonce = data.getBigUint64(0, true) - const found = !!data.getUint32(8) - this.#cpuBuffer.unmap() - - if (found) { - const hex = nonce.toString(16).padStart(16, '0') - return hex - } else { - return await this.search(hash, threshold) - } + do { + // Set up uniform buffer object + // Note: u32 size is 4, but total alignment must be multiple of 16 + const uboView = new DataView(new ArrayBuffer(48)) + for (let i = 0; i < 64; i += 8) { + const uint32 = hash.slice(i, i + 8) + uboView.setUint32(i / 2, parseInt(uint32, 16)) + } + const random = Math.floor((Math.random() * 0xffffffff)) + uboView.setUint32(32, random, true) + uboView.setUint32(36, threshold, true) + this.#device.queue.writeBuffer(this.#uboBuffer, 0, uboView) + + // Reset `found` flag to 0u in WORK before each calculation + this.#device.queue.writeBuffer(this.#gpuBuffer, 8, new Uint32Array([0])) + + // Bind UBO read and GPU write buffers + const bindGroup = this.#device.createBindGroup({ + layout: this.#bindGroupLayout, + entries: [ + { + binding: 0, + resource: { + buffer: this.#uboBuffer + }, + }, + { + binding: 1, + resource: { + buffer: this.#gpuBuffer + }, + }, + ], + }) + + // Create command encoder to issue commands to GPU and initiate computation + const commandEncoder = this.#device.createCommandEncoder() + const passEncoder = commandEncoder.beginComputePass() + + // Issue commands and end compute pass structure + passEncoder.setPipeline(this.#pipeline) + passEncoder.setBindGroup(0, bindGroup) + passEncoder.dispatchWorkgroups(256, 256, 256) + passEncoder.end() + + // Copy 8-byte nonce and 4-byte found flag from GPU to CPU for reading + commandEncoder.copyBufferToBuffer( + this.#gpuBuffer, + 0, + this.#cpuBuffer, + 0, + 12 + ) + + // End computation by passing array of command buffers to command queue for execution + this.#device.queue.submit([commandEncoder.finish()]) + + // Read results back to Javascript and then unmap buffer after reading + await this.#cpuBuffer.mapAsync(GPUMapMode.READ) + await this.#device.queue.onSubmittedWorkDone() + const dataBuffer = this.#cpuBuffer.getMappedRange().slice(0) + this.#cpuBuffer.unmap() + + if (dataBuffer == null) throw new Error(`Failed to get data from buffer.`) + const dataView = new DataView(dataBuffer) + nonce = dataView.getBigUint64(0, true) + found = !!dataView.getUint32(8) + } while (!found) } catch (err) { - console.warn(`Error getting data from GPU, retrying. ${err}`) - return await this.search(hash, threshold) + console.warn(`Error getting data from GPU. ${err}`) + } finally { + this.#busy = false + return nonce.toString(16).padStart(16, '0') } } }