Skip to content

GPU reference card

This page covers the KDB-X GPU module APIs, including inputs, outputs, and examples for each function.

GPU objects

Results that live on the GPU are returned as KDB-X virtual objects (type 112h): GPU tables, keyed tables, vectors and dictionaries are virtual tables, and tensors are virtual tensors. Results on the CPU (for example .gpu.from, or .gpu.lj of two CPU tables) are plain q data. Every .gpu function accepts both virtual objects and plain q data.

Plain q works on a virtual table: select, exec (without by) and functional ?[T;c;b;a] (including select[n] and select[n;<c]), lj, ij, aj, aj0, ajf, ajf0, wj, wj1, sublist, `c xasc T, meta T, cols T, `k xkey T and console display, which fetches only the rows shown. On a virtual tensor, sublist slices the leading axis as q's sublist slices a matrix, and console display fetches only the rows shown, inside a list or dictionary too. These keywords keep their native behavior for CPU arguments.

q primitives do not see inside a virtual object:

Primitives return wrong data, not an error

#, _ and @ cannot be intercepted, so q applies them to the virtual wrapper itself. 3#v and 3#T return copies of the whole object in a general list, and 1 _ v and v@1 3 signal type or rank. Never use them on a virtual object; use the .gpu functions below, or sublist, which does work.

On a virtual tensor X, most q primitives signal type, rank or length, but these return a value that can pass for an answer:

  • The reductions and scans sum, prd, min, max, sums, mins, maxs, deltas, ratios, fills, prev, +/, */, |/ and +\, and first, last, reverse, {x} each X and 2 mmax X return X itself, unchanged.
  • inv X returns `T, not a matrix inverse (inv is !:, so it is key X).
  • X=Y, X in Y and X~Y compare the wrappers, not the elements, and give one boolean: 1b for the same object (X=X), and 0b for two tensors whose elements are equal.
  • count X and count each X are 1, 2 mcount X is ,1i, null X is 0b, type X is 112h, attr X is `, value X is the wrapper dictionary, and 2#X, X,X and enlist X are lists holding the wrapper.

raze X, distinct X and group X work on a host copy and give q's answer.

Instead of Use
T`col or T[`col] exec col from T
count T (returns 1) .gpu.count T
update/delete ... from T .gpu.update
n!T `k xkey T (and () xkey K to unkey)
flip T, key T, value T, x!y .gpu.from first
n#v, n#T, `c#T, n _ v, v@i .gpu.take, .gpu.drop, .gpu.gather
type T, attr T .gpu.type, .gpu.attr
sum X, max X, first X ... on a tensor (return X) .gpu.tensor.sum[axes;keepdims;X], .max, .first ... (reductions)
sums X, maxs X ... on a tensor (return X) .gpu.tensor.sums[axis;X], .maxs ... (scans)
inv X (returns `T) .gpu.tensor.inv X
count X (returns 1) first .gpu.tensor.shape X
X=Y, X~Y (compare the wrappers) .gpu.tensor.from both first
deltas, ratios, fills, prev, reverse, mmax, mcount on a tensor .gpu.tensor.from first, then the q primitive

Nested lists

A nested (list of lists) column can be transferred with .gpu.to, read back with .gpu.from, counted with .gpu.count, sliced by rows with .gpu.take and .gpu.sublist, displayed at the console, and carried through .gpu.lj and the .gpu.aj family as a column of the left table. .gpu.iasc, .gpu.idesc, .gpu.asc, .gpu.xasc, .gpu.xdesc, .gpu.gather, .gpu.append and .gpu.bin reject it with GPU_NOT_IMPLEMENTED, and leave the object unchanged. So do .gpu.aj, .gpu.aj0, .gpu.ajf and .gpu.ajf0 when a column of t2 that they would return is nested, and .gpu.ajf and .gpu.ajf0 when they would fill from a nested column of t1 (.gpu.lj and .gpu.ij already reject a nested right-hand column). The console shows the first rows of a GPU object by slicing it on the GPU first; a nested object cannot be sliced there, so it is read back whole and sliced on the host, which copies the whole object.

Numeric keywords such as sum, abs and mmu are not overloaded; use .gpu.tensor for tensors. A select on a tensor signals type.

Keywords

aj append asc attr bin cntDev compact config cor count
cov drop from gather getDev getMemRelThres group iasc idesc ij
lj memDev meta nvtx onDev profiler scov select setDev setMemRelThres
sublist sync take tensor to type ungroup update ver wj
wj1 xasc xdesc xgroup xto

By category

data transfer from, to, xto
configuration config
device management cntDev, compact, getDev, getMemRelThres, memDev, onDev, setDev, setMemRelThres, sync
list append, count, drop, gather, group, sublist, take
join aj, ij, lj, wj, wj1
math cor, cov, scov
meta attr, type
query select, update
sort asc, bin, iasc, idesc
table meta, ungroup, xasc, xdesc, xgroup
tensor tensor

Note

Examples use the following naming convention:

  • Lowercase names indicate CPU-resident objects (for example, t, a).
  • Uppercase names indicate GPU or mixed-residency objects (for example, T, A).
q).gpu:use`kx.gpu
q)\S 42

aj

As-of join

.gpu.aj  [c; t1; t2]
.gpu.aj0 [c; t1; t2]
.gpu.ajf [c; t1; t2]
.gpu.ajf0[c; t1; t2]

Where:

  • t1 is a CPU, GPU or mixed-residency table
  • t2 is a CPU, GPU or mixed-residency table
  • c is a symbol vector of n column names, common to t1 and t2, and of matching type
  • column cn is of a sortable type (typically time)

returns a table with records from the left-join of t1 and t2. In the join, columns c0...cn-1 are matched for equality, and the last value of cn (most recent time) is taken. For each record in t1, the result has one record with the items in t1, and

  • if there are matching records in t2, the items of the last (in row order) matching record are appended to those of t1;
  • otherwise the remaining columns are null.

Current limitations

  • c is limited to one or two column names. The same limit applies to .gpu.bin.
  • The last (ordered) key column must be long, timestamp, timespan, int, month, date, minute, second, or time, and must have the same type in t1 and t2. The same limit applies to .gpu.bin.
  • Other key columns must have the same type in t1 and t2, and must be of a type supported by .gpu.group.
  • Enum key columns are matched by symbol, so their domains may differ between t1 and t2, as in q. A symbol that t2's domain does not contain finds no match, and its row gets nulls. The result keeps the key column of t1 (so its domain), except that aj0 and ajf0 take every key column from t2 in q but only the time column on the GPU: with different domains the enum key column of their result has the symbols of q's result but the domain of t1. A plain symbol column cannot be matched with an enum column (GPU_NOT_IMPLEMENTED).
q)t:([]time:2026.01.01D10:01:01 2026.01.01D10:01:03 2026.01.01D10:01:04;sym:`msft`ibm`ge;qty:100 200 150)
q)q:([]time:2026.01.01D10:01:00 2026.01.01D10:01:00 2026.01.01D10:01:00 2026.01.01D10:01:02;sym:`g#`ibm`msft`msft`ibm;px:100 99 101 98)
q)T:.gpu.to t
q)Q:.gpu.to q

Join on a single column - every row in t is matched against the most recent row overall in q:

q).gpu.aj[`time;T;Q]
time                          sym  qty px
------------------------------------------
2026.01.01D10:01:01.000000000 msft 100 101
2026.01.01D10:01:03.000000000 ibm  200 98
2026.01.01D10:01:04.000000000 ibm  150 98

Join on two columns - each row in t is matched against the most recent row in q with the same sym. For two-column joins, the last key column must be sorted ascending within each exact-match key in t2.

The exact-match column may have the parted (`p) or grouped (`g) attribute, or no attribute:

  • With `p, equal keys must occupy contiguous runs. .gpu.to builds an index from these runs without sorting or storing explicit row IDs.
  • With `g, .gpu.to builds a grouped index.
  • Without either attribute, each join builds and releases a temporary sorted index.

Both attributed indexes are retained with the GPU-resident column and reused by subsequent joins. To benefit from reuse, transfer t2 once and keep the resulting GPU-resident table.

q).gpu.aj[`sym`time;T;Q]
time                          sym  qty px
------------------------------------------
2026.01.01D10:01:01.000000000 msft 100 101
2026.01.01D10:01:03.000000000 ibm  200 98
2026.01.01D10:01:04.000000000 ge   150

The ge row has a null px because there is no ge in q.

To use a parted exact-match column, first arrange the RHS by key and time, then apply `p# before transferring it:

q)qp:update `p#sym from (`sym`time xasc q)
q)QP:.gpu.to qp
q).gpu.attr exec sym from QP
`p
q)(aj[`sym`time;t;qp])~.gpu.from .gpu.aj[`sym`time;T;QP]
1b

The same index is reused by subsequent calls using QP, including .gpu.aj0, .gpu.ajf, and .gpu.ajf0.

aj vs. aj0

aj and aj0 return different times in their results:

aj    boundary time from t1
aj0   actual time from t2
q).gpu.aj0[`sym`time;T;Q]
time                          sym  qty px
------------------------------------------
2026.01.01D10:01:00.000000000 msft 100 101
2026.01.01D10:01:02.000000000 ibm  200 98
2026.01.01D10:01:04.000000000 ge   150

ajf, ajf0

ajf and ajf0 fill from t1 if the corresponding value in t2 is null.

q)t0:([]time:2#2026.01.01D00:00:01;sym:`a`b;p:1 1;n:`r`s)
q)t1:([]time:2#2026.01.01D00:00:01;sym:`a`b;p:0 1)
q)t2:([]time:2#2026.01.01D00:00:00;sym:`g#`a`b;p:1 0N;n:`r`s)
q)T1:.gpu.to t1
q)T2:.gpu.to t2

With aj, the null p value for b in t2 is preserved in the result:

q).gpu.aj[`sym`time;T1;T2]
time                          sym p n
-------------------------------------
2026.01.01D00:00:01.000000000 a   1 r
2026.01.01D00:00:01.000000000 b     s

With ajf, null values in t2 are filled from t1:

q).gpu.ajf[`sym`time;T1;T2]
time                          sym p n
-------------------------------------
2026.01.01D00:00:01.000000000 a   1 r
2026.01.01D00:00:01.000000000 b   1 s
q)t0 ~ .gpu.from .gpu.ajf[`sym`time;T1;T2]
1b

ajf0 combines both: fills nulls from t1 and uses the actual time from t2:

q).gpu.ajf0[`sym`time;T1;T2]
time                          sym p n
-------------------------------------
2026.01.01D00:00:00.000000000 a   1 r
2026.01.01D00:00:00.000000000 b   1 s

append

Append two lists

.gpu.append[x;y]
.gpu.append[`name;y]

Where x and y are CPU or GPU-resident lists, dictionaries or tables, returns x joined to y.

Where x is a symbol atom, it names a global instead: y is appended to it and the symbol returned. Refer to appending by name.

Tensors

.gpu.append does not accept tensors and signals type. Join tensors with .gpu.tensor.concat.

q)A:.gpu.to a:til 5
q)B:.gpu.to b:a+10
q).gpu.append[A;b]
0 1 2 3 4 10 11 12 13 14

q)D:.gpu.to d:`a`b!(a;b)
q).gpu.append[D;d]
a| 0  1  2  3  4  0  1  2  3  4
b| 10 11 12 13 14 10 11 12 13 14

q)T:.gpu.to t:2#flip d
q)t
a b
----
0 10
1 11
q).gpu.append[T;t]
a b
----
0 10
1 11
0 10
1 11

Only the `g attribute is preserved on GPU-resident appends.

Extending a parted column clears its `p attribute and releases its parted index, including when the append extends the column in place.

.gpu.append falls back to CPU join given entirely CPU-resident arguments.

q).gpu.append[a;b]
0 1 2 3 4 10 11 12 13 14
q).gpu.append[a;b]
0 1 2 3 4 10 11 12 13 14

Appending by name

Naming the target lets .gpu.append extend it in place instead of allocating a new vector and copying both inputs: the name proves the binding holds the only reference. Repeated appends to a growing global avoid the allocation and copying that A:.gpu.append[A;y] repeats each time.

q)A:.gpu.to a:til 5
q).gpu.append[`A;5 6 7]
`A
q)A
0 1 2 3 4 5 6 7

q)T:.gpu.to t:([]a:1 2;b:10 20)
q).gpu.append[`T;([]a:enlist 3;b:enlist 30)]
`T
q)T
a b
----
1 10
2 20
3 30

Extending in place needs spare capacity, so it applies only under the `buddy allocation strategy. Refer to the alloc setting of .gpu.config.

.gpu.append copies when anything else refers to the target, as `t upsert y does.

Other holders keep the old value and only the name is rebound, so the result is the same either way; only the cost differs. .gpu.append decides per column, so sharing one column does not stop the others extending in place.

q)A:.gpu.to a:til 5
q)C:A                    / a second reference
q).gpu.append[`A;5 6 7]
`A
q)A
0 1 2 3 4 5 6 7
q)C            / unchanged
0 1 2 3 4

A failed append, for example on a type mismatch, leaves the global unchanged.

A symbol atom names a global, so appending a literal symbol needs it enlisted.

q)S:.gpu.to `a`b`c
q).gpu.append[enlist`abc;S]
`abc`a`b`c

asc

Ascending sort

.gpu.asc x

Where x is a GPU-resident vector, returns its items in ascending order of value, with the `s attribute set, indicating the list is sorted.

If x already has the `s attribute, .gpu.asc returns x unchanged without sorting it.

Sorting on GPU-resident symbols is not yet implemented; consider an enumeration instead.

The exception is a symbol vector that already has the `s attribute: .gpu.asc returns it without comparing any items.

q)A:.gpu.to a:7 4 7 1 0 5 9 9 2 9
q)a
7 4 7 1 0 5 9 9 2 9
q).gpu.attr .gpu.asc A
`s
q).gpu.asc A
`s#0 1 2 4 5 7 7 9 9 9

iasc

Ascending grade

.gpu.iasc x

Where x is a GPU-resident vector, returns the indices needed to sort x in ascending order.

If x already has the `s attribute, .gpu.iasc returns til count x without sorting.

q)A:.gpu.to a:7 4 7 1 0 5 9 9 2 9
q)a
7 4 7 1 0 5 9 9 2 9
q).gpu.iasc A
4 3 8 1 5 0 2 6 7 9
q)a .gpu.from .gpu.iasc A
0 1 2 4 5 7 7 9 9 9

idesc

Descending grade

.gpu.idesc x

Where x is a GPU-resident vector or table, returns the indices needed to sort x in descending order. As in q, the grade is stable: equal values keep their original order, and nulls sort last.

q)A:.gpu.to a:7 4 7 1 0 5 9 9 2 9
q)a
7 4 7 1 0 5 9 9 2 9
q).gpu.from .gpu.idesc A
6 7 9 0 2 5 1 8 3 4
q)a .gpu.from .gpu.idesc A
9 9 9 7 7 5 4 2 1 0

xasc

Sort a table in ascending order of specified columns

.gpu.xasc[x;y]

Where x is a symbol vector of column names defined in table y, returns y sorted in ascending order by x. The sort is by the first column specified, then by the second column within the first, and so on. All columns referenced in x must be GPU-resident.

If x names a single column that already has the `s attribute, .gpu.xasc returns y unchanged without sorting.

