Skip to main content

GPU Evaluators

@luma.gl/graph/gpu-simple-graph has two evaluator layers:

  • GPUDataEvaluator runs one lazy transform over one fixed-width GPUData chunk or GPUDataView.
  • GPUVectorEvaluator applies one lazy GPUDataEvaluator transform independently to every ordered GPUData chunk in a GPUVector.

There is no GPUTable evaluator input path. Streaming code should pass an incoming GPUData chunk or borrowed GPUDataView directly to GPUDataEvaluator operations, or wrap a GPUVector with GPUVectorEvaluator when the same transform should preserve every existing chunk boundary.

GPU evaluator split

Usageโ€‹

One incoming GPUDataโ€‹

import {GPUDataEvaluator, add} from '@luma.gl/graph/gpu-simple-graph';

const offset = GPUDataEvaluator.fromConstant([1, 2, 3]);
const translatedChunk = add(incomingGPUData, offset);

const translatedVector = await translatedChunk.evaluate(device);

One GPUVectorโ€‹

import {GPUDataEvaluator, GPUVectorEvaluator, add} from '@luma.gl/graph/gpu-simple-graph';

const offset = GPUDataEvaluator.fromConstant([1, 2, 3]);
const translatedVector = GPUVectorEvaluator.fromGPUVector(vector).mapGPUData(data =>
add(data, offset)
);

const outputVector = await translatedVector.evaluate(device);

GPUVectorEvaluator preserves vector.data[] order and chunk boundaries. It does not combine streaming batches or pack buffers implicitly.

Interleaved attribute inputโ€‹

import {add} from '@luma.gl/graph/gpu-simple-graph';
import {makeGPUDataViewFromAttribute} from '@luma.gl/graph/gpu-data';

const positions = makeGPUDataViewFromAttribute({
buffer: interleavedBuffer,
bufferLayout,
attributeName: 'positions',
length: instanceCount
});

const translatedPositions = add(positions, [10, 0, 0]);
const packedOutput = await translatedPositions.evaluate(device);

Input views retain their offset and stride, so multiple attributes may borrow the same interleaved buffer. Operation results use newly materialized packed outputs by default. Call setTargetBuffer() before evaluation to direct an operation result into an existing buffer range.

Evaluate into an existing bufferโ€‹

import {cleanEvaluate, fround, GPUDataEvaluator} from '@luma.gl/graph/gpu-simple-graph';

const source = GPUDataEvaluator.fromArray(new Float64Array(values), {size: 3});
const splitPositions = fround(source);

splitPositions.setTargetBuffer({
buffer: attributeBuffer,
byteOffset: destinationByteOffset
});

await cleanEvaluate(device, splitPositions);

The target is borrowed. Evaluation writes directly into attributeBuffer and does not allocate or copy through a separate output buffer. cleanEvaluate() recycles unreferenced owned dependency buffers, but preserves the borrowed root target. Calling splitPositions.destroy() is not required to release the target and never destroys attributeBuffer.

GPUDataEvaluatorโ€‹

GPUDataEvaluator describes a 2D row layout backed by CPU values, one borrowed GPUData chunk, borrowed GPUDataView, another GPUDataEvaluator, or a lazy Operation output. Each row contains size scalar elements of the same numeric type.

GPUDataEvaluatorPropsโ€‹

PropertyTypeDescription
id?stringOptional debug name used by toString().
typeSignedDataTypeScalar element type, such as 'float32' or 'uint32'.
sizenumberNumber of scalar elements in each row.
offset?numberByte offset to the first element of the first row. Defaults to 0.
stride?numberByte distance between adjacent rows. Defaults to ValueType.BYTES_PER_ELEMENT * size.
normalized?booleanWhether integer values are normalized when exposed as vertex formats.
value?TypedArrayCPU-side data for the evaluator.
buffer?Buffer | DynamicBufferBorrowed GPU buffer backing this evaluator.
gpuData?GPUDataBorrowed fixed-width GPUData chunk backing this evaluator.
format?GPUVectorFormatOptional memory format preserved for GPUVector interop.
source?Operation | GPUDataEvaluator | nullLazy source for this evaluator.
isConstant?booleanWhether every row shares the same value. Defaults to false.
length?numberRow count. Optional when isConstant is true or value is provided.

