GPU.js

repository·develop·Indexed 12 days ago

https://github.com/gpujs/gpu.js

A JavaScript acceleration library for GPGPU (General Purpose computing on GPUs) that transpiles JavaScript functions into shader language to run computations on the GPU, with a fallback to standard JavaScript. Version 2.24.0 supports browser and Node.js environments, TypeScript, and graphical output to canvas.

Tokens
19.2K
Snippets
70
Records
88
Agent score
96%

What's inside GPU.js

  1. Rules and restrictions for Pipeline orchestration functions

    develop

    When writing the orchestration function for createPipeline, you must follow strict trace-time rules. Violations will throw an error at build time (during the first call).

    Prohibited operations during orchestration (trace-time):

    • Reading handles: You cannot read an element or property of a handle (e.g., handle[0]). This throws: "pipeline intermediate results cannot be read during orchestration".
    • Arithmetic/Conditions on handles: Using a handle in arithmetic or conditional logic (via valueOf or Symbol.toPrimitive) is forbidden.
    • Non-deterministic functions: Calling Math.random() is forbidden; orchestration must be deterministic.
    • External consumption: Calling any function that consumes a handle (other than a GPU.js kernel created by the same GPU instance) is forbidden.

    Legal operations:

    • Kernel calls: Calling GPU.js kernels created by the same GPU instance using handles or plain JS values as arguments.
    • Non-handle arguments: Using numbers or arrays (uploaded per call) as kernel arguments.
    • Captured values: Plain values captured via closures are frozen into the plan at trace time.
  2. Accepting input types in GPU.js kernels

    develop

    GPU.js kernels can accept several types of input arguments directly in the kernel function. Supported types include:

    • Numbers: Scalar values.
    • Arrays: 1d, 2d, or 3d arrays of numbers. Supported array types include Array, Float32Array, Int16Array, Int8Array, Uint16Array, and UInt8Array.
    • HTML Elements: Single HTMLImageElement, an array of images (HTMLImageArray), or a HTMLVideoElement (V2+).
    • Pre-flattened Arrays: For faster data upload, use the input function to pass a flattened array with specified dimensions.

    Memory Layout for input(): When using input(flattenedArray, [width, height, depth]), the dimensions follow [x, y, z] where x is the fastest-varying (innermost) index. A kernel access arg[i][j][k] maps to z = i, y = j, x = k. This means input(flat, [X, Y, Z]) is equivalent to a nested array of shape [Z][Y][X].

    const { input } = require('gpu.js');
    // Faster upload of pre-flattened arrays
    const value = input(flattenedArray, [width, height, depth]);
  3. Declare variables and functions inside kernels

    develop

    GPU.js supports several variable types and private functions within the kernel function body:

    • Number: Integers or floats (e.g., let v = 1 or let v = 1.1).
    • Boolean: (e.g., let v = true).
    • Array(2), Array(3), or Array(4): Fixed-size arrays (e.g., let v = [1, 2, 3]).
    • private Function: Functions defined inside the kernel scope. The return type is inferred from the function's return value.
    const kernel = gpu.createKernel(function() {
      // Private function
      function myPrivateFunction() {
        return [0.08, 2, 0.1, 3];
      }
      
      // Using declaration
      const array3 = [0.08, 2, 0.1];
      
      return myPrivateFunction(); 
    }).setOutput([100]);
  4. Pipeline execution backends and fusion

    develop

    GPU.js pipelines execute using different strategies depending on the backend and the complexity of the plan:

    Generic Executor

    Used as a correctness reference and fallback. It executes steps sequentially through existing kernel machinery.

    • CPU: Uses plain arrays.
    • WebGL/WebGL2: Uses textures end-to-end.
    • WebGPU: Uses buffer handles.

    WebAssembly (Wasm) Fused Executor

    Designed for high performance by keeping intermediates in Wasm memory.

    • Sync path: All steps compile over a single Wasm memory layout [pipeline args | plan buffers]. Intermediates never leave Wasm memory.
    • Threaded path: Uses workers with Atomics-based barriers between steps over shared memory. Falls back to sync-fused if threads are unavailable.

    WebGPU Fused Executor ('fused-encoder')

    Compiles the plan into a single command encoder.

    • Every step is recorded as a compute pass into one command encoder.
    • Uses persistent STORAGE buffers on the device with static alternating bind groups for ping-ponging.
    • Performs one queue.submit and one mapAsync readback per pipeline call.

    Degradation: If a plan contains elements that cannot be fused (e.g., GPU-resident handle arguments or certain vector intermediates), the system will degrade to the Generic Executor and provide a fallbackReason.

  5. Use the 'async' mode for cross-platform compatibility

    develop

    The async mode is the recommended way to write portable GPU.js code. It automatically selects the most performant backend available in the current environment and handles the transition to webgpu if supported.

    Important: When using mode: 'async', all kernel calls return a Promise, so you must use await to retrieve results.

    const gpu = new GPU({ mode: 'async' });
    const kernel = gpu.createKernel(function(a) {
      return a[this.thread.x] * 2;
    }).setOutput([64]);
    
    const result = await kernel(myArray);
  6. How Pipeline double-buffering works

    develop

    The GPU.js pipeline compiler automatically handles memory residency through a static liveness analysis. If a kernel step's output buffer is required as an input by a later step (or the same step in a loop), the compiler implements automatic double-buffering (ping-pong).

    For example, in the pattern u = sweep(u, q) inside a loop, the compiler detects that u is being overwritten while also being read. It will compile this to use two alternating buffers with a single kernel instance to prevent data corruption.

  7. Use Pipelining for fast kernel-to-kernel communication

    develop

    Pipelining allows values to be sent directly from one kernel to another via a texture, resulting in extremely fast computing.

    How to enable:

    • Use the kernel setting pipeline: true in createKernel.
    • Or call kernel.setPipeline(true) on an existing kernel.

    Memory Management:

    • By default (immutable: false), kernel results are reused.
    • If you want to keep results around, use immutable: true.
    • Cleanup: In GPU mode, use texture.delete() to release memory. If you want to keep the texture in memory but reset its values to zero, use texture.clear().
    • Cloning: When in pipeline mode, you can use texture.clone() to create a copy of the output.
    const kernel1 = gpu.createKernel(function(v) {
        return v[this.thread.x];
    })
      .setPipeline(true)
      .setOutput([100]);
    
    const kernel2 = gpu.createKernel(function(v) {
        return v[this.thread.x];
    })
      .setOutput([100]);
    
    const result1 = kernel1(array);
    // result1 is a Texture
    const result2 = kernel2(result1);
    // result2 is a Float32Array
  8. How GPU.js works

    develop

    GPU.js is a GPGPU (General Purpose computing on GPUs) acceleration library. It works by automatically transpiling simple JavaScript functions into shader language (like GLSL or WGSL) and compiling them to run on your GPU.

    If a GPU is not available, the library falls back to running the functions in regular JavaScript. A typical kernel function computes a single element in a multi-dimensional matrix (e.g., a 2D array) using this.thread.x and this.thread.y to identify the current execution unit.

  9. Debug GPU kernels using return values

    develop

    Since you cannot set breakpoints on the GPU, the best way to inspect values is to return them early. By modifying your kernel to return a specific intermediate value, you can see exactly what the GPU is calculating at that step.

    For graphical kernels, you can also use this.color(r, g, b) to visually debug by coloring pixels based on specific conditions.

    // Debugging values by returning early
    const gpu = new GPU({ mode: 'cpu' });
    const kernel = gpu.createKernel(function(arg1, time) {
      const x = this.thread.x * time;
      return x; // Return x to see its value instead of continuing
      const v = arg1[this.thread.y][x];
      return v;
    }).setOutput([100, 100]);
    
    // Debugging graphical output
    const gpu = new GPU({ mode: 'cpu' });
    const kernel = gpu.createKernel(function(arg1, time) {
      const x = this.thread.x * time;
      if (x < 4 || x > 2) {
        this.color(1, 0, 0); // Red
        return;
      }
      return arg1[this.thread.y][x];
    }, { output: [100, 100], graphical: true });
  10. Install GPU.js in the Browser

    develop

    To use GPU.js in a web browser, include the gpu-browser.min.js script in your HTML. You can host it locally or use a CDN.

    Local path:

    <script src="dist/gpu-browser.min.js"></script>

    CDN options:

    • https://unpkg.com/gpu.js@latest/dist/gpu-browser.min.js
    • https://cdn.jsdelivr.net/npm/gpu.js@latest/dist/gpu-browser.min.js
    <script src="dist/gpu-browser.min.js"></script>
  11. Debug kernels on the CPU

    develop

    If you need to use the debugger statement to inspect a compiled kernel, set the GPU mode to 'cpu'. This runs the kernel on the CPU, allowing you to step through the actual compiled logic.

    const gpu = new GPU({ mode: 'cpu' });
    const kernel = gpu.createKernel(function(arg1, time) {
        debugger; // Breakpoint will trigger here on the CPU
        const v = arg1[this.thread.y][this.thread.x * time];
        return v;
    }, { output: [100, 100] });
    const gpu = new GPU({ mode: 'cpu' });
    const kernel = gpu.createKernel(function(arg1, time) {
        debugger;
        const v = arg1[this.thread.y][this.thread.x * time];
        return v;
    }, { output: [100, 100] });
  12. Use TypeScript with GPU.js

    develop

    GPU.js provides full TypeScript support. You can use IKernelFunctionThis to type the this context within your kernel functions, allowing access to this.thread, this.constants, etc. You can also use generics with createKernel and createKernelMap to enforce strong typing on kernel return values and arguments.

    import { GPU, IKernelFunctionThis } from 'gpu.js';
    const gpu = new GPU();
    
    function kernelFunction(this: IKernelFunctionThis): number {
      return 1 + this.thread.x;
    }
    
    const kernelMap = gpu.createKernel<typeof kernelFunction>(kernelFunction)
      .setOutput([3,3,3]);
    
    const result = kernelMap();
    
    console.log(result as number[][][]);