q)T:.gpu.to t:0N?([]sym:raze 2#/:`a`b`c; date:6#2025.01.01+til 2; val:50+6?10f)
q)t
sym date       val
-----------------------
c   2025.01.02 51.1578
b   2025.01.01 57.52659
b   2025.01.02 59.49526
a   2025.01.01 59.74317
c   2025.01.01 53.74586
a   2025.01.02 50.81106
q).gpu.xasc[`date;T]
sym date       val     
-----------------------
b   2025.01.01 57.52659
a   2025.01.01 59.74317
c   2025.01.01 53.74586
c   2025.01.02 51.1578 
b   2025.01.02 59.49526
a   2025.01.02 50.81106
q).gpu.xasc[`date`val;T]
sym date       val     
-----------------------
c   2025.01.01 53.74586
b   2025.01.01 57.52659
a   2025.01.01 59.74317
a   2025.01.02 50.81106
c   2025.01.02 51.1578 
b   2025.01.02 59.49526

q)/ Use a symbol enum to sort on symbols
q).gpu.xasc[`sym`date;T]
'GPU_SYMBOL_SORTING_NOT_IMPLEMENTED
q)syms:exec distinct sym from t
q)T:.gpu.to t:update `syms$sym from t
q).gpu.xasc[`sym`date;T]
sym date       val     
-----------------------
a   2025.01.01 59.74317
a   2025.01.02 50.81106
b   2025.01.01 57.52659
b   2025.01.02 59.49526
c   2025.01.01 53.74586
c   2025.01.02 51.1578

xdesc

Sort a table in descending order of specified columns

.gpu.xdesc[x;y]

Where x is a symbol vector of column names defined in table y, returns y sorted in descending order by x. The sort is by the first column specified, then by the second column within the first, and so on. Equal keys keep their original row order, and nulls sort last, as in q's xdesc. All columns referenced in x must be GPU-resident. The result does not carry `s.

q)T:.gpu.to t:([]sym:`a`b`a`c; date:2025.01.01 2025.01.02 2025.01.02 2025.01.01; val:1 2 3 4f)
q).gpu.from .gpu.xdesc[`date;T]
sym date       val
------------------
b   2025.01.02 2
a   2025.01.02 3
a   2025.01.01 1
c   2025.01.01 4

attr

Attributes of a GPU or CPU object

.gpu.attr x

Where x is any CPU or GPU-resident object, returns its attribute as a symbol atom.

The possible attributes are:

code attribute
`s sorted
`u unique
`p parted
`g grouped

A null symbol result ` means no attributes are set on x.

All attributes are preserved on .gpu.to, while all but `s are lost on .gpu.from.

Vanilla attr returns ` for GPU-resident virtual objects regardless of their true attributes.

q)G:.gpu.to g:`g#9 9 2 1 5 0 9 9 6 1
q)g
`g#9 9 2 1 5 0 9 9 6 1
q)attr g
`g
q)attr G        / vanilla attr insufficient on gpu virtual objects
`
q).gpu.attr G
`g
q).gpu.attr g   / .gpu.attr falls back to vanilla for CPU-resident objects
`g

q).gpu.attr .gpu.to til 10
`
q).gpu.attr .gpu.asc .gpu.to til 10
`s

q)attr .gpu.from .gpu.to `s#1 2 3
`s
q)attr .gpu.from .gpu.to `p#1 2 3
`               / only `s preserved on .gpu.from

bin

Binary search

.gpu.bin[x;y]

Vectors

Where:

  • x is a sorted GPU-resident vector.
  • y is a GPU-resident vector of the same type.

returns the index of the last item in x which is ≤ each y[i]. The result is -1 where y[i] is less than the first item of x.

Ordered-column types

.gpu.aj and .gpu.bin support long, timestamp, timespan, int, month, date, minute, second, and time ordered columns.

q)A:.gpu.to a:0 2 4 6 8 10
q)B:.gpu.to b:5 6 7
q).gpu.bin[A;B]
2 3 3

q)B:.gpu.to b:-10 0 4 5 6 20
q).gpu.bin[A;B]
-1 0 2 2 3 5

.gpu.bin uses a binary search algorithm, which is generally more efficient on large data. The items of x must be sorted ascending although .gpu.bin does not verify this property.

Tables

Where:

  • x is a GPU-resident table of n columns
  • y is a GPU-resident table with the same schema

returns the indices of the last row of x for which

  • the first n-1 columns each match the first n-1 columns of the corresponding row of y, and
  • the last column is not greater than the last column of the corresponding row of y.

If no items match the criteria, either because there are no rows that match in the first n-1 columns, or because the last value is smaller than the last value in the first such row, 0N is returned.

.gpu.aj and .gpu.bin are limited to one or two columns.

Indexes for two-column searches

.gpu.bin reuses the index built by .gpu.to when the exact-match column of x has the parted (`p) or grouped (`g) attribute. A parted index is built from contiguous key runs without sorting or storing explicit row IDs. Without a reusable index, each call builds and releases a temporary sorted index. Refer to as-of joins for attribute and ordering requirements.

Enum exact-match columns

An enum exact-match column is compared by symbol, so x and y may use different enum domains. The index of x is used as it is: the symbols of y are translated into the domain of x, and a symbol that the domain of x does not contain gives 0N.

q)A:.gpu.to a:([]a:`p#`p`p`p`q`q`q;b:0 2 4 0 2 4)
q)B:.gpu.to b:([]a:`q;b:-1 1 3 5)
q).gpu.bin[A;B]
0N 3 4 5

To use bin with a table, the last column need not be sorted overall, but it needs to be sorted within the equivalence classes defined by the first n-1 columns (as shown in the previous example).

cov

Covariance

.gpu.cov[x;y]

Where x and y are flat, same-length, GPU-resident float vectors on the currently selected device, returns a one-element GPU-resident float vector containing their population covariance.

Rows where either x or y is null are excluded as complete pairs. The covariance is calculated over the remaining valid pairs.

Inputs must be flat float vectors on the current device.

CPU-resident inputs, nested lists, matrices, and other q types are not supported. Both vectors must reside on the GPU currently selected by .gpu.setDev.

Note

The result is a one-element GPU-resident vector rather than a q float atom. Use first .gpu.from result to obtain the corresponding q atom.

q)x:1 2 3 4f
q)y:2 4 6 8f
q)X:.gpu.to x
q)Y:.gpu.to y

q)R:.gpu.cov[X;Y]
q)R
,2.5
q)R
,2.5
q)first .gpu.from R
2.5

Nulls and their corresponding pairs are excluded:

q)X:.gpu.to 1 2 0n 4f
q)Y:.gpu.to 2 4 6 8f
q)first .gpu.from .gpu.cov[X;Y]
3.111111

The result is null when there are no valid pairs. Empty inputs also return a one-element float-null vector.

q)first .gpu.from .gpu.cov[.gpu.to `float$();.gpu.to `float$()]
0n

Info

Floating-point results may differ slightly from CPU cov because the GPU reduction can combine values in a different order.

scov

Sample covariance

.gpu.scov[x;y]

Where x and y are flat, same-length, GPU-resident float vectors on the currently selected device, returns a one-element GPU-resident float vector containing their sample covariance.

For n valid pairs, sample covariance is calculated from population covariance as:

scov = cov × n ÷ (n - 1)

Rows where either input is null are excluded as complete pairs, and n is the number of remaining valid pairs.

The input and output restrictions are the same as for .gpu.cov.

q)X:.gpu.to 1 2 3 4f
q)Y:.gpu.to 2 4 6 8f
q)first .gpu.from .gpu.scov[X;Y]
3.333333

The correction factor uses the number of valid pairs:

q)X:.gpu.to 1 2 0n 4f
q)Y:.gpu.to 2 4 6 8f
q)first .gpu.from .gpu.scov[X;Y]
4.666667

Sample covariance is null when fewer than two valid pairs remain:

q)first .gpu.from .gpu.scov[.gpu.to enlist 3f;.gpu.to enlist 9f]
0n

q)first .gpu.from .gpu.scov[.gpu.to 0n 2 0n;.gpu.to 1 4 0n]
0n

Info

Floating-point results may differ slightly from CPU scov because the GPU reduction can combine values in a different order.

cor

Correlation coefficient

.gpu.cor[x;y]

Where x and y are flat, same-length, GPU-resident float vectors on the currently selected device, returns a one-element GPU-resident float vector containing their Pearson correlation coefficient.

Rows where either input is null are excluded as complete pairs. The input and output restrictions are the same as for .gpu.cov.

The result is between -1f and 1f for finite, non-constant inputs:

q)X:.gpu.to 1 2 3 4f
q)Y:.gpu.to 2 4 6 8f
q)first .gpu.from .gpu.cor[X;Y]
1f

q)Y:.gpu.to 8 6 4 2f
q)first .gpu.from .gpu.cor[X;Y]
-1f

Nulls and their corresponding pairs are excluded:

q)X:.gpu.to 1 2 0n 4f
q)Y:.gpu.to 2 4 6 8f
q)first .gpu.from .gpu.cor[X;Y]
1f

Correlation is null if either input is constant across the valid pairs, if fewer than two valid pairs remain, or if no valid pairs remain:

q)X:.gpu.to 3 3 3f
q)Y:.gpu.to 1 2 3f
q)first .gpu.from .gpu.cor[X;Y]
0n

Info

Floating-point results may differ slightly from CPU cor because the GPU reduction can combine values in a different order.

lj

Left join

.gpu.lj[left;right]

Where:

  • left is a CPU, GPU, or mixed-residency table
  • right is a CPU, GPU, or mixed-residency keyed table

returns a mixed-residency table equivalent to left lj right for supported inputs.

The result preserves columns from left. Columns copied only from left keep their original residency. Columns gathered from right keep the residency of the corresponding RHS columns. Therefore, a mixed-residency RHS table can produce a mixed-residency result. Use .gpu.from to materialize any GPU-resident result columns on the CPU.

q)left:([]id:1 2 3 4j; qty:10 20 30 40)
q)right:`id xkey ([]id:1 3j; px:100 300)
q).gpu.lj[.gpu.to left;.gpu.to right]
id qty px
----------
1  10  100
2  20
3  30  300
4  40

Multiple RHS key columns are supported.

q)left:([]sym:`a`a`b`b; id:1 2 1 2j; qty:10 20 30 40)
q)right:`sym`id xkey ([]sym:`a`b; id:2 1j; px:200 300)
q).gpu.lj[.gpu.to left;.gpu.to right]
sym id qty px
--------------
a   1  10
a   2  20  200
b   1  30  300
b   2  40

Supported key types are integral types, integer-backed temporal types, symbols, and enums with matching domains. int and long key columns may be joined with each other. RHS value columns must be simple, non-nested vectors supported by GPU gather.

.gpu.lj returns nyi for unsupported inputs. These include non-keyed RHS tables, RHS key columns missing from left, float-backed key types, datetime keys, nested result columns, and enum keys with different domains.

Index reuse

With a single key column, .gpu.lj probes the persistent index that .gpu.to builds when the RHS key column has the parted (`p), grouped (`g) or unique (`u) attribute, instead of building a hash table over every RHS row on each call. To benefit, transfer the keyed RHS once and keep the GPU-resident table. Joins with more than one key column, int/long key pairs, and RHS key columns without any of these attributes, or with no rows, build the hash table on each call. To compare the two paths, set the environment variable GPULIB_JOIN_INDEX to hash, which forces the hash table; other non-empty values are reported on stderr and ignored.

Warning

If right contains duplicate keys and .gpu.lj uses the persistent index, it returns the first matching RHS row, as q lj does. Otherwise it selects any matching RHS row, which may differ from the row q lj returns.

ij

Inner join

.gpu.ij[left;right]

Where:

  • left is a CPU, GPU, or mixed-residency table
  • right is a CPU, GPU, or mixed-residency keyed table

returns a mixed-residency table equivalent to left ij right for supported inputs.

Only rows from left whose key values match a row in right are included. The order of matching rows from left is preserved.

The result contains all columns from left, followed by columns from the RHS value table that do not already exist in left. When a non-key column exists on both sides, its result values come from right.

Result columns gathered from left have the same residency as their corresponding LHS columns. Columns gathered from right have the same residency as their corresponding RHS columns. Therefore, CPU, GPU, or mixed-residency inputs can produce a mixed-residency result. Use .gpu.from to materialize any GPU-resident result columns on the CPU.

q)left:([]id:1 2 3 4j; qty:10 20 30 40)
q)right:`id xkey ([]id:1 3j; px:100 300)
q).gpu.ij[.gpu.to left;.gpu.to right]
id qty px
----------
1  10  100
3  30  300

Multiple RHS key columns are supported.

q)left:([]sym:`a`a`b`b; id:1 2 1 2j; qty:10 20 30 40)
q)right:`sym`id xkey ([]sym:`a`b; id:2 1j; px:200 300)
q).gpu.ij[.gpu.to left;.gpu.to right]
sym id qty px
--------------
a   2  20  200
b   1  30  300

Supported key types are integral types, integer-backed temporal types, symbols, and enums with matching domains. int and long key columns may be joined with each other. Result columns must be simple, non-nested vectors supported by gather.

.gpu.ij returns nyi for unsupported inputs. These include non-keyed RHS tables, RHS key columns missing from left, float-backed key types, datetime keys, nested result columns, and enum keys with different domains.

Index reuse

With a single key column, .gpu.ij probes the persistent index that .gpu.to builds when the RHS key column has the parted (`p), grouped (`g) or unique (`u) attribute, instead of building a hash table over every RHS row on each call. To benefit, transfer the keyed RHS once and keep the GPU-resident table. Joins with more than one key column, int/long key pairs, and RHS key columns without any of these attributes, or with no rows, build the hash table on each call. To compare the two paths, set the environment variable GPULIB_JOIN_INDEX to hash, which forces the hash table; other non-empty values are reported on stderr and ignored.

Warning

If right contains duplicate keys and .gpu.ij uses the persistent index, it returns the first matching RHS row, as q ij does. Otherwise it selects any matching RHS row, which may differ from the row q ij returns.

wj

Window join with prevailing value

.gpu.wj[windows;commonColumns;left;rightAndAggregations]

.gpu.wj is the GPU implementation of q wj. Its arguments and window semantics match q wj for supported inputs.

The result preserves the rows and columns of left and appends one GPU-resident column for each aggregation. Use .gpu.from to materialize the result on the CPU.

q)trades:([]sym:`ibm`ibm;
    time:10:00:05 10:00:10;
    price:100 101f)

q)quotes:update `p#sym from
    ([]sym:5#`ibm;
      time:10:00:02 10:00:04 10:00:06 10:00:09 10:00:11;
      ask:101 102 103 104 105;
      bid:99 100 101 102 103)

q)windows:-2 1+\:trades`time

q).gpu.wj[
    .gpu.to windows;
    `sym`time;
    .gpu.to trades;
    (.gpu.to quotes;(max;`ask);(min;`bid))]
sym time     price ask bid
--------------------------
ibm 10:00:05 100   103 99
ibm 10:00:10 101   105 101

GPU-specific support and limitations:

  • windows may be CPU-resident, contain individually GPU-resident vectors (.gpu.to each windows), or be transferred as a nested GPU object (.gpu.to windows).
  • commonColumns contains zero or more equality-key columns followed by one temporal column. For a time-only join, use enlist `time.
  • Equality keys support boolean, byte, char, short, int, long, symbol, enum, timestamp, month, date, timespan, minute, second, and time. Corresponding LHS and RHS types must match exactly, including enum domains. Any number of equality keys is supported.
  • The temporal column supports timestamp, month, date, timespan, minute, second, and time.
  • Supported aggregations and RHS input types are:
Aggregations RHS input types
sum, min, max, avg boolean, byte, char, short, int, long, real, float, timestamp, month, date, timespan, minute, second, time
count, first, last the preceding types, plus GUID, symbol, and enum

Aggregation result types follow q.

The following columns must be GPU-resident:

  • on the LHS, the equality-key and temporal columns;
  • on the RHS, the equality-key, temporal, and aggregate columns.

Other LHS columns may be CPU- or GPU-resident and are returned unchanged. Nested aggregate columns and unlisted operations or input types are not supported.

Warning

When equality keys are supplied, the first RHS equality-key column must have the parted attribute (p#). RHS rows must be ordered by all equality keys in commonColumns order, followed by ascending temporal value within each complete key tuple.

For a time-only join, the RHS must be globally ordered by ascending temporal value. .gpu.wj does not sort the RHS or validate temporal ordering.

Index reuse

With a single equality key, .gpu.wj and .gpu.wj1 reuse the parted index that .gpu.to builds for the RHS key column, instead of grouping the RHS on every call. To benefit, transfer the RHS once and keep the GPU-resident table:

q)Q:.gpu.to quotes
q).gpu.from .gpu.wj[windows;`sym`time;.gpu.to trades;(Q;(max;`ask);(min;`bid))]