Static Methodsโ€‹

GPUDataEvaluator.fromArray(value, props?): GPUDataEvaluatorโ€‹

Creates one evaluator from a typed array or numeric array. Plain JavaScript arrays use props.type or 'float32' by default. Float64Array inputs are reinterpreted as uint32 pairs for GPU-oriented operations such as fround().

GPUDataEvaluator.fromConstant(value, type?): GPUDataEvaluatorโ€‹

Creates one constant evaluator with a shared row value. A scalar becomes a one-element row, and an array becomes a row with value.length elements.

GPUDataEvaluator.fromGPUData(data, options?): GPUDataEvaluatorโ€‹

Creates one evaluator view over a fixed-width GPUData chunk. The input must have a fixed-width GPUData.format and matching rowByteLength. Strided rows are preserved. The evaluator borrows data.buffer and does not destroy it.

GPUDataEvaluator.fromGPUDataView(view, options?): GPUDataEvaluatorโ€‹

Creates an evaluator over a borrowed fixed-width GPUDataView, preserving its format, length, byte offset, and byte stride. Existing operations accept views directly through GPUDataEvaluatorInput.

CPU, WebGL, and WebGPU support strided 32-bit component formats. Other formats remain subject to backend capabilities; unsupported WebGPU storage types fail explicitly rather than being repacked. Offsets and strides must be aligned to the stored scalar component width.

Methodsโ€‹

setTargetBuffer(target: GPUDataEvaluatorTargetBuffer): voidโ€‹

Assigns borrowed storage for the next evaluation of a deferred operation output. This method is only valid before evaluation and on an evaluator whose source is an Operation.

PropertyTypeDescription
bufferBufferBorrowed buffer that receives the operation output.
byteOffset?numberByte offset of the first output row. Defaults to 0.
byteStride?numberByte distance between adjacent output rows. Defaults to the operation output's current packed stride. Backend layout restrictions still apply.

setTargetBuffer() records the target without changing the evaluator's logical layout. When evaluation begins, the target replaces the normal pooled output allocation and offset, stride, and byteLength are updated to describe the physical target layout. The target buffer must belong to the evaluation device and be large enough for the resulting range.

The evaluator borrows the assigned buffer. Evaluating or destroying the evaluator never transfers ownership of or destroys that buffer.

evaluate(device: Device, options?): Promise<GPUVector>โ€‹

Materializes one single-chunk GPUVector on the provided device. Lazy dependencies are evaluated before the operation handler writes the output.

evaluateSync(device: Device, options?): GPUVectorโ€‹

Materializes one single-chunk GPUVector synchronously. This is useful for call sites that must stay synchronous, but it is stricter than evaluate():

  • backend lookup must already be resolved
  • dependencies are evaluated recursively without awaiting
  • any required CPU value must already be available

If those conditions are not met, evaluateSync() throws.

readValue(startRow?: number, endRow?: number): Promise<TypedArray>โ€‹

Reads evaluator contents back to the CPU. This is intended for debugging or inspection and may be slower than staying on the GPU.

destroy(): voidโ€‹

Releases cached GPU storage owned by this evaluator and prevents future evaluation. Borrowed buffers supplied through the constructor, fromGPUData(), fromGPUDataView(), or setTargetBuffer() are not destroyed.

GPUVectorEvaluatorโ€‹

GPUVectorEvaluator is the official GPUVector path. It wraps ordered GPUDataEvaluator chunks and materializes one output GPUVector with the same chunk boundaries.

Static Methodsโ€‹

GPUVectorEvaluator.fromGPUVector(vector): GPUVectorEvaluatorโ€‹

Creates one chunk-preserving evaluator over a fixed-width GPUVector. The vector must have at least one GPUData chunk and must not be interleaved.

GPUVectorEvaluator.fromGPUDataEvaluators(evaluators, options?): GPUVectorEvaluatorโ€‹

