Skip to main content

GPUHashIndex

Overview

GPUHashIndex builds a fixed-capacity uint32 key/value lookup table, and GPUHashIndexQuery looks up a packed batch of keys without submission or CPU readback. Together they provide the sparse identity lookup that dense group arrays cannot: map stable object IDs to rows, join sparse feature IDs to attributes, deduplicate identifiers, or resolve categorical dictionaries whose key space is much larger than their population.

The first contract is deliberately bounded. Capacity is chosen up front, every row examines at most maxProbeCount slots, one key value is reserved as the empty marker, and both build and query publish collision-work statistics. This makes worst-case GPU work and memory visible to command- graph consumers instead of hiding resize, allocation, or readback behind a map-like API.

Choosing the right hash-graph feature

These features compose; they do not provide competing command schedulers or silently concatenate their inputs:

FeatureInput it owns logicallyUse it when
GPUHashIndexOne packed right-side key batch.The complete lookup dictionary or property table already lives in one contiguous chunk.
GPUBatchHashIndexSeveral ordered right-side key chunks.Streamed record batches must populate one shared index without repacking or losing source-row offsets.
GPUHashIndexQueryOne left-side key batch against either index type.Every source row must retain its position, a found mask, and an optional matched value.
GPUHashJoinOne left-side key batch against either index type.Only matching left/right row pairs should be published, with an explicit output capacity.
GPUBatchHashJoinSeveral preserved left-side chunks against either index type.Independent source batches need separate stable pairs, counts, capacities, and diagnostics.

The right-index choice and left-query choice are independent. For example, a GPUBatchHashIndex can index many Arrow record batches while GPUHashIndexQuery looks up one interactive selection, or GPUBatchHashJoin can join many left batches against that same shared multi-batch index. Every object contributes commands to the caller's existing GPUCommandGraph.

Concepts

Sparse identity is different from dense grouping

GPUGroupAggregation is ideal when category IDs are dense: group i writes output row i. That would be wasteful for object IDs such as 17, 8042, and 3900000000, because a direct-address array must cover the largest possible ID. A hash index stores only a caller-selected capacity and turns each sparse key into a bounded sequence of table probes.

This is the useful bridge between GPU-resident datasets. A visibility result can contain stable object IDs while a property table uses unrelated row numbers; a hash query maps the IDs to rows so a later compute or render pass can gather attributes. The same mechanism supports sparse joins, feature registries, selection membership, and dictionary decoding.

A sorted key/value table offers deterministic layout and logarithmic lookup, and is often better when ordered traversal or range queries are also required. A hash index targets repeated exact-key queries: build once, then resolve many changing batches with near-constant expected work. It also avoids sorting the source solely to establish an identity map.

The tradeoff is explicit. Open addressing becomes more expensive as the table fills, and hostile or unlucky key distributions can consume the probe limit. The build statistics make that cost observable so an application can choose a larger capacity, increase the probe bound, or switch to a sorted representation based on evidence.

Capacity, probing, and overflow

Capacity must be a positive power of two. Keys are hashed to an initial slot and use linear probing for at most maxProbeCount slots, which defaults to capacity. 0xffffffff is reserved as GPU_HASH_INDEX_EMPTY_KEY; source rows containing it are counted as invalid and query rows containing it are reported missing without probing.

The build never resizes. When a distinct key cannot claim or find a slot within the bound, its row increments the overflow statistic. At capacity, the number of retained distinct keys is exact but the retained subset is intentionally unspecified because parallel insertion order is not a source- order contract. Applications that require a particular retained subset must size the table to avoid overflow or preprocess the keys.

Duplicate keys retain a deterministic value

Parallel insertion order does not decide duplicate values. Each occupied slot atomically retains the lowest source-row index, and a final pass copies that row's value. Rebuilding the same inputs therefore produces the same key-to-value mapping even when workgroups execute in a different order. When no value input is supplied, the retained value is firstValue + sourceRow, making the primitive directly useful as a key-to-row index.

An empty explicit values view is also valid, including a zero-length view positioned at the end of its backing buffer. Empty rebuilds clear the table and statistics without binding unavailable input rows.

Statistics expose the cost model

Build statistics contain:

[unique keys, duplicate rows, overflow rows, invalid rows, total probes, maximum probes]

Query statistics contain:

[found keys, missing keys, total probes, maximum probes]

The per-query probes output supports finer diagnostics. Load factor unique / capacity, average probes, maximum probes, and overflow together indicate whether the chosen capacity and bound are healthy. Counters are uint32; construction rejects workloads whose maximum aggregate probe count would overflow them.

Graph ownership and current scope

Callers own the persistent input and output buffers. The build contributes initialization, parallel insertion, and deterministic value-finalization passes; the graph owns only one transient source-row buffer. Query contributes statistics initialization and lookup. Neither object compiles, encodes, submits, resizes, or reads back on its own.

This single-batch primitive supports packed uint32 keys and values and full rebuilds. GPUBatchHashIndex adds preserved right-side vector chunks, while GPUHashJoin and GPUBatchHashJoin provide bounded row-pair publication. Deletion and tombstones, 64-bit keys, custom hash callbacks, independently partitioned right tables, and one-to-many matches remain outside the current contracts.

Usage

const index = new GPUHashIndex({
keys: objectIds,
values: objectRows,
tableKeys,
tableValues,
statistics: buildStatistics,
maxProbeCount: 32
});
index.addToGraph(graph);

new GPUHashIndexQuery({
index,
keys: selectedObjectIds,
values: selectedRows,
found: selectedRowsFound,
probes: selectedRowsProbeCounts,
statistics: queryStatistics
}).addToGraph(graph);

To generate row IDs instead of reading an aligned values buffer:

new GPUHashIndex({
keys: featureIds,
firstValue: batchBaseRow,
tableKeys,
tableValues,
statistics: buildStatistics
}).addToGraph(graph);

Constructors

new GPUHashIndex({
id?,
keys,
values?,
firstValue?,
tableKeys,
tableValues,
statistics,
maxProbeCount?
});

new GPUHashIndexQuery({
id?,
index,
keys,
values,
found,
probes,
statistics,
maxProbeCount?
});

All views are packed GraphDataView<'uint32'> values in the target graph. Table key and value capacities must match, build statistics require six rows, query statistics require four rows, and aligned query outputs must match the query-key length. Writable views cannot overlap each other or their read inputs.