Modulo Arithmetic in GPU.js Thread Indexing

This article explores whether standard modulo arithmetic can be applied to thread indexing within gpu.js kernels. It provides a concise explanation of how the modulo operator functions during the compilation of JavaScript to WebGL shader code, demonstrates practical use cases such as mapping linear dimensions and cyclic boundary wrapping, and highlights critical nuances regarding floating-point handling and performance on the GPU.

Yes, thread indexing in gpu.js can be manipulated using standard JavaScript modulo arithmetic (%). In gpu.js, kernels access individual thread execution positions through this.thread.x, this.thread.y, and this.thread.z. When you apply the % operator to these properties, the gpu.js compiler translates the operation into equivalent shader code (typically GLSL's mod() function in WebGL 1 or integer remainder in WebGL 2), allowing developers to alter and remap index values directly on the GPU.

Flattening and Reconstructing Multi-Dimensional Indices

One of the primary use cases for modulo arithmetic on thread indices is transforming a single-dimensional kernel space into multi-dimensional matrix coordinates. When running a flat 1D kernel across a large dataset, modulo arithmetic determines the row and column positions efficiently:

const kernel = gpu.createKernel(function() {
    const width = 100;
    const col = this.thread.x % width;
    const row = Math.floor(this.thread.x / width);
    return col + row;
}).setOutput([10000]);

In this setup, each batch of 100 threads resets its column index from 0 to 99, effectively converting linear thread indices into a 2D coordinate grid.

Circular Buffers and Boundary Wrapping

Modulo arithmetic is also utilized to implement circular buffers, toroidal grids (such as in cellular automata simulations), or repeating pattern generation. If an algorithm requires sampling neighboring cells without checking complex branch conditions at the boundaries, modulo indexing wraps coordinates seamlessly:

const toroidalKernel = gpu.createKernel(function(grid, size) {
    const nextX = (this.thread.x + 1) % size;
    const prevX = (this.thread.x - 1 + size) % size;
    return grid[this.thread.y][nextX] + grid[this.thread.y][prevX];
}).setOutput([512, 512]);

Adding size before applying the modulo ensures that the dividend remains positive, avoiding negative index edge cases.

Technical Considerations and Limitations

While modulo operations work reliably in gpu.js, several low-level behaviors must be kept in mind:

  1. Negative Numbers: The JavaScript % operator calculates the remainder, not a strict mathematical modulo. For negative values, (-1 % 5) yields -1. To ensure positive array indices, offset negative values prior to the operation.
  2. WebGL Transpilation: In WebGL 1 mode, shaders primarily rely on floating-point arithmetic. Modulo calculations on large integer values may encounter precision limits if numbers exceed 24-bit floating-point integer representations (values above \(16,777,216\)).
  3. Performance: Modulo division is computationally heavier on GPU execution units than addition or bitwise operations. When working with power-of-two dimensions (such as 256, 512, or 1024), using bitwise AND operations (this.thread.x & (width - 1)) can provide better performance than modulo arithmetic.