Creates one vector evaluator from already-built ordered chunk evaluators.

Methodsโ€‹

mapGPUData(transform): GPUVectorEvaluatorโ€‹

Applies one lazy GPUDataEvaluator transform independently to each preserved chunk. Use this for row-local streaming transforms that should not repack source batches.

evaluate(device: Device, options?): Promise<GPUVector>โ€‹

Materializes every chunk evaluator and returns one GPUVector with preserved chunk order and boundaries.

evaluateSync(device: Device, options?): GPUVectorโ€‹

Synchronously materializes every chunk evaluator and returns one GPUVector with preserved chunk order and boundaries. This has the same synchronous requirements as GPUDataEvaluator.evaluateSync().

destroy(): voidโ€‹

Releases cached GPU resources owned through child GPUDataEvaluator instances.

Remarksโ€‹

  • Leaf operations accept GPUDataEvaluator, GPUData, or GPUDataView, not GPUVector.
  • Use GPUVectorEvaluator.fromGPUVector(vector).mapGPUData(...) for vector-wide transforms that should preserve streaming chunks.
  • GPUDataEvaluator operation outputs own their automatically allocated backing resource. Outputs configured with setTargetBuffer() borrow their backing resource instead.
  • Borrowed GPUData chunks are not destroyed by GPUDataEvaluator.destroy().
  • Borrowed GPUDataView buffers are not destroyed by GPUDataEvaluator.destroy().
  • Target buffers assigned with GPUDataEvaluator.setTargetBuffer() are not destroyed or recycled by the evaluator.
  • Synchronous evaluation is intended for already-prepared graphs where backend registration and any required CPU values are available up front.

Experimental explicit-submission POCโ€‹

Why this is neededโ€‹

The portable evaluator is a suitable replacement for backend-specific attribute transforms, but its WebGPU handlers currently own submission. Separate attribute outputs and chains of different operation types can therefore call device.submit() repeatedly during one deck.gl update. A host renderer cannot record those operations and its consumers together on an explicit command encoder or choose a single submission point.

This is a scheduling and integration gap, not an existing GPU ordering bug: sequential queue submissions already preserve producer/consumer order. Nor does every nested arithmetic call cause a submission: the evaluator already fuses arithmetic expressions. The useful cases are multiple outputs and chains crossing operation boundaries, such as arithmetic followed by a non-contiguous swizzle and another arithmetic expression. GPU aggregation is another prospective consumer, but reduction support is outside this initial POC.

GPUEvaluationScope demonstrates recording those existing row operations without submitting, then letting the caller submit once. Tests verify submission count and results; a frame-time improvement has not been measured. This draft API is intended to establish the execution and resource-lifetime contract before extending every backend handler or integrating deck.gl.

Exampleโ€‹

import {GPUDataEvaluator, GPUEvaluationScope, add, multiply, swizzle} from '@luma.gl/graph/gpu-simple-graph';

const source = GPUDataEvaluator.fromArray(new Float32Array([1, 2, 3, 4]), {size: 2});
const scaled = multiply(source, 2);
const reordered = swizzle(scaled, [1, 0]);
const positions = add(reordered, 1);
const colors = add(source, 10);

const scope = new GPUEvaluationScope(device);
try {
const positionVector = await scope.evaluate(positions);
const colorVector = await scope.evaluate(colors);
// No evaluator submission has occurred. Additional commands consuming these
// buffers may be appended to scope.commandEncoder before submission.
scope.submit();
} catch (error) {
// Evaluation errors already cancel the scope; cancel() also handles errors
// in caller-appended commands and is harmless after an earlier cancellation.
scope.cancel();
throw error;
}

The existing evaluator.evaluate(device) API retains its current behavior. This POC does not change its submission policy, fuse additional shaders, or replace the command-graph API.