Joins with more than one equality key group the RHS on every call. Appending to the RHS with .gpu.append clears its `p attribute and index, after which .gpu.wj rejects the table until `p# is reapplied on the CPU and the RHS is transferred again.

To compare the two paths, set the environment variable GPULIB_WJ_GROUPING to sort, which groups the RHS on every call even when the index is available; other non-empty values are reported on stderr and ignored.

wj1

Window join without prevailing value

.gpu.wj1[windows;commonColumns;left;rightAndAggregations]

.gpu.wj1 is the GPU implementation of q wj1. It has the same GPU-specific support, residency requirements, and ordering requirements as .gpu.wj.

Note

Unlike q wj1, .gpu.wj1 treats an inverted window as an empty range instead of raising a domain error.

compact

Reclaim the capacity reserved by GPU-resident vectors

.gpu.compact x

Where x is a GPU-resident vector, dictionary, table, keyed table, or any list of them, reallocates the data of every GPU-resident vector it holds to exactly the bytes that vector uses, and returns x. Anything inside x that is not GPU-resident is left as it is, so you can compact a mixed-residency table.

x itself must be GPU-resident or a shape that can hold something that is, otherwise .gpu.compact signals type. A tensor is accepted and left as it is, alone or inside a list or dictionary: it holds no reserved capacity, and its storage is shared with its views. .gpu.compact does not accept a symbol atom as the name of a global, as .gpu.append does. It updates the object in place, so it does not need the name.

.gpu.compact reallocates data that other references can still point to, so it runs only on the main thread. From any other thread it signals `noupdate, as q does for a global amend from a secondary thread. This includes calls inside peach, and client queries in a process started with a multi-threaded input queue (a negative -s), which do not run on the main thread.

Under the default `buddy allocation strategy, a vector reserves up to twice the memory its data needs, so that .gpu.append can extend it in place. When a table stops growing, call .gpu.compact to release the reserved memory. In the following example, each column needs 3 MiB plus 8 bytes, so `buddy reserves 4 MiB per column. After compaction, the two columns use 6,291,472 bytes.

q).gpu.config ([alloc:`buddy])
q)n:1+3*1024*1024 div 8   / 3MB + 1 element per column
q)T:.gpu.to ([]a:n?0j; b:n?0j)

q).gpu.sync[]; .gpu.memDev[]`used
8388608
q).gpu.compact T;
q).gpu.sync[]; .gpu.memDev[]`used
6291472

Only column data is compacted.

Index allocations associated with grouped (`g), parted (`p) and unique (`u) attributes, and the offsets behind nested columns, retain their allocated capacity.

.gpu.compact allocates the new copy before it releases the original, so compacting a column needs free device memory equal to the size of that column. If that memory is not available, .gpu.compact copies through host memory instead. This path is slower, releases the original first, and counts against the -w memory limit of the process.

config

Get or set the runtime configuration

.gpu.config[]
.gpu.config d

Where d is a symbol-keyed dictionary of settings to apply, returns nothing. Called with no argument, returns every setting as a dictionary.

q).gpu.config[]
memRelThres| 4294967296
alloc      | `buddy
q).gpu.config ([memRelThres:20*1024*1024*1024;alloc:`tight])
q).gpu.config[]
memRelThres| 21474836480
alloc      | `tight

Applying a dictionary is all-or-nothing: every key and value is validated first, so a setting that is not valid leaves the configuration unchanged. An unrecognized key, or an out-of-range value for a recognized key, signals `domain. A value of the wrong type signals `type.

q).gpu.config ([alloc:`asdf])
'domain
q).gpu.config ([asdf:`buddy])
'domain

Settings

Setting Type Default Scope Description
memRelThres long 4294967296 device Memory release threshold in bytes.
alloc symbol `buddy global Allocation strategy for GPU-resident vectors.

memRelThres is a property of the memory pool of the active device, so it is read and written for whichever device .gpu.setDev has selected, and each device carries its own value. When the pool has reserved more than memRelThres bytes, it releases unused memory back down to the threshold at synchronization points, such as .gpu.sync or .gpu.from. It does not limit how much memory can be allocated. The peak field of .gpu.memDev is the peak reserved memory, and can be used for sizing.

Setting memRelThres can be a significant performance win and is recommended.

You can set a global-scope setting only from the main thread. From any other thread, .gpu.config signals `noupdate, as .gpu.compact does. You can read settings from any thread.

alloc is global and controls how much memory a GPU-resident vector reserves:

  • `buddy rounds each allocation up to a power of two, as KDB-X does on the host. The spare capacity lets .gpu.append extend a vector in place.
  • `tight allocates exactly the bytes the data needs, which can save up to half the VRAM of a column. .gpu.append can never extend in place, so every append allocates a new vector and copies both inputs into it.

`buddy is necessary but not sufficient for an in-place append.

The target must also be provably unshared. Refer to appending by name.

Use `tight when a dataset has to fit in VRAM and is not being appended to; keep the default `buddy for workloads that append. Changing alloc affects allocations made from that point on; vectors already on the device keep the capacity they were created with.

q).gpu.config ([memRelThres:0])
q)n:1+3*1024*1024 div 8   / 3MB + 1 element
q)used:{.gpu.sync[]; b:.gpu.memDev[]`used; c:.gpu.to x?0j; .gpu.sync[]; .gpu.memDev[][`used]-b}

q).gpu.config ([alloc:`buddy])
q)used n
4194304

q).gpu.config ([alloc:`tight])
q)used n
3145736

count

Count of a CPU or GPU-resident object

.gpu.count x

Where x is:

  • a vector, returns the number of elements
  • a dictionary, returns the number of keys
  • a table, returns the number of rows
  • anything else, 1

Vanilla count returns 1 for GPU-resident virtual objects regardless of their true size.

q)A:.gpu.to a:til 5
q)D:.gpu.to d:`a`b!(a;a+10)
q)T:.gpu.to t:flip d

q).gpu.count each (a;d;t)
5 2 5
q).gpu.count each (A;D;T)
5 2 5

q)count each (a;d;t)
5 2 5
q)count each (A;D;T)
1 1 1

drop

Drop specified count, keys or columns

.gpu.drop[x;y]

Where x is a symbol atom or vector, returns a dictionary/table y without the specified keys/columns.

Note

Unlike the standard drop, .gpu.drop does not support dropping by count; it only supports dropping by key or column name.

q)T:.gpu.to t:([]a:`x`y`z;b:0 2 4;c:1 2 3)
q)T
a b c
-----
x 0 1
y 2 2
z 4 3
q).gpu.drop[`b] T
a c
---
x 1
y 2
z 3
q).gpu.drop[`b] t
a c
---
x 1
y 2
z 3
q).gpu.drop[`a`b] t
c
-
1
2
3
q).gpu.drop[`a`b] T
c
-
1
2
3

from

Retrieve data from the GPU

.gpu.from x

Where x is any CPU, GPU or mixed-resident object. Returns x moved to the CPU. A tensor, alone or inside a list or dictionary, comes back as .gpu.tensor.from returns it; like .gpu.tensor.from, a tensor above rank 2 signals nyi.

All attributes are preserved on .gpu.to, while all but `s are lost on .gpu.from.

q)A:.gpu.to a:til 5
q)D:.gpu.to d:`a`b!(a;a+10)
q)T:.gpu.to t:flip d

q).gpu.from A
0 1 2 3 4
q).gpu.from a
0 1 2 3 4
q).gpu.from D
a| 0  1  2  3  4
b| 10 11 12 13 14
q).gpu.from T
a b
----
0 10
1 11
2 12
3 13
4 14

gather

Gather specified indices of a list

.gpu.gather[x;y]

Where:

  • x is a GPU-resident vector
  • y is a short, int, or long vector of indices into x, either CPU-resident or GPU-resident.

returns the elements of x at each index y.

q)A:.gpu.to a:7 4 7 1 0 5 9 9 2 9
q)a
7 4 7 1 0 5 9 9 2 9
q)B:.gpu.to b:2 4 6 8
q)a b
7 0 9 2
q).gpu.gather[A;B]
7 0 9 2

q).gpu.asc[A]
`s#0 1 2 4 5 7 7 9 9 9
q).gpu.gather[A] .gpu.iasc A
0 1 2 4 5 7 7 9 9 9

Null, negative, and out-of-range indices produce the source type's q null value in the output (including NaN for real, float, and datetime, a space for char, and the empty symbol for symbol). Boolean and byte vectors use zero, as in native q indexing.

q)A:.gpu.to a:1 2 3
q).gpu.gather[A;.gpu.to 1 2 0N]
2 3 0N

getDev

Get current GPU

.gpu.getDev x

Where x is:

  • null, returns the currently selected device index
  • a GPU-resident object, returns the device index on which it resides
q).gpu.setDev 0
q).gpu.getDev[]
0
q)A:.gpu.to 1 2 3
q).gpu.setDev 2
q).gpu.getDev[]
2
q).gpu.getDev A
0

getMemRelThres

Get the memory release threshold

.gpu.getMemRelThres[]

Deprecated: use .gpu.config instead.

.gpu.getMemRelThres[] is equivalent to .gpu.config[]`memRelThres.

Get the memory release threshold for the GPU memory pool of the active device. Refer to the memRelThres setting of .gpu.config for what the threshold does.

q).gpu.getMemRelThres[]
4294967296
q).gpu.setMemRelThres[1024]
q).gpu.getMemRelThres[]
1024
q).gpu.setDev 2
q).gpu.getMemRelThres[]
4294967296

group

Apply GPU grouped attribute

.gpu.group x

Where x is a GPU-resident vector, returns x with the `g attribute set.

If x has the parted (`p) attribute, .gpu.group replaces its parted index with a grouped index and changes the attribute to `g. The same applies to columns passed to .gpu.xgroup.

.gpu.aj and .gpu.bin order on long, timestamp, timespan, int, month, date, minute, second or time columns.

All attributes are preserved on .gpu.to, while all but `s are lost on .gpu.from.

q)A:.gpu.to a:7 4 7 1 0 5 9 9 2 9
q)a
7 4 7 1 0 5 9 9 2 9
q)G:.gpu.group A
q).gpu.attr G
`g
q).gpu.from G
7 4 7 1 0 5 9 9 2 9     / `g not preserved on .gpu.from

xgroup

Apply GPU grouped attribute to specific columns/keys

.gpu.xgroup[x;y]

Where x is a symbol atom/list specifying GPU-resident keys/columns of a dictionary/table y, returns y with specified columns/keys grouped.

q)T:.gpu.to t:flip `a`b!(1 2 3 4;10 20 30 40)
q).gpu.meta T
c| t f a r
-| ---------
a| j     gpu
b| j     gpu
q).gpu.meta .gpu.xgroup[`a] T
c| t f a r
-| ---------
a| j   g gpu
b| j     gpu

ungroup

Flatten a table's list-valued columns into one row per element

.gpu.ungroup t

Where t is a GPU-resident table, returns t with every row's list-valued columns exploded into one row per element; atom-valued columns are repeated once per exploded row. Matches native q's ungroup: every list-valued column in a row must have the same count (else `length), a row whose list columns have count 0 vanishes from the result, and a table with no list-valued column at all raises `rank.

q)t:([] a:1 2 3; b:(1 2;0#0N;3 4 5))
q)ungroup t
a b
---
1 1
1 2
3 3
3 4
3 5
q).gpu.ungroup .gpu.to t
a b
---
1 1
1 2
3 3
3 4
3 5
q)t2:([] x:1.5 2.5 3.5 4.5 5.5 6.5 7.5 8.5 9.5 10.5; y:0 0 0 0 0 1 1 1 1 1)
q)ungroup select a:sums x by y from t2
y a   
------
0 1.5 
0 4   
0 7.5 
0 12  
0 17.5
1 6.5 
1 14  
1 22.5
1 32  
1 42.5
q).gpu.ungroup .gpu.select[.gpu.to t2;();enlist[`y]!enlist`y;enlist[`a]!enlist(sums;`x)]
y a   
------
0 1.5 
0 4   
0 7.5 
0 12  
0 17.5
1 6.5 
1 14  
1 22.5
1 32  
1 42.5

memDev

Get memory info for current GPU

.gpu.memDev[]

Returns memory utilization for the currently selected device in bytes, similar to .Q.w[].

q).gpu.memDev[]
used| 512
heap| 33554432
peak| 33554432
mphy| 23661248512
free| 23426301952

q)a:.gpu.memDev[]
q).gpu.memDev[]-a
used| 0
heap| 0
peak| 0
mphy| 0
free| 0
q)A:.gpu.to til 1000
q).gpu.memDev[]-a
used| 8192
heap| 0
peak| 0
mphy| 0
free| 0
q).gpu.setDev 1
q).gpu.memDev[]
used| 0
heap| 0
peak| 0
mphy| 23661248512
free| 23459856384

meta

Metadata of a table, including GPU-residency info

.gpu.meta x

Where x is a CPU, GPU or mixed-residency table, returns the metadata including a residency column. q's own meta returns the same result for a GPU or mixed-residency table.

q)syms:-10?`3
q)q:`time xasc([]time:2026.07.10D12:32+5?00:00:00;sym:5?syms;px:95+5?10)
q).gpu.meta q
c   | t f a r  
----| ---------
time| p   s cpu
sym | s     cpu
px  | j     cpu
q).gpu.meta .gpu.to q
c   | t f a r  
----| ---------
time| p   s gpu
sym | s     gpu
px  | j     gpu
q).gpu.meta .gpu.xto[`time`sym] q
c   | t f a r  
----| ---------
time| p   s gpu
sym | s     gpu
px  | j     cpu

q)/ q's meta gives the same result on a GPU table
q)meta .gpu.xto[`time`sym] q
c   | t f a r  
----| ---------
time| p   s gpu
sym | s     gpu
px  | j     cpu

cntDev

Count of available GPUs

.gpu.cntDev[]

Returns the number of available GPUs. A process may be limited by setting CUDA_VISIBLE_DEVICES in its environment.

q).gpu.cntDev[]
4
q){.gpu.setDev x;.gpu.getDev[]} peach til 4
0 1 2 3

q) / in another process limited to devices 1 and 3
q)getenv `CUDA_VISIBLE_DEVICES
"1,3"
q).gpu.cntDev[]
2
q){.gpu.setDev x;.gpu.memDev[]} peach til .gpu.cntDev[]
used heap peak mphy        free
--------------------------------------
0    0    0    23661248512 23459856384
0    0    0    23661248512 23459856384

nvtx

Use the NVIDIA NVTX tool to record ranges that can be viewed in Nsight Systems.

end

Mark the end of a record range

.gpu.nvtx.end[range_id]

Mark the end of the range with the id range_id. Refer to .gpu.nvtx.start.

range_id:.gpu.nvtx.start["big select statement"]
.gpu.from .gpu.select[...]
.gpu.nvtx.end[range_id]

start

Mark the beginning of a record range

range_id:.gpu.nvtx.start[desc_str]

Mark the beginning of the range with a descriptive string. .gpu.nvtx.start returns a long id value that should be used with .gpu.nvtx.end to stop recording. There are a few ways to use NVTX ranges with Nsight Systems that can be helpful for finding bottlenecks.

range_id:.gpu.nvtx.start["big select statement"]
.gpu.from .gpu.select[...]
.gpu.nvtx.end[range_id]

profiler

Access to the CUDA profiler control API

Use it to selectively profile a specific section of your application.

start

Calls cudaProfilerStart

.gpu.profiler.start[]

stop

Calls cudaProfilerStop

.gpu.profiler.stop[]

setDev

Set current GPU

.gpu.setDev x

Where x is a long from 0 to .gpu.cntDev[]-1, selects the device with that index.

.gpu.setDev takes an index into the available device list. For a process with CUDA_VISIBLE_DEVICES='3,1', .gpu.setDev 0 selects the device with system ID 3.

q)getenv `CUDA_VISIBLE_DEVICES
"3,1"
q).gpu.getDev[]
0
q)A:.gpu.to til 1000
q).gpu.setDev 1
q).gpu.getDev[]
1
q)B:.gpu.to til 1000
q).gpu.getDev each (A;B)
0 1

