Skip to main content

GPUTranspose

At a glance

QuestionAnswer
ProblemTranspose one packed two-dimensional scalar field without leaving the command graph.
Reads / writesReads a row-major source view and writes a caller-provided transposed destination.
OwnershipPublic inputs and outputs are caller-owned; scratch storage is graph-owned transient memory.
Output contractOne exact out-of-place transposed field.
Expected workBounded tiled dispatches over shared source/destination chunk regions.
ChunksOne packed 2D domain with independent input/output chunk boundaries.
Conditions / budgetsMay be conditioned with its dependent branch; encoding, submission, and publication remain application-owned.
Neighborhoodpacked field → GPUTranspose → FFT, matrix operation, or layout conversion.

Overview

GPUTranspose adds an out-of-place two-dimensional matrix transpose to a GPUCommandGraph. It operates directly on packed graph data views or chunked vectors, supports rectangular matrices and partial edge tiles, and does not compile, submit, or read data back by itself.

The implementation moves 16-by-16 tiles through workgroup memory. The tile has one padded column to reduce workgroup-memory bank conflicts, and both reads and writes remain coalesced. This makes the primitive useful as a data-layout building block for multidimensional FFTs, tensor-like algorithms, and tiled numerical kernels without introducing a tensor abstraction.

When to use

Use GPUTranspose when a row-major GPU field must change axis order and the transposed result feeds another GPU operation, especially for rectangular FFT stages or tiled numerical pipelines. It is most useful when it avoids CPU readback or makes later memory access coalesced. For tiny matrices, one-off CPU data, or consumers that can read the original strided layout efficiently, a separate GPU transpose pass may cost more than it saves.

Usage

import {GPUTranspose} from '@luma.gl/experimental';

graph.add(new GPUTranspose({
id: 'transpose-points',
input,
output,
rows: 480,
columns: 640
}));

input and output accept packed, four-byte-aligned GraphDataView or GraphVectorView values with the same scalar format. Supported formats are float32, sint32, and uint32. Each operand must contain at least rows * columns elements. Source and destination chunks must use separate graph buffers, and output chunks must not overlap one another.

The input uses row-major indexing input[row * columns + column]. The output has shape columns by rows and uses output[column * rows + row].

Chunk boundaries may differ and may split either matrix rows or tiles. They describe storage for one logical matrix. Extra input/output capacity is untouched. Every encoding overwrites the matrix prefix using the current inputs; raw 32-bit transfers preserve signed zeros, NaN payloads, and integer values exactly.

Empty matrices

rows and columns are non-negative integers. When either is zero, the transpose contributes no graph node and allocates no transient resources. Non-empty dimensions do not need to be multiples of 16; bounds checks handle every partial tile.

Graph and ownership contract

Every chunk must belong to the graph passed to getCommandNodes(), including unused capacity. The primitive contributes one compute node per source/destination pair with shared elements, declaring the source as storage-read and the destination as storage-write. Atomic operands retain one node. It uses no transient buffers, packing, or hidden copies. Compilation, physical resource resolution, command encoding, submission, and optional readback stay with the graph and caller.

Statistics and limits

transpose.stats and makeGPUTransposeStats(rows, columns) expose the logical element count, tile grid, tile count, and fixed [16, 16, 1] workgroup size. The graph maps the tile count across WebGPU's bounded three-dimensional dispatch space. The device must support 256 invocations per workgroup and workgroup dimensions of at least 16 by 16.

These statistics describe the logical matrix. Individual node workloads report their shared element counts and actual tile regions. Chunk boundaries may cause the same logical tile to appear in multiple nodes, so the sum of dispatched tile counts may exceed transpose.stats.tileCount.

The matrix element count must fit a uint32 index range. Logical view capacity and the graph's buffer descriptors apply their own byte-size and device-limit checks.

Performance notes

Transpose is normally limited by global-memory bandwidth: every element is read once and written once, with little arithmetic to hide memory latency. The 16-by-16 tile turns the otherwise strided destination traffic into coalesced writes, while its padded workgroup-memory row reduces bank conflicts. Matrices whose dimensions are multiples of 16 use every invocation; narrow matrices and partial edge tiles spend some lanes only on synchronization and bounds checks.

For each source/destination chunk pair, lowering intersects their matrix regions and dispatches only the shared tile bounds. The kernel masks both source and destination membership, reading and writing each logical element once across the complete operation. Empty intersections produce no dispatch. Planning considers every input/output chunk pair, and heavily fragmented boundaries add partially occupied workgroups and dispatch overhead. Optimizing that planning and scheduling remains follow-up work; chunk storage is never combined implicitly.

Subgroups do not remove the need for a cross-row data reorder before coalesced writes, and subgroup matrix shapes vary between devices. The portable workgroup tile is therefore the primary path. A future device-specific subgroup specialization would need to beat the padded tile in the benchmark before becoming an automatic strategy.

Performance measurement

runGPUTransposeBenchmark(device, props) correctness-gates and compares the tiled implementation against a simple one-invocation-per-element reference path. It reports CPU encoding distributions and GPU timestamp distributions when the device exposes timestamp-query. Defaults exercise a 2048-by-2048 matrix; rows, columns, warmup iterations, and measured iterations are configurable.

The benchmark performs explicit readback for its correctness gate. GPUTranspose itself remains free of CPU synchronization. Its node workload metadata also reports the exact logical bytes read and written and the maximum tile and invocation counts for application-level profiling.