Submission and lifetime contractโ€‹

  • A scope is WebGPU-only and owns its encoder. Append commands through commandEncoder, but call scope.submit() to finish and submit it. Do not finish or submit the encoder separately.
  • Explicit scopes select opted-in WebGPU handlers even for single-row attributes and constants. The ordinary evaluate(device) API retains its CPU fast path. GPU-computed constants stay GPU-resident and broadcast from row zero when consumed by later row operations.
  • Await each scope.evaluate() before starting another evaluation, submitting, or cancelling. Evaluation finishes CPU encoding; it does not wait for GPU completion.
  • The scope retains participating evaluators and computation resources. Evaluator destruction, readback, and evaluation outside the scope are rejected while the scope is pending. Existing output buffers remain available for binding to later commands on the same encoder.
  • Successful submission releases these evaluator leases. Queue ordering allows subsequent GPU consumers without a CPU wait. Explicit readback still waits for the GPU in the usual way.
  • cancel() is harmless after submission or an earlier cancellation. Otherwise it discards commands and invalidates newly materialized evaluator views, allowing a fresh evaluation. Existing materializations are preserved. Assigned output targets and layouts are restored so a cancelled operation can be retried.
  • Evaluation failure cancels the entire scope, including earlier successful evaluations. Submitted and cancelled scopes cannot be reused. Batch submission must not race evaluation.
  • The scope borrows external buffers. The application must keep raw buffers alive and unchanged until submission and must not read pending returned vectors directly. These raw resource operations cannot be intercepted by evaluator leases. CPU input uploads may occur immediately; cancellation discards encoded computation, not queue writes or external application changes.

Deliberate POC limitsโ€‹

GPUDataEvaluator, chunk-preserving GPUVectorEvaluator, and opt-in WebGPU row handlers are supported: arithmetic, dot, equalAll, fround, interleave, length, select, and swizzle. CPU fallback, multi-pass extent, gather, segmentedMap, sequence, color conversion, and raw-byte casts are not included. Unsupported handlers fail explicitly rather than internally submitting. The existing immediate-submission API remains available for those operations.

Before promoting this API, extend supported handlers, measure representative deck.gl frame workloads, and decide whether this execution scope should instead lower into the existing GPU command graph. Caller-owned encoders and a coordinated submission receipt would also need an explicit lifetime design; a bare submit: false flag is insufficient.

Inspection metadataโ€‹

GPUDataEvaluator.getInspection() and GPUVectorEvaluator.getInspection() return an immutable snapshot of the lazy dependency graph without allocating GPU resources, resolving backend handlers, executing work, or reading back data. outputs contains ordered node references (one per vector chunk); nodes describes operation names, dependencies, layout, canonical format when representable, and current evaluation state. Shared evaluators appear once, even when several chunks reference them. IDs are local to each snapshot. Nodes describe execution dependencies after expression fusion, so an authored arithmetic chain may appear as one operation. Snapshots contain no buffer handles or CPU payloads. Request another snapshot to observe later evaluation state.

const inspection = vectorEvaluator.getInspection();
for (const node of inspection.nodes) {
console.log(node.id, node.operation, node.dependencies, node.format);
}

Vector evaluation scopesโ€‹

GPUEvaluationScope.evaluate() accepts both data and vector evaluators. On WebGPU, vector chunks are recorded sequentially into the scope's encoder, preserving their order and boundaries and reusing shared dependencies. Supported backend handlers must explicitly support deferred submission.

const scope = new GPUEvaluationScope(device);
try {
const vector = await scope.evaluate(vectorEvaluator);
// Bind vector chunks in further commands on scope.commandEncoder here.
scope.submit();
} finally {
scope.cancel(); // Harmless after submission; discards work on failure.
}

Await each evaluation before starting another or submitting. Pending outputs must not be read or consumed outside this encoder until submission. Evaluators and their chunks, including cached outputs, remain retained until submission or cancellation. Cancellation discards newly cached outputs and preserves previously evaluated ones; a failing chunk cancels the whole scope. Borrowed buffers must remain alive and unchanged until submission. Ordinary evaluate() retains its existing caching and parallel chunk evaluation behavior. These APIs belong to @luma.gl/graph/gpu-simple-graph and do not depend on @luma.gl/graph.