.gpu.setDev selects the device for the calling host thread, and it stays selected there until the next setDev.

A branch of peach runs on a pool thread that q reuses, so the branch's device stays on that thread after the branch returns, and a nested peach branch starts on whatever device its own thread last used. Set the device in the branch that does the work, never rely on it carrying over to a later branch, and use .gpu.onDev to scope a device to one call.

onDev

Run a call with one device current, restoring the previous one

.gpu.onDev[d;f]

Where d is a long from 0 to .gpu.cntDev[]-1 and f is a function that takes one argument, makes device d current, calls f[], and restores the device that was current before, whether f returns or signals. Returns f's result.

.gpu.setDev is per host thread (see the warning there), so a peach branch that sets a device leaves its pool thread on it. .gpu.onDev scopes the device to the call: pool threads are never left on another device, and an error in the branch cannot leave one there either.

q).gpu.getDev[]
0
q).gpu.onDev[1;{.gpu.getDev .gpu.to til 1000}]
1
q).gpu.getDev[]
0

f is called as f[], so its one argument arrives as ::. A lambda without an argument list, such as {.gpu.to til 1000}, takes the one argument x: x is :: there, so .gpu.onDev[d;{.gpu.to x}] uploads :: rather than signaling. Pass a branch's own values to f by projecting them, as q has no closures: .gpu.onDev[d;{[t;x] ...}[t]] leaves x to arrive as ::. This is also how to create data on a chosen device without moving the calling thread's, for example in a nested peach branch that works on its outer branch's device: .gpu.onDev[d;{[t;x] .gpu.to t}[t]].

f can be a lambda, a projection, a composition (which takes its rightmost function's arguments) or another .gpu function, as long as it leaves exactly one argument open, or the symbol naming one. .gpu.onDev checks f before it changes the device, so a refused f never runs and the device is unchanged:

  • 'type: f is not a function: a list, dictionary, atom, string or file handle, a q operator or primitive such as +, neg or ::, or an iterator-derived function such as +/ or g' (call it from a lambda instead), or a symbol that names none of these. An undefined symbol signals its own name.
  • 'rank: f leaves more than one argument open, such as {[t;x] ...} without its projection or {[a;b;c] ...}[;1;]. Without this check, f[] would return a projection without running f.

q shows an elided argument as ::, so an argument projected as :: also counts as open: {[a;b;c] ...}[::;1] is refused although only c is open. Make the call inside a lambda instead: {g[::;1;3]}.

A .gpu.setDev inside f holds until f returns, and .gpu.onDev then restores the device that was current at the call. .gpu.onDev calls nest: an inner call restores the outer call's device. An invalid d signals before f runs.

select

qSQL and functional select and exec on a GPU table

select ... from T ...
exec ... from T ...
?[T;c;b;a]
.gpu.select[T;c;b;a]

A GPU table works with q's own qSQL select and exec (without by), and with the functional form ?[t;c;b;a]: the query runs on the GPU and returns a GPU table, which you can query again, join or sort without copying it back. Displaying a result at the console copies only the rows shown; use .gpu.from to bring the whole result to the CPU.

qSQL

q)t:([]sym:`AAPL`MSFT`AAPL`MSFT`AAPL;p:150 300 152 298 155;size:100 200 150 300 250)
q)T:.gpu.to t

q)select from T where sym=`AAPL,p>151
sym  p   size
-------------
AAPL 152 150
AAPL 155 250

q)select p_min:min p,p_max:max p,p_avg:avg p by sym from T
sym | p_min p_max p_avg
----| --------------------
AAPL| 150   155   152.3333
MSFT| 298   300   299

q)select vwap:size wavg p by sym from T
sym | vwap
----| -----
AAPL| 153.1
MSFT| 298.8

q)select notional:p*size,lp:log p from T
notional lp
-----------------
15000    5.010635
60000    5.703782
22800    5.023881
89400    5.697093
38750    5.043425

q)select sum size by sym from T where p>151
sym | size
----| ----
AAPL| 400
MSFT| 500

q)/ fby, and membership in a literal or a global list
q)select from T where p>(avg;p) fby sym
sym  p   size
-------------
MSFT 300 200
AAPL 155 250
q)syms:`MSFT`GOOG
q)select from T where sym in syms
sym  p   size
-------------
MSFT 300 200
MSFT 298 300

q)/ the first or last n rows, optionally after an ascending sort
q)select[2;<p] from T
sym  p   size
-------------
AAPL 150 100
AAPL 152 150
q)select[-2] from T
sym  p   size
-------------
MSFT 298 300
AAPL 155 250

q)/ a result is a GPU table: query it again without copying it back
q)select from (select from T where p>151) where sym=`AAPL
sym  p   size
-------------
AAPL 152 150
AAPL 155 250

q)/ exec returns a GPU vector, or a dictionary of them
q)exec p from T
150 300 152 298 155

Use the functional form for what qSQL cannot express on a GPU table:

Not supported on a GPU table Instead
update ... from T (signals type) .gpu.update
delete ... from T (signals type) .gpu.drop for columns, a select with the opposite where for rows
select[>c], a descending sort (signals nyi) .gpu.from the rows and sort on the CPU
a lambda's local or argument in a query, as in {select from T where p>x} build the query with its value: {?[T;enlist(>;`p;x);0b;()]}
select distinct, exec ... by, exec k!v .gpu.from the columns and finish on the CPU

Functional form

.gpu.select mirrors the functional query operator ?[t;c;b;a], and ?[T;c;b;a] on a GPU table calls it.

.gpu.select[t;c;b;a]

Where:

  • t is a table with all columns on device.
  • c is the where phrase, a list of constraints.
  • b is the by phrase.
  • a is the select or exec phrase.

All columns of t must be resident on the GPU.

Expression names and membership

Unquoted expression names resolve to a table column first, then to a q variable. A qSQL select on a GPU table uses the same interpreter rules. A column therefore takes precedence over a global of the same name; the special row-index name i retains its existing meaning. Variables can be globals, current-namespace names, or qualified names such as .filters.assets. Unknown names error; existing selector and parameter validation errors are retained. Caller-local lambda arguments are not captured.

Variable values must be supported primitive atoms or flat homogeneous lists. A symbol read from a variable is data, not another name to resolve. Quoted symbols remain literal values. Variables are read again for each query, and projected variable lists own their result storage.

in accepts literal sets, atom/list variables, columns, and expressions composed of supported interpreter operations. For example:

q).filters.assets:`a`b
q)T:.gpu.to ([]y:`a`b`c;a:`c`a`b)
q)/ The RHS is the table column, not a global named a.
q).gpu.select[T;enlist(in;`y;`a);0b;()]
q)/ A qualified variable supplies an independent membership set.
q).gpu.select[T;enlist(in;`y;`.filters.assets);0b;()]
q)/ A computed device set is also accepted.
q)N:.gpu.to ([]x:1 2 3;a:0 1 2)
q).gpu.select[N;enlist(in;`x;(+;`a;1));0b;()]

An atom is a singleton membership set; an empty set matches nothing. Device-produced sets are searched without modifying their source data. Later where clauses see surviving table rows, but external variable lists are not filtered. Grouped membership searches column-derived sets within each group and external sets in full. As in q, aggregates over external vectors instead partition those vectors by the query groups and require a matching row count. Vector arithmetic checks lengths; a singleton vector is not an atom and does not broadcast.

Mixed-type membership follows q by the length of the set and where the in is

When the set's type differs from the column's, q's rule depends on how many elements the set has and, for an empty set, on where the in is written. in follows it. Anything q accepts that the GPU cannot reproduce exactly is refused with GPU_NOT_IMPLEMENTED (never answered differently), and what q rejects with type is GPU_INVALID_STATEMENT.

  • An atom or a one-element set is compared as = across numeric, temporal, character and boolean types (1 2 3h in enlist 1i). q rejects some temporal pairs with type: month against minute, second, time, timespan or datetime, and date against minute, second, time or timespan, in either order. So does in. A long column against an int-backed temporal atom (month, date, minute, second, time), or the reverse, narrows the long to int with saturation first (0Wd matches a long of 2147483648 or more); against a plain int it is exact.
  • Refused: the pairings q accepts for which the GPU has no comparison (timestamp with month, timestamp with timespan, month with date), date with datetime (the conversions differ from q's at nulls and infinities), and anything where q's float tolerance (3.0000000000000004 = 3) could change the answer: a float, real or datetime column against another type, or a float atom other than an integer of magnitude up to 1e9, a null or an infinity.
  • An empty set of another type matches nothing only when the in is the whole of the first where clause and its left side is a bare table column. An expression, i or a variable on the left, a later clause, a clause that merely contains it, or a select, exec, by or update expression, gives type and so does in. The int/long pair is accepted everywhere.
  • A set of two or more elements is accepted only for int and long. An int column against a long set compares exactly. A long column against an int set narrows each column value to int first, saturating at the int limits (2147483648 and 0W match 0Wi, -0W matches -0Wi, a long null matches 0Ni) and compares as int. Every other pairing is a q type error.
  • Attributed columns (g#, s#, u#, p#): q searches the column's index for an atom or a one-element set, and for a p# column in the first where clause for a longer set too, which narrows the set to the column's type. The GPU does not reproduce that search, so these are refused with GPU_NOT_IMPLEMENTED: an atom of another type over any attribute; a one-element set of another type over p# or s#; a set of two or more of another type over p# in the first where clause; and, over s#, a mixed set that a plain column would reject with type (q's binary search accepts it). A one-element set over g# or u#, and a longer set over g# or u#, is compared as on a plain column. Outside the first where clause, p# behaves like a plain column.
  • A set whose length is not known when the query is planned (a column or a computed set) is accepted for an int column against a long set; any other mismatched type gives GPU_NOT_IMPLEMENTED, because the result depends on the length.

Host literals and directly resolved host lists use sorted membership-specific copies; computed sets use GPU radix sorting. Eligible first-clause filters on a g# column also reuse its index for variable and computed RHS sets. Newly eligible floating-point cases retain the ordinary membership path to preserve NaN equality. These optimizations never reorder the source data.

Existing operation, mixed-type membership, and enum-domain restrictions still apply. This does not enable arbitrary q functions or nested-list variable data.

Run parse on a qSQL statement to see how to represent it as a functional select

q)parse "select p_min:min p, p_max:max p, p_avg:avg p by sym from t"
?
`t
()
(,`sym)!,`sym
`p_min`p_max`p_avg!((min;`p);(max;`p);(avg;`p))
q)t:([]sym:`AAPL`MSFT`AAPL`MSFT`AAPL;p:150 300 152 298 155;size:100 200 150 300 250)
q)T:.gpu.to t

q)/ select all columns
q).gpu.select[T;();0b;()]
sym  p   size
-------------
AAPL 150 100
MSFT 300 200
AAPL 152 150
MSFT 298 300
AAPL 155 250

q)/ select specific columns
q).gpu.select[T;();0b;`sym`p!(`sym;`p)]
sym  p
--------
AAPL 150
MSFT 300
AAPL 152
MSFT 298
AAPL 155

q)/ where sym = `AAPL - enlist the symbol to distinguish it from a column name
q).gpu.select[T;enlist(=;`sym;enlist`AAPL);0b;()]
sym  p   size
-------------
AAPL 150 100
AAPL 152 150
AAPL 155 250

q)/ multiple where conditions are ANDed
q).gpu.select[T;((=;`sym;enlist`AAPL);(>;`p;151));0b;()]
sym  p   size
-------------
AAPL 152 150
AAPL 155 250

q)/ aggregation by group
q).gpu.select[T;();enlist[`sym]!enlist`sym;`p_min`p_max`p_avg!((min;`p);(max;`p);(avg;`p))]
sym  p_min p_max p_avg
-------------------------
AAPL 150   155   152.3333
MSFT 298   300   299

q)/ computed column: avg price * size by sym
q).gpu.select[T;();enlist[`sym]!enlist`sym;enlist[`avgpxsz]!enlist(avg;(*;`p;`size))]
sym  avgpxsz
-------------
AAPL 25516.67
MSFT 74700

q)/ unary ops in the select phrase
q).gpu.select[T;();0b;enlist[`logp]!enlist(log;`p)]
logp
--------
5.010635
5.703782
5.023881
5.697093
5.043425

Select or exec

As in native q, the ? operator runs either a select or an exec, depending on the arguments provided for b and a:

b a Behavior Result
0b Dictionary or () select Table
Dictionary Dictionary or () select with by Keyed table
() or `symbol$() Single expression exec Vector, or an atom if the expression aggregates
() or `symbol$() Dictionary exec Dictionary of name → vector/atom

A "single expression" is anything that is not a dictionary. For example, it can be a symbol atom naming a column, or a parse-tree expression such as (sum;`p).

For ungrouped exec queries, vector results remain on the GPU. Scalar results are returned as q atoms on the CPU.

q)t:([]sym:`AAPL`MSFT`AAPL`MSFT`AAPL;p:150 300 152 298 155;size:100 200 150 300 250)
q)T:.gpu.to t

q)/ return one column
q).gpu.select[T;();();`p]
150 300 152 298 155

q)/ return a scalar aggregate
q).gpu.select[T;();();(sum;`size)]
1000

q)/ a dictionary by with a dictionary phrase is a select, so this returns the same
q)/ keyed table as `select minp:min p, maxp:max p by sym from t`
q).gpu.select[T; (); (enlist `sym)!enlist `sym; `minp`maxp!((min;`p);(max;`p))]
sym | minp maxp
----| ---------
AAPL| 150  155
MSFT| 298  300

Current limitations

  • Grouped exec is not supported. This includes a dictionary for b with a single expression for a.

  • A symbol atom for b (?[T;();`a;…]) is not supported, on keyed and plain tables alike. Use a dictionary: enlist[`a]!enlist`a.

  • Using 1b for b (select distinct) is not supported.

  • An empty exec phrase (()) with () for b (q's last-row forms) is not supported.
  • An empty by dictionary (()!()) is supported only with a dictionary phrase, where, as in q, it makes one group of every surviving row: a non-aggregate column is returned as a single nested cell, and a table or where clause with no surviving rows gives an empty result. The exec and empty-phrase forms are not supported.

  • Non-empty symbol-vector by forms, such as enlist `sym or `sym`date, are not supported. Use a dictionary for named or multiple grouping columns.

Where clause

  • c is a list of parse-tree expressions, each evaluating to a boolean vector.
  • Expressions can use all supported operations.
  • () (empty list) means no constraints.
  • Multiple conditions are applied sequentially.

ANDing clauses together may lead to better performance when individual clauses are not highly selective.

For enumerated columns, host symbol membership sets are resolved against the column's domain. Already-enumerated sets must use the same domain; cross-domain membership is not supported.

When comparing to symbol atoms, enlist distinguishes a value from a column name. For example, use (=;`sym;enlist`assetA).

By clause

  • b is a dictionary mapping output key names to input column names: enlist[`sym]!enlist`sym.
  • 0b or () means no grouping. Use 0b for select semantics and () for exec semantics.

The output is usually sorted by the key columns; this is not true for symbol key columns. Try using an enumeration instead.

Select clause

  • a is a dictionary mapping output column names to input columns or parse-tree expressions.
  • () selects all columns.
  • Bare symbols in dictionary values resolve to columns first, then q variables. To reference a literal symbol, enlist it.

fby

fby aggregates one column over the groups of another and gives every row its own group's result, so the output is as long as the table.

The aggregate and the column it applies to are paired with enlist, mirroring qSQL's (agg;col):

q)/ these are equivalent
q)select from t where y > (avg;y) fby x
q)?[t;enlist(>;`y;(fby;(enlist;avg;`y);`x));0b;()!()]

