@neutrium/rand
v1.0.0
Published
High-performance random number generators for JavaScript, WebAssembly and WebGPU.
Maintainers
Readme
@neutrium/rand
High-performance deterministic and secure random number generators for web and node applications.
The package provides several backend options including low-overhead JavaScript generators, fast WebAssembly (WASM) via the Rust/rand crate and faster WebGPU for large workloads.
Contents
- Installation
- Quick Start
- Demos
- Algorithms
- Choosing an Algorithm and Backend
- API
- Testing
- Benchmarking
- Development
- License
Installation
Install @neutrium/rand with npm or pnpm as shown below. Note Node.js 20 or newer is required (The package is ESM-only).
# Install using pnpm
pnpm add @neutrium/rand
# Install using npm
npm install @neutrium/randQuick start
import { Random } from "@neutrium/rand";
const rng = Random.fast("experiment-42");
const scalar = rng.next();
const integer = rng.next_u32();
const values = rng.float64_array(1_000_000);
const reusable = rng.fill(new Float32Array(1_000_000));
const bounded = rng.integer(100);
const normal = rng.normal(0, 1);Use the uniform asynchronous create API when the backend may vary:
const rng = await Random.create({
seed: "experiment-42"
});
const values = await rng.float64_array(5_000_000);
await rng.destroy_async();Demos
- Monte Carlo Option Pricing - Observe the demo in action using
pnpm demoand going to the url listed.
Algorithms
Overview
A number of algorithms have been included in @neutrium/rand. These algorithms have been selected because they are best-in-class for specific use cases. They are summarised below:
| Algorithm | Primary use | Security | | --- | --- | --- | | Xoshiro128++ | Low-overhead JavaScript scalar calls | Non-cryptographic | | Xoshiro256++ | Sequential CPU Monte Carlo | Non-cryptographic | | Philox4x32-10 | Counter-partitioned CPU/GPU work | Non-cryptographic | | PCG32 | Explicit independent CPU streams | Non-cryptographic | | ChaCha20 | Deterministic state-inference resistance | Cryptographic primitive | | System CSPRNG | Fresh unpredictable entropy | Cryptographic | | SplitMix64 | Internal seed expansion | Non-cryptographic |
Xoshiro128++
A four-word, 128-bit-state generator designed around 32-bit rotates, XORs, and shifts. These operations map efficiently to JavaScript integer operators, making it the scalar default.
References: Blackman and Vigna, Scrambled Linear Pseudorandom Number Generators; xoshiro128++ reference implementation; rand_xoshiro
Xoshiro256++
A 256-bit-state generator for sequential Monte Carlo workloads. The JavaScript implementation represents 64-bit lanes as 32-bit words to avoid BigInt in the hot path. The WASM bridge uses the rand_xoshiro crate.
References: Blackman and Vigna, Scrambled Linear Pseudorandom Number Generators; xoshiro256++ reference implementation; rand_xoshiro
Philox4x32-10
A counter-based generator transforming a 128-bit counter with a 64-bit key over ten rounds and returning four uint32 words per block. Counter independence makes deterministic CPU workers and WebGPU dispatch straightforward. The TypeScript, Rust/WASM, and WGSL implementations are exact-parity tested.
References: Salmon et al., Parallel Random Numbers: As Easy as 1, 2, 3; Random123 Reference Implementations.
ChaCha20
The 20-round ChaCha stream cipher is used as a deterministic random word stream. SHA-256 from @noble/hashes derives the key and nonce material from the public seed API. Security depends on the entropy and secrecy of the seed.
References: Bernstein, ChaCha, a variant of Salsa20; RFC 8439.
System CSPRNG
Uses globalThis.crypto.getRandomValues backed by the platform’s secure random source. Requests are cached for scalar efficiency and chunked to the Web Crypto request limit.
Reference: Web Cryptography API.
SplitMix64
A 64-bit mixer used internally to expand seeds into the generator state. It is exported only for advanced seeding workflows and is not a security generator.
References: Steele, Lea, and Flood, Fast Splittable Pseudorandom Number Generators; SplitMix64 reference implementation.
PCG32
The XSH-RR 64/32 variant combines a 64-bit linear-congruential transition with an output permutation. The odd increment selects one of 2^63 independent streams. Numeric stream IDs must be non-negative integers below 2^63; strings and byte arrays are hashed into that stream space. The JavaScript implementation uses portable BigInt arithmetic; the WASM bridge uses rand_pcg.
References: O'Neill, PCG A Family of Simple Fast Space-Efficient Statistically Good Algorithms for Random Number Generation; PCG reference implementation; rand_pcg
Choosing an Algorithm and Backend
Choosing the best backend can be complicated, as performance depends on more than just generator throughput. Initialisation, allocation, worker coordination, GPU submission, readback, and downstream calculation all impact performance. This section provides guidance on selecting the best backend; however, for performance-critical applications, it is recommended that you benchmark on your target hardware.
CPU (Javascript & WASM) vs GPU (WebGPU)
This package includes implementations that will run on the CPU or the GPU. To reduce the core dependencies, the WebGPU implementations are maintained on a separate subpath:
import { Random } from "@neutrium/rand";
// or
import { Random } from "@neutrium/rand/webgpu";Both paths use the same high-level type names: RandomCapabilities, RandomCreateOptions, RandomGenerator, Recommendation, and RecommendationOptions. The WebGPU subpath widens those types with its optional backend, output-location, and timestamp fields. Recommendation is asynchronous on both paths, so switching imports does not change the calling
convention or require renamed types.
If you have the potential to run on the GPU and don’t mind the additional dependencies, favour importing @neutrium/rand/webgpu.
Ask the library
To receive a deterministic recommendation from the library:
import { Random } from "@neutrium/rand/webgpu";
const recommendation = await Random.recommend({
call_pattern: "bulk",
length: 8_000_000,
security: "none",
output_location: "gpu",
workers: 1
});
console.log(recommendation);
// {
// algorithm: "philox4x32-10",
// backend: "webgpu",
// confidence: "default",
// reason: "...",
// alternatives: [...]
// }If confidence: "benchmark-required" is returned, the backend recommendation is highly dependent on transfer or deployment details.
Recommendation objects intentionally contain valid algorithm and backend creation options. Pass them directly to the corresponding creation API, adding only stream state, such as the seed and stream identifier:
const recommendation = await Random.recommend({ length: 100_000 });
const rng = await Random.create({
...recommendation,
seed: "experiment-42"
});The default crossover values — 16,384 for WASM and 1,000,000 for WebGPU — are conservative routing hints, not performance guarantees. Calibrate them with pnpm benchmark on the deployment target to find the best crossover points.
Manual Selection
The following list is some general guidelines on selecting which algorithm and backend will be best for you.
- If output must be freshly unpredictable, use
Random.secure(). - If output is deterministic but must resist state inference, use
Random.secure(seed). - If calls are scattered scalars, use
Random.scalar(seed). - If work is divided between CPU workers, assign Philox streams/counter ranges or PCG stream IDs.
- If a large simulation consumes values on the GPU, use Philox WebGPU and keep the buffer resident.
- If generated values return to the CPU, benchmark WASM against WebGPU end-to-end.
- Otherwise, use Xoshiro256++ JavaScript for small batches and WASM for large batches.
A general selection matrix is presented below. Note that this does not consider the requirements for cryptographically secure random numbers.
| Environment | Workload | Starting choice | Why | | --- | --- | --- | --- | | CPU only | One or scattered values | Xoshiro128++ JS | Lowest API/setup overhead | | CPU only | Small sequential batch | Xoshiro256++ JS | No WASM boundary cost | | CPU only | Large sequential batch | Xoshiro256++ WASM | Bulk transfer amortises setup | | Multiple CPU workers | Reproducible partitions | Philox | Counter and stream partitioning | | Multiple CPU workers | Long independent streams | PCG32 | Explicit odd-increment streams | | GPU available | Small batch | JavaScript/WASM | GPU submission dominates | | GPU available | Large batch returned to CPU | Benchmark Philox WebGPU and WASM | Readback is hardware-dependent | | GPU available | Large GPU-resident simulation | Philox WebGPU | Avoids the largest transfer |
API
A summary of the high level API is provided below. Detailed API documentation is available at /docs/api/.
import { Random } from "@neutrium/rand";
Random.fast(seed); // Xoshiro256++
Random.scalar(seed); // Xoshiro128++
Random.parallel(seed, stream, counter); // Philox4x32-10
Random.stream(seed, stream); // PCG32
Random.secure(); // System CSPRNG
Random.secure(seed); // Deterministic ChaCha20
await Random.create(options); // Uniform async generator factory
await Random.capabilities(); // Probes runtime capabilities
await Random.recommend(workload); // Recommends a algorithm and backendApplications that may use WebGPU replace only the import. The method names and calling conventions remain the same:
import { Random } from "@neutrium/rand/webgpu";
await Random.capabilities(); // Also reports verified WebGPU availability
await Random.recommend(workload); // Includes WebGPU in workload recommendations
await Random.create(options); // Uniform async API with WebGPU fallbackSeeds may be numbers, bigints, strings, or byte arrays. Equal seed bytes and options produce equal streams across supported platforms and parity-tested backends.
Synchronous generators provide:
next_u32();
next();
next_f64();
fill(target);
uint32_array(length);
float32_array(length);
float64_array(length);
bytes(length);
integer(max_exclusive);
normal(mean, standard_deviation);
exponential(rate);
destroy();fill(target) always reuses a caller-owned buffer. The typed *_array() methods always allocate a new buffer. Synchronous generators return the array directly; Random.create() from either package path exposes the same allocation names asynchronously.
With backend: "auto", the first non-empty array request supplies the workload length used to choose JavaScript, WASM, or optional WebGPU. The selected backend is then retained so subsequent calls continue the same stream.
Passing an AbortSignal to an array method on a generator returned by Random.create() enables cooperative JavaScript and WASM generation in bounded chunks. Cancellation can consume values generated before the signal is observed, so discard or checkpoint that generator when retrying must reproduce the same stream position.
Parallel CPU Workers
Parallel reproducibility requires deterministic ownership of streams or counter ranges. A shared seed will create the same sequences of random numbers.
Philox streams
Give each worker a unique persisted stream ID:
const worker_id = 7;
const rng = Random.parallel("portfolio-2026", worker_id);
const samples = rng.fill(new Float64Array(2_000_000));Do not derive stream IDs from transient process IDs or queue order. A job coordinator should assign them.
Philox emits four uint32 words per counter block. Allocate non-overlapping block ranges when jobs may generate different lengths:
const words_per_job = 4_000_000;
const blocks_per_job = Math.ceil(words_per_job / 4);
const starting_counter = job_id * blocks_per_job;
const rng = Random.parallel(seed, stream_id, starting_counter);Validate that the largest job ID cannot exceed the supported counter space.
PCG streams
Use Random.stream(seed, stream_id) when the application already models long-lived independent CPU streams. Philox is generally easier for arbitrary job partitioning because a counter directly identifies work.
GPU Simulations
Philox is available through WebGPU because its counter-based blocks can be computed independently. WebGPU is useful when a large generated buffer is consumed by subsequent GPU work.
WebGPU is an optional API surface. It is not loaded or included in the root @neutrium/rand declarations. Install the WebGPU declarations and import the dedicated subpath only in projects that use it:
pnpm add @neutrium/rand
pnpm add -D @webgpu/typesAdd "@webgpu/types" to compilerOptions.types, then import the subpath:
import { Random } from "@neutrium/rand/webgpu";CPU readback
const rng = await Random.create({
algorithm: "philox4x32-10",
backend: "auto",
seed,
stream: 7
});
const values = await rng.uint32_array(length);
await rng.destroy_async();This includes allocation, dispatch, command submission, GPU-to-CPU copying, and mapping. Compare it against WASM on the target device. A discrete GPU can have excellent compute throughput while losing the complete workload benchmark to readback latency.
GPU Resident
const rng = await Random.create({
algorithm: "philox4x32-10",
backend: "webgpu",
seed,
stream: 7
});
const generated = await rng.generate_buffer({
length,
usage: GPUBufferUsage.COPY_SRC
});
// Create the consumer bind group on generated.device.
// Bind generated.buffer as read-only storage.
// Submit the consumer work before destroying either object.
generated.buffer.destroy();
await rng.destroy_async();The returned buffer length must be a positive multiple of four, and it must fit maxBufferSize, maxStorageBufferBindingSize, and dispatch limits. Typed-array requests are chunked; GPU-resident requests deliberately fail rather than returning several buffers with an ambiguous consumer contract.
Testing
Several tests are included in @neutrium/rand. These are listed below, along with any requirements in addition to running pnpm bootstrap.
| Command | Additional setup | Purpose | Notes |
| --- | --- | --- | --- |
| pnpm test | None after bootstrap | Deterministic, property, parity, and distribution tests | Node correctness, parity, properties, distribution smoke tests, and mocked WebGPU lifecycle tests |
| pnpm test:coverage | None after bootstrap | Enforced coverage thresholds | Runs the Node suite with enforced coverage thresholds |
| pnpm test:rust | None after bootstrap | Native Rust bridge tests | Uses the toolchain pinned in rust-toolchain.toml |
| pnpm test:package | tar on PATH | Packed ESM, declarations, export restrictions, and file layout | Rebuilds and inspects the packed ESM-only package |
| pnpm test:statistical | None after bootstrap | Deterministic distribution smoke tests | Fast deterministic distribution checks; it does not invoke PractRand |
| pnpm test:browser | Playwright Chromium, Firefox, and WebKit | Tests Chromium, Firefox, and WebKit availability | Builds first, then runs the browser suite in all three engines |
| pnpm statistical:practrand | PractRand RNG_test executable on PATH | Statistical testing using the PractRand battery | Streams a large battery input; PractRand is not a pnpm dependency |
Generated test artifacts are grouped under reports/: coverage output in reports/coverage, Playwright's HTML report in reports/playwright, and individual test-run artifacts and statistical reports in reports/tests.
Statistical Tests
External statistical test suites are not installed by default and require additional setup.
PractRand
PractRand is a C++ test suite and can be downloaded from the
official PractRand site. Follow installation.txt to build the RNG_test command and place that executable on PATH.
Use a small local smoke run before starting the default multi-gigabyte battery:
PRACTRAND_WORDS=1048576 \
PRACTRAND_SEEDS=smoke \
pnpm statistical:practrandFor a release-scale run:
PRACTRAND_ALGORITHM=xoshiro256++ \
PRACTRAND_WORDS=268435456 \
PRACTRAND_SEEDS=run-a,run-b,run-c \
PRACTRAND_REPORT=reports/tests/practrand.json \
pnpm statistical:practrandTestU01 and Other External Batteries
Install and build the battery separately, then generate a reproducible binary stream for the adapter or harness you use with it:
pnpm stream philox4x32-10 ./philox.bin 1000000000 test-seedExact backend parity means one algorithm stream can receive the expensive statistical battery while every backend receives parity tests.
Benchmarking
This library includes benchmarks to compare the performance of each algorithm on your hardware. The benchmarks measure generator speed, setup costs, memory allocation patterns, backend initialisation, and end-to-end workload behaviour with the JavaScript, WASM, worker-based, and WebGPU backends, where supported.
Benchmark Process
Running pnpm benchmark builds the package and then runs the main Node benchmark harness. The benchmark results can be used to understand:
- Which backend is best for single values or bulk workloads
- How allocation overhead changes with array size
- Whether backend initialisation dominates short-lived workloads
- How WebGPU behaves when output must be copied back to the CPU
All benchmarks run adaptively for a minimum duration of 250ms (by default). The reported values are the median and interquartile range, with raw sample rates being saved in the /reports/benchmarks/ folder.
Running the Benchmarks
There are several benchmarks you can run to understand performance with different configurations:
pnpm benchmarkmeasures the generator performance on CPU using javascript and wasmpnpm benchmark:scenariosmeasures the performance in realistic situations by determining the value of π through a Monte Carlo simulationpnpm benchmark:workersmeasures the cost and scaling of a consistent workload across various numbers of node worker threadspnpm benchmark:web-workersmeasures the cost and scaling of a consistent workload across various numbers of web worker threadspnpm benchmark:webgpuis the dedicated browser-backed WebGPU comparison.pnpm benchmark:allruns all aforementioned benchmarks
Benchmark Parameters
Several benchmark parameters can be set, as shown below:
BENCH_MIN_SAMPLE_MS=500 \
BENCH_SAMPLES=15 \
pnpm benchmarkThe full list of available benchmark parameters are listed below:
| Parameter | Description | Scope | Default |
| :--- | :--- | :--- | :--- |
| BENCH_JSON | Sets the output file for the raw report | benchmark | reports/benchmarks/main.json |
| BENCH_MIN_SAMPLE_MS | How long each sample runs in milliseconds | all | 250 |
| BENCH_SIZES | The array lengths tested | all | 1,64,1024,16384,262144,1048576 |
| BENCH_SAMPLES | How many samples are collected | all | 10 |
| BENCH_WORKERS | Number of workers to use in each simulation | workers, web-workers | 1, 2, 4, 8 |
| BENCH_WORKER_NUMBERS | Total number of samples to run for each simulation | workers, web-workers | 20_000_000 |
| BENCH_WEB_WORKER_JSON | Sets output file for the raw report | web-worker | reports/benchmarks/web-workers.json
| BENCH_WEB_WORKER_CHANNEL | Sets the chromium executable used by Playwright | web-worker | chromium |
| BENCH_WEB_WORKER_HEADED | Sets whether benchmark is run headed | web-worker | 0 |
| BENCH_WORKER_JSON | Sets the output file for the raw report | workers | reports/benchmarks/workers.json |
| BENCH_SCENARIO_JSON | Sets the output file for the raw report | scenario | reports/benchmarks/scenarios.json |
| BENCH_WEBGPU_JSON | Sets output file for the raw report | webgpu | reports/benchmarks/webgpu.json
WebGPU Specifics
Node cannot assume a GPU-backed navigator.gpu, so this benchmark launches Chromium and runs an isolated WebGPU workload there.
For a quick hardware check, run the smoke test:
pnpm benchmark:webgpu:smokeIf headless Chromium cannot access the GPU for webgpu tests, retry with:
BENCH_WEBGPU_HEADED=1 pnpm benchmark:webgpuThe benchmark rejects software fallbacks such as SwiftShader and llvmpipe by default. Set BENCH_WEBGPU_ALLOW_FALLBACK=1 when you intentionally want to measure software WebGPU.
Benchmark Regression Comparison
To compare two benchmark runs and check for regressions, generate separate JSON files for each benchmark:
BENCH_JSON=reports/benchmarks/baseline.json pnpm benchmark
BENCH_JSON=reports/benchmarks/candidate.json pnpm benchmarkA regression can then be run using pnpm benchmark:compare, optionally setting a regression threshold:
BENCH_REGRESSION_PERCENT=10 \
pnpm benchmark:compare \
reports/benchmarks/baseline.json \
reports/benchmarks/candidate.jsonIf the performance percentage change for any of the benchmarks exceeds the threshold, benchmark:compare will exit with a non-zero return value.
Development
Setup
The build requires:
- Node.js 20 or newer.
- The pnpm version declared by
packageManagerinpackage.json. - rustup, which manages the Rust compiler and WebAssembly compilation target. No previous Rust experience or separate Cargo installation is required.
After cloning the repository, run:
pnpm run bootstrapThis command verifies Node and pnpm, installs the Rust toolchain, profile, and targets
declared by rust-toolchain.toml, installs pnpm dependencies, and builds the library.
If rustup is not installed, pnpm run bootstrap stops with an installation link rather than failing later in the build. Restart the terminal after installing rustup so its commands are available on PATH.
For browser tests and browser-backed benchmarks, install Playwright's browsers separately because they are a large optional download:
pnpm run setup:browsersVerification
Run the complete non-browser verification suite and browser tests with:
pnpm check
pnpm test:browser
pnpm benchmark:smokeLicense
This project is licensed under the MIT License - see the LICENSE file for details. The published bundles also contain third-party software. Its attribution and license terms are recorded in THIRD_PARTY_NOTICES.md, and the machine-readable component inventory is published as sbom.cdx.json.
What this Means
You are free to:
- Use this plugin for personal or commercial purposes
- Modify and distribute the code
- Include it in other projects
Under the following conditions:
- You must include the original license and copyright notice
Disclaimer
This plugin is provided "as is", without warranty of any kind. Use at your own risk.