A multi-column group key is a table, so it takes the usual flipped-dictionary form — (+:;(!;enlist`x`z;(enlist;`x;`z))) is ([]x;z).

q)t:([]sym:`AAPL`MSFT`AAPL`MSFT`AAPL;p:150 300 152 298 155;size:100 200 150 300 250)
q)T:.gpu.to t

q)/ each row gets its own sym's average price
q).gpu.select[T;();0b;enlist[`p_avg]!enlist(fby;(enlist;avg;`p);`sym)]
p_avg
--------
152.3333
299
152.3333
299
152.3333

q)/ grouped by two keys, the second computed: ([]sym;size>150)
q).gpu.select[T;();0b;enlist[`p_max]!enlist(fby;(enlist;max;`p);(+:;(!;enlist`sym`big;(enlist;`sym;(>;`size;150)))))]
p_max
-----
152
300
152
300
155

q)/ the canonical use - filter rows against their own group's aggregate
q).gpu.select[T;enlist(>;`p;(fby;(enlist;avg;`p);`sym));0b;()]
sym  p   size
-------------
MSFT 300 200
AAPL 155 250

Let q build the phrase

fby trees are wordy, so it is often easier to parse a qSQL statement and take the phrase you want — the select phrase is the last element, and the where phrase is element 2 (doubly nested, so it needs a first):

q)last parse "select p_avg:(avg;p) fby sym from t"          / select phrase
q)first (parse "select from t where p > (avg;p) fby sym") 2  / where phrase

Supported aggregates are sum, min, max, count, first, last, avg, var and dev. Also supported operations are ratios, deltas, mins, maxs, sums, prds, fills, prev, next, iasc and rank. The group key may be a column, `i, an expression, or a table of several columns. Under a by clause the fby is evaluated per query group, as in q.

Supported operations

Not all types are supported for all operations, but in general the supported types are scalars and 1D vectors of basic atomic types.

  • Binary ops: =, <>, <, >, <=, >=, +, -, *, %, |, &, in, within, xbar, xexp, ^
  • Unary ops: abs, log, exp, sin, asin, cos, acos, tan, atan, floor, ceiling, sqrt
  • Aggregate ops: sum, min, max, avg, count, first, last, var, dev, wavg
  • Scan ops: sums, prds, mins, maxs, fills, ema
  • Each-prior ops: ratios, deltas
  • Windowed scan ops: mavg, msum, mmin, mmax, mdev
  • Shift ops: prev, next, xprev
  • Sorting ops: iasc, rank, xrank
  • Grouped broadcast: fby (refer to fby)
  • Casting

In parse-tree form, <> is (';~:;=), >= is (';~:;<), and <= is (';~:;>).

Results may deviate slightly from base kdb+. For more information, refer to CUDA Mathematical Functions.

Enum domains in binary operations

Enum values compare by their domain index, so an equality (=) or fill (^) between enum values of different domains is rejected with GPU_NOT_IMPLEMENTED, including when one side is a scalar. q compares them by symbol. Use aj, aj0, ajf, ajf0 or bin to match enum keys by symbol across domains.

Seeded scan over an empty input

For a seeded scan (for example, fills with a seed value) over an empty column, when the seed type differs from the input type, .gpu.select returns a typed empty list instead of q's typeless ().

Seeded fills

The seed of fills[seed;x] must be an atom, as in q: a literal, a constant expression (1+2), an aggregate (max x), or a scan of one (sums max x). Any other seed (a column, a vector, a dictionary, a table) is a q nyi error and gives GPU_INVALID_STATEMENT, in a select, update, where or by clause. Under by, an aggregate seed seeds each group with its own value. Not implemented: an aggregate seed over a constant input under by (fills[max x;5] by g) gives GPU_NOT_IMPLEMENTED. Rejected with GPU_INVALID_STATEMENT although q accepts them: a seeded fills as the seed under by (fills[fills[max x;max y];y] by g) and an mmax or mmin aggregate seed (fills[(mmax;2;(max;x));y]`).

Month cast range

Casting a month to a date or timestamp matches kdb+ for months from -23999 (0000.02m) up to about 70555437. Outside that range, .gpu.select returns the calendar-correct day count, or an infinity whose sign follows the month, and may differ from kdb+.

setMemRelThres

Set the memory release threshold

.gpu.setMemRelThres num_bytes

Deprecated: use .gpu.config instead.

.gpu.setMemRelThres num_bytes is equivalent to .gpu.config ([memRelThres:num_bytes]).

Set the memory release threshold for the GPU memory pool of the active device to num_bytes bytes. Refer to the memRelThres setting of .gpu.config for what the threshold does.

q)/ set the current active device to device 1
q).gpu.setDev 1
q)/ set memory release threshold of device 1 to 10GB
q).gpu.setMemRelThres 10*1024*1024*1024

sublist

Head, tail or slice of a list

.gpu.sublist[x;y]

Where x is:

  • a positive long atom, returns the first x items of y.
  • a negative long atom, returns the last x items of y.
  • a long vector, returns up to x[1] items from y, starting at item x[0]. The result contains no more items than are available in y.

y can also be a tensor, sliced on its leading axis: .gpu.sublist[2;X] and 2 sublist X are the first two rows of the matrix X, as 2 sublist x is of the host matrix x. A count that reaches the length returns y itself, as q does.

Negative starting points are not yet implemented for GPU-resident vectors or tensors.

q)/ Head or tail, similar to .gpu.take but limited to the list length
q)A:.gpu.to a:2 3 5 7 11
q).gpu.sublist[3;A]
2 3 5
q).gpu.sublist[3;A]
2 3 5
q).gpu.sublist[10;A]
2 3 5 7 11
q).gpu.take[10;A]
2 3 5 7 11 2 3 5 7 11

q)syms:-10?`3
q)T:.gpu.to t:`time xasc([]time:2026.07.10D12:32+5?00:00:00;sym:5?syms;px:95+5?10)
q)t
time                          sym px 
-------------------------------------
2026.07.10D18:48:49.000000000 adl 104
2026.07.11D03:15:53.000000000 cgl 97 
2026.07.11D04:57:05.000000000 hmc 104
2026.07.11D06:32:25.000000000 fjk 96 
2026.07.11D09:26:01.000000000 cgl 104
q).gpu.sublist[-2] T
time                          sym px 
-------------------------------------
2026.07.11D06:32:25.000000000 fjk 96 
2026.07.11D09:26:01.000000000 cgl 104


q)/ Slice
q)A:.gpu.to a:2 3 5 7 11
q).gpu.sublist[1 2;A]
3 5

q) / Negative start for GPU nyi
q)a
2 3 5 7 11
q)sublist[-2 5;a]
0N 0N 2 3 5
q).gpu.sublist[-2 5;a]
0N 0N 2 3 5
q).gpu.sublist[-2 5;A]
'nyi

sync

Wait for outstanding GPU work

.gpu.sync[]

Blocks the calling CPU thread until all work on the currently selected GPU has completed. Returns null.

.gpu.sync is useful to avoid 'wsfull when the CPU provides work faster than the GPU can consume it, or when more explicit control of memory is needed.

q)a:til 1000000
q)-16!a
1i                  / only 1 reference to a
q)A:.gpu.to a       / returns before the copy to device is completed
q)-16!a             / the gpu stream owns a reference to a to facilitate this
2i
q)r:.gpu.from A     / implicit sync before return to q, but .gpu.from does no destructor work.
q)-16!a             / gpu stream still owns a reference to a, despite the copy having completed
2i

q).gpu.sync[]       / explicit sync, waits for the stream to complete then completes any destructor work
q)-16!a
1i

q)/ Most other apis will run any outstanding destructor work but without syncing.
q)a:til 1000000; b:til 1000000  / Get new objects, both rc=1
q)A:.gpu.to a; B:.gpu.to b      / Both return A & B before either copy is complete:
q)-16!a
2i
q)-16!b
2i
q)B:.gpu.to b                   / Both copies have completed, doing another doesn't sync but does clean up the refs.
q)-16!a
1i
q)-16!b
2i
q).gpu.sync[]                   / And a final sync cleans up the latest ref.
q)-16!a
1i
q)-16!b
1i

q)a:til 1000000; b:til 1000000  / Get new objects, both rc=1
q)A:.gpu.to a; .gpu.sync[]; B:.gpu.to b / Ensures A is done first, second copy can clean up its ref.
q)-16!a
1i
q)-16!b
2i

take

Select leading or trailing items from a list or dictionary, named entries from a dictionary, or named columns from a table

.gpu.take[x;y]

Where x is:

  • a positive long atom, returns the first x items of y
  • a negative long atom, returns the last x items of y
  • a symbol list, returns the specified keys/column of a dictionary/table y
q)A:.gpu.to a:til 5
q)D:.gpu.to d:`a`b!(a;a+10)
q)T:.gpu.to t:flip d

q).gpu.take[2;A]
0 1
q).gpu.take[2;D]
a| 0  1  2  3  4
b| 10 11 12 13 14
q).gpu.take[`b`a;D]
b| 10 11 12 13 14
a| 0  1  2  3  4 
q).gpu.take[2;T]
a b
----
0 10
1 11
q).gpu.take[-2;T]
a b
----
3 13
4 14
q).gpu.take[`b;T]
b
--
10
11
12
13
14

tensor

Create dense GPU tensors and perform arithmetic, reductions, cumulative scans, sorting, concatenation and linear algebra on the GPU

Tensor objects are distinct from GPU table columns, and are returned as virtual tensors. Construction and transfer accept homogeneous q int, long, real, and float data (KI, KJ, KE, and KF), stored densely in row-major order. Elementwise unary and binary arithmetic, reductions and scans support all four dtypes with the operation-specific promotion described below. mmu, inv, chol, and lsq also take all four dtypes, as does PCA; mmu and lsq accept mixed pairs. Concatenation, sorting and gather take all four dtypes, and typed storage, transfers and random generation retain their existing dtype support.

.gpu.tensor.to x
.gpu.tensor.from tensor
.gpu.tensor.shape tensor
.gpu.tensor.meta tensor
.gpu.tensor.reshape[shape;tensor]
.gpu.tensor.transpose tensor
.gpu.tensor.cast[dtype;tensor]

.gpu.tensor.mmu[left;right]
.gpu.tensor.inv tensor
.gpu.tensor.chol tensor
.gpu.tensor.lsq[targets;predictors]
.gpu.tensor.sum[axes;keepdims;tensor]
.gpu.tensor.avg[axes;keepdims;tensor]
.gpu.tensor.prd[axes;keepdims;tensor]
.gpu.tensor.min[axes;keepdims;tensor]
.gpu.tensor.max[axes;keepdims;tensor]
.gpu.tensor.first[axes;keepdims;tensor]
.gpu.tensor.last[axes;keepdims;tensor]
.gpu.tensor.sums[axis;tensor]
.gpu.tensor.prds[axis;tensor]
.gpu.tensor.mins[axis;tensor]
.gpu.tensor.maxs[axis;tensor]
.gpu.tensor.asc[axis;tensor]
.gpu.tensor.desc[axis;tensor]
.gpu.tensor.pca[configuration;tensor]
.gpu.tensor.concat[axis;tensors]
.gpu.tensor.take[count;tensor]
.gpu.tensor.view[count;tensor]
.gpu.tensor.gather[spec;tensor]

.gpu.tensor.random.create configuration
.gpu.tensor.random.meta generator
.gpu.tensor.random.reset generator
.gpu.tensor.random.bits[generator;shape]
.gpu.tensor.random.uniform[generator;shape;dtype]
.gpu.tensor.random.normal[generator;shape;mean;stddev;dtype]
.gpu.tensor.random.lognormal[generator;shape;mean;stddev;dtype]
.gpu.tensor.random.poisson[generator;shape;lambda]
.gpu.tensor.random.randint[generator;shape;low;high]

shape and strides in .gpu.tensor.meta are long vectors. dtype is the q short type code, device is the allocation device, and contiguous reports logical row-major contiguity. The axisNames value is a general list containing one symbol vector per axis; `symbol$() denotes an unnamed axis.

q)x:.gpu.tensor.to (1 2 3f;4 5 6f)
q).gpu.tensor.shape x
2 3
q).gpu.tensor.transpose x
1 4
2 5
3 6
q).gpu.tensor.reshape[enlist 6;.gpu.tensor.transpose x]
1 4 2 5 3 6f
q).gpu.tensor.meta x
dtype     | 9h
rank      | 2
shape     | 2 3
strides   | 3 1
device    | 0
contiguous| 1b
axisNames | (`symbol$();`symbol$())

reshape requires the same element count and otherwise signals length. A row-contiguous tensor (contiguous is 1b, as it is for a transpose view with an extent of 1) is reshaped by metadata alone and shares its storage. A transpose view whose extents both exceed 1 (contiguous is 0b) is first copied into a new row-contiguous tensor, so the result does not share storage with the view, and its elements follow the view's logical order. That copy, like the one from makes of such a view, needs free device memory of about the size of the view while the call runs. reshape may produce tensors above rank 2 (up to rank 8), consumed by unary/binary arithmetic, reductions, scans, sorting and concatenation. Host transfer remains rank 0–2: reshape a contiguous higher-rank result to a vector before downloading it. transpose is a metadata-only rank-2 view. mmu supports vector-vector, vector-matrix, matrix-vector, and matrix-matrix forms, and accepts transpose views. Its operands can have any combination of KI, KJ, KE, and KF dtypes. The result is KF if either operand is KF, otherwise KE if either is KE, otherwise KF for integer-only pairs. Operands are converted on the GPU before multiplication; integer nulls become floating nulls and propagate through the product, even when multiplied by zero. Other integer values are cast numerically. Conversion can round integers above 2^24 in KE or 2^53 in KF; integer products do not use wrapping arithmetic. Each converted operand needs temporary device memory equal to its element count times the result element width. Matching KE and matching KF pairs need no conversion. A float64 matrix times its own transpose (mmu[x;transpose x] or mmu[transpose x;x]) is a symmetric product; builds for Ada GPUs (sm_89) compute a large one faster, from 1392 rows (or columns) of the product in multiples of 16, with a contraction length of 100 to 10000 in multiples of 4 (measured on an L4). The values are the same product to rounding, though for some data (constant rows) the rounding error can be several times the general product's. Other sizes, other architectures and products requiring dtype conversion use the general product, as do KE tensors. inv and chol preserve KE/KF and promote KI/KJ to KF. lsq uses the same promotion as mmu: KF takes precedence over KE, and integer-only pairs return KF. Integer nulls are converted to floating nulls before solving; other integers are cast numerically, with the same precision limits as mmu. PCA also preserves KE/KF and promotes integers to KF. inv accepts square matrices. chol returns the lower-triangular Cholesky factor L of a symmetric positive definite matrix, with A ~ mmu[L;transpose L] and exact zeros above the diagonal; it reads only the lower triangle of its input and signals GPU_TENSOR_NOT_POSITIVE_DEFINITE when there is no factor. lsq uses q's features-by-observations contract:

targets[m,n] lsq predictors[k,n] -> coefficients[m,k]

These solvers compute in the result dtype: KE uses float32 factorization and solves rather than computing in float64 and narrowing. Float32 has lower accuracy, particularly for lsq, whose normal equations square the predictor matrix's condition number; use KF for ill-conditioned problems or tight residual bounds. inv and chol convert directly into their working copy; lsq needs an additional device buffer of elements * result_element_width bytes per converted operand. Singular inv and rank-deficient or underdetermined lsq return correctly shaped floating nulls in the result dtype. Cholesky failure remains GPU_TENSOR_NOT_POSITIVE_DEFINITE.

mmu, inv, chol, lsq and pca accept transpose views. inv and chol give exactly the result of the row-contiguous tensor with the same elements; mmu and lsq agree with it to rounding (a numerically borderline rank-deficient lsq problem may differ in whether it returns nulls). Unsupported ranks and mismatched dimensions are rejected with GPU_TENSOR_SHAPE.

Arithmetic, reductions, scans, asc, desc, concat, mmu, inv and lsq enqueue work on the device stream. Singular and rank-deficient inv/lsq results are resolved on the device. Downloads wait for results; the table bridge remains device-to-device. chol and PCA are explicit exceptions: they synchronize to validate numerical status, while keeping their outputs resident.

Casting tensors

.gpu.tensor.cast[dtype;tensor] converts between int, long, real, and float on the device:

Target symbol Short type code Character code
`int 6h "i"
`long 7h "j"
`real 8h "e"
`float 9h "f"
x:.gpu.tensor.to 1.2 -2.5 0n;
i:.gpu.tensor.cast[`int;x]; / q rounding, including -2.5 to -3
f:.gpu.tensor.cast[9h;i];
e:.gpu.tensor.cast["e";f];

Ranks 0–8, empty tensors, transpose views, and offset views are supported. Shape and all axis names are preserved. If the target dtype already matches, the result is a retained reference to the original tensor: no allocation or kernel, with the same strides, offset, and value bits. Otherwise the result owns independent row-contiguous storage in logical element order; the source is unchanged. Changing dtype needs device memory of elements * target_element_width, with no additional promoted input buffer. The conversion enqueues on the current device stream without a host-result synchronization.

Value conversions match native q's numeric casts, not C++ integer truncation:

  • Integer nulls and floating NaNs become destination nulls. A dtype-changing cast does not preserve NaN payloads; an identity cast preserves every bit, including payloads and signed zero.
  • Floating-to-integer conversion rounds halves away from zero and saturates out-of-range values and floating infinities to destination integer ±infinity. It follows q's binary64 add-half behavior even near 2^52, where casting the integral float 4503599627370497f to long yields 4503599627370498j.
  • Integer widening is exact; narrowing saturates to destination integer ±infinity rather than wrapping. Non-null integer infinity sentinels are ordinary numeric values when widened or converted to floating types.
  • Floating conversion uses nearest-even rounding and preserves signed zero. As in q, widening a real subnormal to float yields signed zero; narrowing a float to a real subnormal also yields signed zero after rounding. Floating overflow produces signed infinity. Integer-to-floating casts can lose precision above 2^24 for real or 2^53 for float. As in q, long-to-real conversion rounds through float64; at large halfway values its result can differ from a direct integer-to-f32 conversion.

The target selector is validated before the tensor, including identity casts. Unknown symbols, character codes, and unsupported short codes report domain. Wrong selector types report type: int/long type-code atoms, vectors, and :: are not accepted. Non-tensor inputs report GPU_TYPE_UNSUPPORTED; tensors on another device report GPU_WRONG_DEVICE. These explicit cast rules do not change arithmetic or solver promotion rules.

Tensor arithmetic and broadcasting

Unary operations are direct members of .gpu.tensor:

neg abs exp log sqrt recip floor ceil erf erfc ncdf
sin cos tan asin acos atan sinh cosh tanh sign square rsqrt expm1 log1p
exp2 log2 log10 trunc round

ceiling aliases ceil; signum aliases sign. Each accepts a KE, KF, KI or KJ tensor and returns an independent row-contiguous tensor of the same shape and axis names. Real and float inputs preserve their dtype. Integer inputs to neg, abs, floor, ceil, sign, square, trunc and round preserve KI or KJ; integer nulls are preserved. The other unary operations convert integer inputs to KF, mapping integer nulls to the float null. Ranks 0–8, empty tensors and rank-2 transpose views are supported. round uses ties-to-even. For floating inputs, floor, ceil, trunc and round preserve the input dtype. sign propagates NaN and maps either signed zero to positive zero. ncdf is the standard normal cumulative distribution.

The binary operations are add, sub, mul, div, pow, minimum, maximum:

x:.gpu.tensor.to (1 2 3f;4 5 6f);
row:.gpu.tensor.to 10 20 30f;
.gpu.tensor.add[x;row] / 2 by 3: adds the row to every row
.gpu.tensor.div[1f;x] / scalar on either side
.gpu.tensor.exp x

Operands may be KE, KF, KI or KJ tensors, or scalar atoms of those types; at least one operand must be a tensor. Boolean, temporal, symbol, vector and other unsupported operands are rejected. Floating operands dominate integer ones: the result is KF if either operand is KF, otherwise KE if either operand is KE. With only integer operands, add, sub, mul, minimum and maximum preserve integers, promoting KI+KJ to KJ; integer-only div and pow return KF. Integer nulls propagate, including to the appropriate floating null when promoted. Integer addition, subtraction and multiplication wrap at the result width. There is no generic map/ew selector and no duplicate top-level .gpu arithmetic API.

Shapes align from the trailing axis, following NumPy/PyTorch broadcasting: dimensions must be equal or one must be 1, and missing leading dimensions act as 1. [m,n] with [n] yields [m,n]; [m,1] with [1,n] yields [m,n]. An extent of 0 with 1 yields 0. Broadcast axes use stride-zero reads; expanded operands are not copied. Results own independent storage and inputs are unchanged. Both tensor operands must belong to the current GPU. A tensor operand may be row-contiguous or a rank-2 transpose view, and the result is a new row-contiguous tensor.

Names propagate from non-expanded axes. Two named contributors to the same result axis must match in value and order or raise GPU_AXIS_MISMATCH; no implicit reordering occurs. An expanded singleton's name is discarded. Scalar atoms supply no names.

Floating NaNs, infinities and signed zeros are valid arithmetic inputs. Both minimum and maximum propagate NaN; table-select maximum has different q-specific null semantics. Tensor power follows floating mathematical power, not the table operator's q-specific special cases. Domain failures produce floating NaNs or infinities as appropriate. No NaN-payload identity or bitwise agreement between NumPy, PyTorch and CUDA is promised, including tied signed-zero min/max results.

Axis-aware tensor reductions

x:.gpu.tensor.to (1 2 3f;4 5 6f);
.gpu.tensor.sum[0;0b;x]  / 5 7 9f
.gpu.tensor.avg[0;0b;x]  / 2.5 3.5 4.5
.gpu.tensor.prd[-1;0b;x] / 6 120f
.gpu.tensor.min[-1;1b;x] / (enlist 1f;enlist 4f)
.gpu.tensor.max[::;0b;x] / 6f, a rank-zero tensor
.gpu.tensor.first[0;0b;x] / 1 2 3f
.gpu.tensor.last[-1;0b;x] / 3 6f
.gpu.tensor.last[::;0b;x] / 6f, a rank-zero tensor
.gpu.tensor.last[0;0b;.gpu.tensor.to (1 2 3;4 5 6)] / 4 5 6, a long vector

sum, avg (the arithmetic mean), prd (product), min and max reduce the given axes of a KE, KF, KI or KJ tensor, and first and last select an element of them, as described below. All preserve the input dtype except integer avg, which converts inputs to KF before summing and returns KF. Real and float sums, products and means accumulate at their input width. axes is :: (every axis), a long atom, or a long vector. Negative indices count from the last axis. Out-of-range indices, duplicate normalized axes, invalid types and a non-boolean keepdims are errors. Multiple axes may be supplied in any order. An empty long vector returns an independent copy, preserving signed-zero bits; integer avg still converts to KF, including integer nulls to floating nulls. With keepdims:1b, reduced axes have extent 1 and no coordinate names; otherwise they disappear. Unreduced names survive. The input may be a row-contiguous tensor or a rank-2 transpose view, and the result is a new row-contiguous tensor that owns its storage. A view gives the same min, max, first and last as the row-contiguous tensor with the same elements, and sum, avg and prd that agree to rounding.

Rank-zero tensors accept ::, atom 0 or atom -1, and an empty long vector; nonempty axis vectors are rejected. Rank-zero output stays rank zero with either keepdims setting. An empty surviving dimension produces an empty result.

For sum, avg, prd, min and max, reducing nothing (a reduced axis of extent zero) gives the operation's identity: 0 for sum, 1 for prd, positive infinity for min and negative infinity for max, as in q, and NaN for avg (0 divided by 0). Integer extrema use the integer infinities (0Wi/-0Wi for KI, 0Wj/-0Wj for KJ). Floating sums and means start from positive zero, so a sum or mean of negative zeros is positive zero; prd, min and max of a lone -0f stay -0f. avg is the sum divided by the number of reduced elements, so it overflows to infinity where the sum does. Integer sums and products wrap modulo the result width; integer means sum in KF instead. An integer null poisons its whole reduction, including a floating null result for integer avg. Null tracking is separate from wrapping arithmetic: an intermediate value with the null's bits does not poison a reduction unless a null was present in its input.

In sum, avg, prd, min and max, NaNs propagate, including q floating nulls; infinities follow ordinary arithmetic. This differs from q, whose vector versions of these skip nulls. min and max return an element of the line, or NaN if any element is NaN, and a tie between -0f and 0f resolves the same way wherever the elements sit: min returns -0f, max returns 0f, unlike mins and maxs, which keep the earlier element. min and max return the same element whatever the order of the reduction, whereas finite floating sum, avg and prd results may differ in low bits from a sequential q or NumPy result because parallel reduction changes the order of the operations, and a prd whose partial products overflow or underflow can differ from a sequential product for the same reason. The output remains on the GPU without a host-result synchronization.

first and last select an element; they do not combine. first returns the element at index 0 of every reduced axis and last the element at the last index of every reduced axis. Over every axis they give the first and last elements of the tensor in row-major order, and listing the axes in any order gives the same element. The element is copied unchanged: nulls and NaN payloads are not made canonical or propagated, -0f stays -0f and infinities are unchanged. This matches q, whose first and last do not skip nulls. They take KE, KF, KI and KJ tensors, and the result has the dtype of the input. A reduced axis of extent zero has nothing to select and gives the dtype's null: 0Ne, 0n, 0Ni or 0Nj.

Axis-aware tensor cumulative scans

x:.gpu.tensor.to (1 3 2f;4 0 9f);
.gpu.tensor.sums[0;x]  / (1 3 2f;5 3 11f)
.gpu.tensor.sums[-1;x] / (1 4 6f;4 4 13f)
.gpu.tensor.prds[-1;x] / (1 3 6f;4 0 0f)
.gpu.tensor.mins[-1;x] / (1 1 1f;4 0 0f)
.gpu.tensor.maxs[0;x]  / (1 3 2f;4 3 9f)

sums, prds, mins and maxs return every inclusive running sum, product, minimum or maximum along one axis, preserving the input shape and all axis names. For example, scanning axis 0 of daily log returns shaped [days,simulations,assets] with sums retains each asset's cumulative log return on every day, and maxs[0;...] of that result is each asset's running peak cumulative log return. Use a reduction such as sum or max when only the total or the peak is required.

axis must be one q long atom. Negative indices count from the last axis. Vectors, ::, functions, other types and out-of-range indices are errors; there is no flattening sentinel or multiple-axis form. Rank-zero tensors accept only axis 0 or -1 and return an independent rank-zero copy.

Inputs may be KE, KF, KI or KJ tensors on the current GPU; all four scans preserve the input dtype. Ranks 0–8, row-contiguous tensors and rank-2 transpose views are supported. Results own independent row-contiguous storage; inputs are unchanged. Empty inputs return an empty result without a scan, and singleton scan axes return independent copies, preserving signed-zero bits.

sums and prds use floating addition and multiplication at the input width, or integer arithmetic that wraps modulo the input width. Integer nulls poison the remaining prefixes of their own line in all four scans. A wrapped intermediate value with the null's bits does not poison later prefixes unless a null was present in the input. mins and maxs return an element of the line, and when two elements compare equal the earlier one is kept, so -0f and 0f ties resolve to whichever comes first (unlike the elementwise minimum and maximum, which promise no tie order). A NaN present in the input, including a q floating null, propagates through the remaining results of its own scan line in all four scans without affecting other lines, as minimum and maximum propagate NaN. Infinities follow ordinary arithmetic. This differs from native q, which treats a null as the operator's identity in sums, prds, mins and maxs (for example mins 0n 1 2f is 0w 1 1).

Finite floating sums and prds results may differ in low bits from sequential q or NumPy results, and from one run to the next, because parallel scans can change the order of the operations; bitwise agreement is not guaranteed. The same reordering means that a sums or prds whose partial results overflow, underflow, or produce a NaN from inf-inf or inf*0 can differ from a sequential scan, even to a finite value where the sequential result is infinite. mins and maxs give the same result on every run.

The operation enqueues work on the device stream without a host-result synchronization. Intermediate values and the result remain on the GPU; download explicitly with .gpu.tensor.from when needed.

Sorting tensors

x:.gpu.tensor.to (3 1 2f;0 9 5f);
.gpu.tensor.asc[-1;x]  / (1 2 3f;0 5 9f)
.gpu.tensor.desc[-1;x] / (3 2 1f;9 5 0f)
.gpu.tensor.asc[0;x]   / (0 1 2f;3 9 5f)
.gpu.tensor.desc[0;x]  / (3 9 5f;0 1 2f)
.gpu.tensor.asc[0;.gpu.tensor.transpose x] / (1 0f;2 5f;3 9f)
w:3 0n 1 0n 2f;
.gpu.tensor.asc[0;.gpu.tensor.to w]  / 0n 0n 1 2 3f
.gpu.tensor.desc[0;.gpu.tensor.to w] / 3 2 1 0n 0n
(.gpu.tensor.from .gpu.tensor.asc[0;.gpu.tensor.to w])~asc w / 1b

asc and desc sort a tensor along one axis, ascending and descending, preserving the shape: axis -1 of a matrix sorts each row on its own and axis 0 sorts each column. Every line along the axis is sorted independently of the others. This differs from q's asc of a list of rows, which orders the rows themselves, and from .gpu.asc, which sorts a GPU column.

axis must be one q long atom, as for the scans. Negative indices count from the last axis. Vectors, ::, functions and other types signal GPU_TYPE_UNSUPPORTED, and an index outside the tensor's rank signals GPU_TENSOR_SHAPE; there is no flattening sentinel or multiple-axis form. Rank-zero tensors accept only axis 0 or -1.

Sorting accepts KE, KF, KI or KJ tensors, and the result has the same dtype and shape. Ranks 0–8, row-contiguous tensors and rank-2 transpose views are supported, and sorting a view gives the same result as sorting a row-contiguous tensor with the same elements. The result is a new row-contiguous tensor that owns its storage; the input is unchanged. An empty tensor gives an empty result, and a rank-zero tensor or an axis of extent 1 gives a copy whose NaNs are made canonical, as in any result. An argument that is not a tensor signals GPU_TYPE_UNSUPPORTED, and a tensor on another GPU signals GPU_WRONG_DEVICE.

The sort is stable in both directions: elements that compare equal keep their order along the axis. desc is therefore not asc reversed when there are ties.

Floating-point values order as in q: -0w is below every finite value and 0w above. Every NaN, including the q nulls 0n and 0Ne, compares equal to every other NaN, and sorts first in asc and last in desc. Each NaN in the result is the same quiet NaN, the one q uses for its null, so the sign and payload bits of an input NaN are not preserved. -0f and 0f compare equal and keep their input order in both directions, so asc and desc of 0f,-0f both return 0f,-0f, signs as they came. For a float vector w, including one with nulls, .gpu.tensor.from .gpu.tensor.asc[0;.gpu.tensor.to w] matches asc w, except that q's asc sets the `s attribute and the result here does not. Integers sort in signed order; the q nulls 0Ni and 0Nj are the smallest values, so they come first in asc and last in desc, as in q.

Every axis keeps its names except the sorted one, whose names are cleared because its elements have moved (also when its extent is 1). The input's names are unchanged.

While the call runs it needs free device memory of up to about twice the tensor's size, for the result and a temporary copy that is released afterwards, plus working storage for the sort. A rank-zero tensor and an axis of extent 1 need no temporary copy, and a sort of more than one line, each of a few thousand elements or fewer, needs no device memory beyond the result. The operation enqueues work on the device stream without a host-result synchronization, so results chain into further tensor operations; download explicitly with .gpu.tensor.from when needed.

Concatenating tensors

a:.gpu.tensor.to (1 2 3f;4 5 6f);
b:.gpu.tensor.to (7 8 9f;10 11 12f);
.gpu.tensor.concat[0;(a;b)]  / (1 2 3f;4 5 6f;7 8 9f;10 11 12f)
.gpu.tensor.concat[1;(a;b)]  / (1 2 3 7 8 9f;4 5 6 10 11 12f)

concat[axis;tensors] joins a list of one or more tensors along axis, the way , joins along the first axis and ,' along the second. axis is one q long atom, and negative indices count from the last axis. tensors is a general list, so a single tensor is passed as enlist t; a bare tensor is an error (GPU_TYPE_UNSUPPORTED). The tensors must have the same dtype (KE, KF, KI or KJ; nothing is promoted), the same rank (1 to 8) and the same extent on every axis except axis. Rank-zero tensors, an empty list, a mix of dtypes, a tensor on another device and disagreeing extents are errors.

The result is a new row-contiguous tensor that owns its storage, and the inputs are unchanged. Row-contiguous tensors and rank-2 transpose views are accepted, so concat[0;enlist t] is a row-contiguous copy of t, a transpose view or not. An input with extent zero along axis adds no elements, extent or names along it; its rank, its other extents and its other-axis names are still checked.

When every input with a nonzero extent along axis is named there, the result names that axis with the concatenated names. Otherwise that axis is unnamed. Every other axis is named when any input names it, and inputs that name it must agree (GPU_AXIS_MISMATCH), empty inputs included. Joined names are not checked for uniqueness: concat[1;(t;t)] of a tensor named `a`b names the axis `a`b`a`b, which toTableAs accepts and toTable rejects as duplicate names. The operation enqueues work on the device stream without a host-result synchronization.

Taking and viewing rows of a tensor

a:.gpu.tensor.to 1 2 3f;
.gpu.tensor.take[2;a]     / 1 2f
.gpu.tensor.take[-2;a]    / 2 3f
.gpu.tensor.take[7;a]     / 1 2 3 1 2 3 1f
.gpu.tensor.take[-7;a]    / 3 1 2 3 1 2 3f
m:.gpu.tensor.to (1 2 3f;4 5 6f;7 8 9f;10 11 12f);
.gpu.tensor.take[2;m]     / (1 2 3f;4 5 6f)
.gpu.tensor.take[-1;m]    / enlist 10 11 12f
.gpu.tensor.view[-2;m]    / (7 8 9f;10 11 12f), without a copy

take[count;tensor] is the tensor form of q's n#x over the leading axis: the first count entries or, when count is negative, the last |count|. For a matrix those entries are rows. Like q, it repeats the entries in a cycle when |count| exceeds the extent of the axis, and a negative count ends on the last entry (-7#1 2 3 is 3 1 2 3 1 2 3). count is one q long or int atom, and tensor is a tensor of rank 1 to 8 of any dtype (KE, KF, KI or KJ), which may be a transpose view. The result has the same dtype, device and rank, with |count| entries on the first axis and every other extent unchanged; take[0;t] is an empty tensor of that shape. Taking a nonzero count from an empty first axis fills the result with the dtype's q null, as q does for typed empty lists. q's long and int nulls, a rank-zero tensor and a result larger than the device can hold are errors (GPU_INVALID_VALUE, GPU_TENSOR_SHAPE and an allocation error); a count that is not a long or int atom, or a tensor that is not a tensor, reports GPU_TYPE_UNSUPPORTED.

take always returns a new row-contiguous tensor that owns its storage, so it never keeps the source alive and the source is unchanged. It needs free device memory for the result. The names of the first axis are repeated with the rows, so a cycled result can name an axis with repeated names (as for concat), and every other axis keeps its names.

view[count;tensor] returns the same first or last |count| rows without copying tensor data: the result shares the storage of tensor, with the same strides and an offset, and no kernel runs. It does not cycle, so a count with |count| above the extent of the first axis, and q's nulls, are errors (GPU_INVALID_VALUE). The result must be a layout the other tensor operators read: a row-contiguous tensor always qualifies, and so does a transpose view when view keeps every row of it (or one of its extents is 1). Only some of the rows of a transpose view are a strided window of the matrix, which is not readable, so view reports GPU_TENSOR_SHAPE there rather than copy; use take. A view keeps all of the storage of the source alive until it is released: view[4;x] of a very large x holds all of x. The names of the first axis are sliced to match, which copies a partial named axis on the host; every other axis keeps its names. Both operations enqueue work on the device stream without a host-result synchronization.

Indexing a tensor with gather

m:.gpu.tensor.to (1 2 3 4f;5 6 7 8f;9 10 11 12f);
.gpu.tensor.gather[1;m]            / 5 6 7 8f, an integer drops its axis
.gpu.tensor.gather[-1;m]           / 9 10 11 12f, counted from the end
.gpu.tensor.gather[(::;2);m]       / 3 7 11f, column 2 of every row
.gpu.tensor.gather[1 2;m]          / 7f, one integer per axis: a rank-zero tensor
.gpu.tensor.shape .gpu.tensor.gather[1 2;m]         / `long$(), the shape of a rank-zero tensor
.gpu.tensor.gather[enlist 2 0;m]   / (9 10 11 12f;1 2 3 4f), a list on axis 0
.gpu.tensor.gather[(::;3 3 0);m]   / (4 4 1f;8 8 5f;12 12 9f), repeats are allowed
.gpu.tensor.gather[(2 0;1 3);m]    / (10 12f;2 4f), the sub-grid of those rows and columns
.gpu.tensor.gather[(::;1010b);m]   / (1 3f;5 7f;9 11f), a mask keeps the true positions
.gpu.tensor.gather[enlist 101b;m]  / (1 2 3 4f;9 10 11 12f)
.gpu.tensor.gather[::;m]           / (1 2 3 4f;5 6 7 8f;9 10 11 12f), a copy
v:.gpu.tensor.to 1 2 3f;
.gpu.tensor.gather[enlist 2 0;v]   / 3 1f, the positions 2 and 0 of a vector

A range is a dictionary with the optional keys `start, `stop and `step; a bare dictionary is a range on axis 0:

m:.gpu.tensor.to (1 2 3 4f;5 6 7 8f;9 10 11 12f);
.gpu.tensor.gather[`start`stop!1 3;m]                  / (5 6 7 8f;9 10 11 12f), rows 1 and 2
.gpu.tensor.gather[(::;`start`stop`step!1 100 2);m]    / (2 4f;6 8f;10 12f), stop clamped to 4
.gpu.tensor.gather[(::;`start`stop`step!0N 0N -2);m]   / (4 2f;8 6f;12 10f), 0N is not given
.gpu.tensor.gather[(::;(enlist `start)!enlist -2);m]   / (3 4f;7 8f;11 12f), the last two columns
.gpu.tensor.gather[(enlist `step)!enlist -1;m]         / (9 10 11 12f;5 6 7 8f;1 2 3 4f), reversed

On a tensor of higher rank every axis takes an item. .gpu.tensor.from downloads tensors of rank 0 to 2, so a result of higher rank is reshaped to a vector first:

t:.gpu.tensor.reshape[2 3 4;.gpu.tensor.to "f"$til 24];     / t[i;j;k] is 12*i+4*j+k
.gpu.tensor.from .gpu.tensor.gather[(1;::;::);t]            / (12 13 14 15f;16 17 18 19f;20 21 22 23f)
.gpu.tensor.from .gpu.tensor.gather[(::;1;-1);t]            / 7 19f
.gpu.tensor.from .gpu.tensor.gather[(1;::;1 2);t]           / (13 14f;17 18f;21 22f), shape 3 2
r:.gpu.tensor.gather[(::;1 2;`start`stop!1 3);t];
.gpu.tensor.shape r                                         / 2 2 2
.gpu.tensor.from .gpu.tensor.reshape[enlist 8;r]            / 5 6 9 10 17 18 21 22f

gather[spec;tensor] selects elements of a tensor along every axis, as NumPy indexing does, with integers, lists, boolean masks and ranges, and copies them into a new row-contiguous tensor that owns its storage. tensor has rank 0 to 8 and any dtype (KE, KF, KI or KJ), and is row-contiguous, a rank-2 transpose view, which is read in place through its strides, or a view of rows. The result has the dtype of tensor, and its elements are copied bit for bit: NaN payloads, -0f and q nulls survive. The source is unchanged, and the result does not keep it alive.

The selection is orthogonal: every axis is selected on its own, and the extent of the result on an axis is the number of positions chosen there. An axis chosen by an integer is dropped from the result, so a spec with one integer for every axis gives a rank-zero tensor.

spec is one of:

spec Selects
A general list One item per axis, in order (see below). An axis with no item is kept whole, so fewer items than axes leave the last axes whole and () is the whole tensor. More items than axes signal GPU_TENSOR_SHAPE.
A long or int vector, such as 1 2 3 One integer per axis, as in q's x . 1 2 3. q collapses (1;2;3) to this vector.
A long or int atom An integer on axis 0.
:: or 0#0 The whole tensor, copied.
A dictionary A range on axis 0.
A table One range per row, one row per axis.

A bare vector of positions is therefore one integer per axis, not a list of positions on axis 0: gather[2 0;v] of a vector v is two integers for one axis and signals GPU_TENSOR_SHAPE, and the list of positions is enlist 2 0.

Any other spec signals GPU_TYPE_UNSUPPORTED: a bare boolean vector (write enlist 101b for a mask on axis 0), a symbol, a float, an enumeration, or a function such as neg.

Each item of a general list selects along its axis:

Item Selects
A long or int atom That position, and the axis is dropped.
A long or int vector, or () Those positions, in that order. Repeats and an empty vector are allowed.
A boolean vector The positions of its 1b entries. Its length must equal the extent of the axis (GPU_TENSOR_SHAPE).
:: Every position.
A dictionary A range of positions; ()!() is every position.

A position is an integer in [-extent, extent), and a negative one counts from the end of the axis. A position outside that interval, a null (0N or 0Ni) and any position on an axis of extent 0 signal GPU_INVALID_VALUE.

A range follows a Python slice. Its dictionary has any of the symbol keys `start, `stop and `step, and long or int atoms as values. A key that is missing, and a value of 0N, 0Ni or ::, leave that bound unspecified: start and stop then default to the ends of the axis in the direction of the step, and step to 1. A negative start or stop counts from the end, both are clamped to the axis and never signal an error, and a negative step runs backwards, so `start`stop`step!0N 0N -1 is every position in reverse. A step of 0 and a key other than the three, or one given twice, signal GPU_INVALID_VALUE, and a key or a value of another type signals GPU_TYPE_UNSUPPORTED. A range of an empty axis, or one that selects nothing, is empty.

Two q behaviors shape how a spec is written:

  • A dictionary with one key needs enlist on both sides, (enlist `step)!enlist -1. `step!-1 and `step!enlist 2 are q enumerations (types -20h and 20h), not dictionaries, and signal GPU_TYPE_UNSUPPORTED. `start`stop!1 3 is a dictionary.
  • q turns a list of dictionaries with the same keys into a table, so enlist d and (d1;d2) are tables (type 98h), not general lists. A table is accepted as one range per row, one row per axis, with each key a column, and a 0N or :: in a column leaves that bound unspecified. A list that mixes dictionaries with other items, such as (d;3;::), or dictionaries with different keys, stays a general list and is read item by item.
m:.gpu.tensor.to (1 2 3 4f;5 6 7 8f;9 10 11 12f);
d1:`start`stop!1 3;
d2:`start`stop!0 2;
type enlist d1                                   / 98h
type (d1;d2)                                     / 98h
type (d1;3;::)                                   / 0h
.gpu.tensor.gather[(d1;d2);m]   / (5 6f;9 10f), rows 1 and 2, columns 0 and 1

The names of the axes follow the selection. An axis chosen by an integer is dropped with its names, an axis kept whole keeps its names, and an axis selected by a range, a list or a mask is named by the names at the selected positions, which repeat when the positions do. The names of the source are unchanged.

gt:.gpu.to ([]a:1 2 3f;b:10 20 30f;c:-1 0 1f);
f:.gpu.tensor.fromTable[`c`a;`features;gt];
(.gpu.tensor.meta .gpu.tensor.gather[(1 0 0;::);f])`axisNames   / (`a`c`c;`symbol$())
(.gpu.tensor.meta .gpu.tensor.gather[(::;1);f])`axisNames       / enlist `c`a

Compared with NumPy. Two index vectors give the sub-grid of those rows and columns, as x[np.ix_([2, 0], [1, 3])] does; NumPy's x[[2, 0], [1, 3]] pairs the indices instead, and has no counterpart here. NumPy moves the axes of a selection that has an integer and a list separated by a slice to the front: x[1, :, [1, 2]] of a tensor of shape 2 3 4 has shape 2 3. gather keeps the axes in the order of the source, and gives shape 3 2 for (1;::;1 2).

NumPy gather spec
x[1] 1
x[-1] -1
x[1, 2] 1 2
x[1:3] `start`stop!1 3
x[:, ::-1] (::;(enlist `step)!enlist -1)
x[[2, 0]] enlist 2 0
x[:, [0, 2]] (::;0 2)
x[np.ix_([2, 0], [1, 3])] (2 0;1 3)
x[:, mask] (::;mask), with mask a boolean vector

Compared with q. Indexing a q list of rows x at depth, x . (2 0;1 3), is orthogonal too and gives the same sub-grid. In q a negative index is out of range and gives a null (x[-1] is null); gather counts negative positions from the end. Where q gives a null for a position beyond the end, gather signals GPU_INVALID_VALUE; ranges alone are forgiving, because they clamp like Python slices, whereas an integer or a list never does. q has no syntax for a range, which is why a range is a dictionary. The argument order differs from the column function .gpu.gather[data;idx], which takes the data first: gather[spec;tensor] takes the selector first, as take and concat do.

Errors. The first of these that applies is reported, in this order: the tensor, the form of spec, the number of items against the number of axes, each axis in turn (the type of its item, then the length of a mask, then its positions, steps and keys), the size of the result, the names of the result and the allocation.

Error Cause
GPU_TYPE_UNSUPPORTED tensor is not a tensor. A spec or an item of a kind the tables above do not list, or a range whose keys are not symbols or whose values are not long or int atoms.
GPU_WRONG_DEVICE tensor is on another device.
GPU_TENSOR_SHAPE spec has more items than the tensor has axes. A mask whose length differs from the extent of its axis. The element count of the result, or the number of positions in its lists and masks, overflows.
GPU_INVALID_VALUE A position outside its axis, a null position, a step of 0, or a range key that is unknown or repeated.
GPU_HOST_ALLOC_FAILED The names of a selected axis would take more than 2^31 - 1 bytes of host memory.

A result that does not fit in device memory reports the allocation error, as for the other tensor operations. gather needs free device memory for the result, plus 8 bytes for each position of a list or a mask while the call runs.

A spec made only of integers, ranges and :: needs no index data: the call enqueues its copy on the device stream without a host-result synchronization, so it chains into further tensor operations. A spec with a list or a mask first builds the selected positions on the host and uploads them, so its cost grows with the number of positions. The names of a partly selected named axis are built on the host.

Not implemented: index tensors that stay on the device (a list or a mask is a q vector, uploaded on every call), zero-copy views for plain slices (view returns the leading rows without a copy), and assignment into a selection.

Principal component analysis

x:.gpu.tensor.to (2 0 1f;0 2 1f;3 1 2f;1 3 2f);
p:.gpu.tensor.pca[2;x]; / eigh on this small Gram, centered, sample variance
p`explainedVariance
.gpu.tensor.mmu[.gpu.tensor.sub[x;p`mean];p`components] / resident projection

Input rows are observations and columns are features. PCA accepts nonempty int, long, real, and float rank-2 tensors (KI, KJ, KE, KF), including transpose and offset views, on the current device. It does not modify the input. KE computes natively in float32 and returns every numerical field as KE; KF stays float64. Integer inputs are converted before arithmetic and return KF fields. Long values above 2^53 may round. Integer nulls become floating nulls and are rejected as nonfinite; other integers, including the reserved integer infinity values, are cast numerically. Configuration is a long k or a dictionary with unique symbol keys:

Option Default Contract
k Required Long, 1 <= k <= min(features, observations-center)
method eigh up to Gram order 512, else full Symbol full or eigh; unrequested it routes to eigh for small Gram orders, where the SVD path is far slower for the same spectrum
center 1b Boolean; subtract column means
ddof 1 Long, 0 <= ddof < observations
returnComponents 1b Boolean; include feature directions
returnScores 1b Boolean; include projected observations

Unknown/duplicate keys and auto/random methods are errors. A one-observation input requires center:0b, ddof:0, and k:1. Without centering, the mean output is a zero vector; PCA does not standardize feature variances.

Result field GPU tensor shape Meaning
components [features,k] Feature directions in descending singular-value order
scores [observations,k] (input-mean) mmu components
mean [features] Subtracted means, or zeros
singularValues [k] Descending singular values
explainedVariance [k] singularValues^2/(observations-ddof)
explainedRatio [k] Squared singular values divided by all optionally-centered input energy

The dictionary also contains method, a q symbol. Disabled components/scores are ::; all other numerical fields remain GPU tensors of the floating compute dtype. Feature names label the first components axis and mean; observation names label the first scores axis. Component-index axes are unnamed. Download explicitly with .gpu.tensor.from. Signs and bases within repeated singular subspaces are not unique.

full uses thin full SVD and permits zero singular values. Zero input energy produces zero explained ratios. eigh forms the smaller Gram matrix, selecting its leading eigenpairs and lifting feature directions for wide inputs. Its Gram order is limited to 32768. This saves substantial storage for many-feature inputs, but forming a Gram matrix squares the condition number. Small Gram orders (up to 512) form the Gram, the centered energy, and narrow-input scores directly over the raw input in one pass, allocating no centered copy; larger orders form a centered copy and retain the symmetric rank-k update in float64. Float32 has lower accuracy and a stricter numerical-rank cutoff; use float64 or retry explicitly with full when appropriate. No implicit precision upgrade occurs.

For eigh, the numerical-rank cutoff is 8*epsilon*max(observations,features)*max(largestEigenvalue,0), where epsilon is FLT_EPSILON for KE and DBL_EPSILON for KF (including promoted integers). Requested eigenvalues within the negative-to-positive cutoff interval raise GPU_TENSOR_PCA_RANK; more negative or nonfinite spectrum values raise GPU_TENSOR_PCA_SPECTRUM. Column means use the typed avg reduction, so integer accumulation promotes to float64 before summing. Input conversion is fused with centering into the solver matrix where one is formed; no separate promoted input copy is allocated, and the small-order eigh path forms no solver matrix at all. Mean accumulation, centered energy, factorization, projection, and spectrum outputs use the compute dtype. Float32 mean or energy overflow therefore reports GPU_TENSOR_NONFINITE, even if the float64 calculation would succeed. PCA rejects nonfinite input or intermediate energy with GPU_TENSOR_NONFINITE, and failed convergence raises GPU_TENSOR_PCA_CONVERGENCE. These checks require stream synchronization even though output arrays remain resident. PCA also holds the device's linear-algebra lock until it completes, so concurrent mmu, inv, chol, lsq and pca calls from other threads on that device wait. For a requested method no fallback occurs inside the library: a caller that wants one retries with method:full when GPU_TENSOR_PCA_RANK is raised, and lets every other error through. The unrequested-method routing above instead falls back to full once on its own when the Gram spectrum is rank-deficient, so zero-energy data still returns zero outputs.

Tensor/table conversion

Pack GPU table columns into a tensor, or scatter a tensor into GPU table columns

.gpu.tensor.fromTable[columns;orientation;table]
.gpu.tensor.toTable[orientation;tensor]
.gpu.tensor.toTableAs[names;orientation;tensor]

Where:

  • table is an unkeyed table;
  • columns is ::, a symbol atom, or a nonempty symbol vector;
  • orientation is :: (the default, equivalent to `features), `features, or `records; and
  • names is a symbol atom for a single output column, or a unique symbol vector with one name per output column.

Every selected input column must be a flat GPU-resident KI, KJ, KE, or KF vector on the current device. All selected columns must have the same exact type and row count; selection order is preserved. There is no implicit promotion. Unselected columns do not participate in validation, so a mixed-residency or mixed-type table can be converted when the selected columns satisfy the contract.

Orientation Tensor shape Tensor-to-table columns
`features [columnCount,rowCount] rows of the tensor (axis 0)
`records [rowCount,columnCount] columns of the tensor (axis 1)

Both fromTable orientations produce canonical row-major tensors. Zero rows are supported; at least one column is required.

The two orientations differ only in physical layout: `records is logically identical to .gpu.tensor.transpose of the `features result, but is physically packed record-major rather than a zero-copy view. Prefer `features (or ::) with transpose where a view suffices — mmu and toTable consume transpose views directly — and use `records when a consumer needs the packed [rowCount,columnCount] buffer itself.

q)gt:.gpu.to ([]a:1 2 3f;b:10 20 30f;c:-1 0 1f)
q)features:.gpu.tensor.fromTable[`c`a;`features;gt]
q).gpu.tensor.shape features
2 3
q)features
-1 0 1
1  2 3
q)(.gpu.tensor.meta features)`axisNames
`c`a
`symbol$()
q).gpu.tensor.toTable[`features;features]
c  a
----
-1 1
0  2
1  3

q)records:.gpu.tensor.fromTable[`c`a;`records;gt]
q).gpu.tensor.shape records
3 2
q)records
-1 1
0  2
1  3

fromTable records selected table column names on the feature axis. toTable reuses those names and reports GPU_INVALID_VALUE if that axis is unnamed, the wrong length, or contains duplicates. Use toTableAs to supply output names explicitly; the override does not mutate tensor metadata.

q)plain:.gpu.tensor.to (1 2f;3 4f;5 6f)
q).gpu.tensor.toTableAs[`bid`ask;`records;plain]
bid ask
-------
1   2
3   4
5   6

The bridge performs asynchronous device-to-device copies: it does not materialize column values on the host. The returned tensor or table owns independent device storage and does not alias its source. Rank-2 row-contiguous tensors and transpose views are supported; arbitrary strided views are rejected.

Axis names follow tensor semantics:

  • transpose swaps the two axes;
  • a shape-identical reshape preserves names, while any changed rank or extent clears them;
  • inv swaps domain and codomain axes;
  • mmu propagates its uncontracted axes;
  • lsq propagates the target-row and predictor-row axes;
  • concat joins the names of its concatenation axis when every input names it, and keeps the names of every other axis, which must agree between the inputs that name them;
  • asc and desc keep the names of every axis except the sorted one, whose names they clear; and
  • gather drops the names of an axis that an integer selects, keeps those of an axis it keeps whole, and names every other axis it selects from with the names at the selected positions.

When both axes of an mmu or lsq contraction are named, the complete symbol vectors must match in value and order. A mismatch reports GPU_AXIS_MISMATCH. If either side is unnamed, contraction is positional, preserving the behavior of tensors created with .gpu.tensor.to.

Note

Table columns and tensors are separate allocation families. The bridge avoids host copies, but it is not zero-copy and its results do not share device buffers with their inputs.

Random tensors

Generate tensor values directly in device memory with cuRAND

A random generator is a separate stateful foreign object. Generated results are ordinary tensors and retain no link to their generator. Rank-2 results can immediately enter the table bridge; nonempty rank-1 or rank-2 results of all four dtypes can enter mmu, and rank-2 results can enter inv, chol, lsq, and PCA subject to their shape requirements. Generator state is explicit and independent of q's \\S seed.

The short constructor creates a Philox generator on the current device:

q)r:.gpu.tensor.random.create 42j
q).gpu.tensor.random.meta r
algorithm | `philox
seed      | 42
offset    | 0
dimensions| ::
ordering  | `best
device    | 0

The dictionary form exposes the creation parameters:

r:.gpu.tensor.random.create
  `algorithm`seed`offset`ordering!(`philox;42j;0j;`best)
s:.gpu.tensor.random.create
  `algorithm`dimensions`offset!(`scrambledSobol64;8j;0j)

Supported algorithms are philox, sobol32, scrambledSobol32, sobol64, and scrambledSobol64. Philox dictionaries require a seed and accept best or legacy ordering. Sobol generators require 1 to 20,000 dimensions and do not accept a seed or an ordering (meta reports their ordering as quasiDefault); their shape must always be dimensions points, matching cuRAND's dimension-major layout.

For Philox, shape is either a long atom (rank-1 shorthand) or a long vector of zero, one, or two items. Zero-sized results do not advance generator state. reset synchronizes the generator's device stream and restores its creation state.

Distribution results are:

Function Result
bits KI for Philox/Sobol32 and KJ for Sobol64; signed q values preserve the raw bit patterns
uniform KE or KF values in [0,1)
normal KE or KF values with finite mean and nonnegative finite standard deviation
lognormal KE or KF; parameters describe the underlying normal distribution
poisson KJ, with 0 < lambda <= 400000
randint Philox-only KJ values in the half-open interval [low,high)

For floating distributions, :: and float select KF; real selects KE. randint combines 64 source bits and uses multiply-high range reduction. It is asynchronous and has at most one extra source value per bucket out of 264; it is not a cryptographic or exact-rejection generator.

Generation is ordered on the module's existing per-device CUDA stream. Assigning a generator to a second q name aliases its state; call create again for independent generator state. With best ordering, exact replay is scoped to the same generator configuration, call sequence, GPU, and CUDA/cuRAND stack. Use legacy when stable Philox output ordering across cuRAND releases is more important than allowing future ordering improvements. The generator's creating device must be current for generation and reset; using it while another device is current reports GPU_WRONG_DEVICE without advancing its state.

to

Send data to the GPU

.gpu.to x

Where x is any CPU, GPU or mixed-resident object. Returns a copy of x with any non-atom moved to the GPU.

All attributes are preserved on .gpu.to, while all but `s are lost on .gpu.from.

For supported vectors with the parted (`p) attribute, .gpu.to validates that each distinct value occupies one contiguous run and builds an index for reuse by .gpu.bin, .gpu.aj, single-key .gpu.wj and .gpu.wj1, and single-key .gpu.lj and .gpu.ij. Index construction is part of the transfer cost. Empty vectors require no index.

If the data does not satisfy the parted condition, .gpu.to clears the attribute on the GPU copy and retains no parted index. Subsequent as-of joins use the temporary sorted-index path, and window joins with equality keys reject the table.

For supported vectors with the unique (`u) attribute, .gpu.to builds the same index, with one row per value, and the GPU copy keeps the `u attribute. Single-key .gpu.lj and .gpu.ij, .gpu.aj and .gpu.bin reuse it. If a value is repeated, .gpu.to clears the attribute on the GPU copy and retains no index. Vectors of types the index does not support, such as nested lists, keep the attribute without an index. Appending to the GPU vector in place clears the attribute and releases the index, because the appended values may repeat a key.

q).gpu.to 10
10
q).gpu.to 10 20 30         / a virtual object; display fetches a copy
10 20 30
q).gpu.type .gpu.to 10 20 30
7h
q).gpu.to `a`b!(10 20;30 40)
a| 10 20
b| 30 40
q).gpu.meta .gpu.to flip `a`b!(10 20;30 40)
c| t f a r
-| ---------
a| j     gpu
b| j     gpu
q).gpu.from .gpu.to flip `a`b!(10 20;30 40)
a  b
-----
10 30
20 40

xto

Send specific columns/keys to the GPU

.gpu.xto[x;y]

Where x is a symbol atom/list specifying keys/columns of a dictionary/table y, returns y with specified non-atoms moved to the GPU.

q).gpu.meta .gpu.xto[`a`c] flip `a`b!(10 20;30 40)   / names not in y are ignored
c| t f a r
-| ---------
a| j     gpu
b| j     cpu
q).gpu.meta .gpu.xto[`a] flip `a`b!(10 20;30 40)
c| t f a r
-| ---------
a| j     gpu
b| j     cpu

type

Type of a GPU-resident object

.gpu.type x

Where x is any CPU or GPU-resident object, returns its type as short.

Vanilla type returns 112h for GPU-resident virtual objects regardless of their true underlying type.

q)A:.gpu.to a:1 2 3
q).gpu.type A
7h
q).gpu.type a
7h
q)type a
7h
q)type A
112h

q)B:.gpu.to b:1 2 3f
q).gpu.type B
9h
q)type B
112h

update

Functional update

The GPU module provides an update function that mirrors the functional-update API ![t;c;b;a], except that t is a table whose columns reside on the GPU.

.gpu.update[t;c;b;a]

.gpu.update takes the same arguments as .gpu.select and supports the same operations. Instead of returning the selected columns, .gpu.update returns the full table, with the columns named in a updated, or added if they do not already exist. Where a where phrase is given, only the matching rows are modified.

q's update ... from T and delete ... from T (and ![T;c;b;a]) are not supported on a GPU table; use .gpu.update.

q)t:([]sym:`AAPL`MSFT`AAPL`MSFT`AAPL;p:150 300 152 298 155;size:100 200 150 300 250)
q)T:.gpu.to t

q)/ double size where sym = `AAPL
q).gpu.update[T;enlist(=;`sym;enlist`AAPL);0b;enlist[`size]!enlist(*;2;`size)]
sym  p   size
-------------
AAPL 150 200
MSFT 300 200
AAPL 152 300
MSFT 298 300
AAPL 155 500

Refer to .gpu.select for the argument details and the list of supported operations.

Type changes are only allowed on a full-column replace

Replacing a whole column may change its type. When a where phrase means only some rows are written, the new values must have the same type as the existing column; safe widening (for example, adding an int to produce values for a long column) is accepted. A mismatch is rejected with GPU_INVALID_STATEMENT.

Symbol and enum representations must match

Writing into an enum column from an enum of a different domain, or into a plain symbol column from an enum column, is rejected with GPU_NOT_IMPLEMENTED, though this is not always checked.

iasc on plain symbol columns may differ from q

As in .gpu.select, iasc over a plain (non-enumerated) symbol column orders by internal symbol pointers rather than the symbol text, so the result may differ from q. Use an enumerated symbol column instead.

ver

Module Version String

q).gpu.ver
"gpulib version: 2.0.0-ada-cuda13.1"

Returns the version string of the module.