KDB-X GPU Acceleration v3.0: Same Q Code, Run On Different Hardware

Nicholas Sansone

Author

Nicholas Sansone

Developer Relations Engineer

Key Takeaways

  1. KDB-X GPU Acceleration introduces virtual GPU tables for supported qSQL operations.
  2. Developers can run the same qSQL queries against CPU- or GPU-resident data.
  3. Virtual tables reduce the need to maintain separate CPU and GPU query definitions.
  4. Three benchmark workloads ran 14.5x, 92.8x, and 449x faster on an NVIDIA H100 than on the CPU baseline.
  5. GPU performance depends on the workload, data shape, transfer costs, and how long data stays on the device.

The KDB-X GPU module version 3.0 changes the way GPU execution fits into a q application. Earlier versions gave developers GPU implementations of familiar operations (see previous blogs in the series: Introducing GPU Acceleration and What’s new in KDB-X GPU Acceleration), but using them often meant maintaining a separate functional query written specifically for the device. By using virtual tables, that split becomes much smaller: write the q expression once, then point it at CPU- or GPU-resident data. There is no code rewrite, no refactoring required – just decide which tables live where.

This allows GPU execution to fit into an existing code base. A research workflow can keep a CPU path for smaller datasets, and move larger runs to GPU. A production pipeline can retain data on device across a sequence of supported operations. Tests can run the same query definitions against both paths and compare the results directly.

This post walks through that change using three workloads: a group VWAP over 100 million trades, an as-of join between 4 million trades and 100 million quotes, and a correlation matrix over 500,000 observations. Each workload is defined once, run on CPU, then run unchanged on an NVIDIA H100.

From a GPU-Specific Query to Standard qSQL

Before v3.0, a GPU select statement was written using the module’s explicit functional API. A typical grouped VWAP with a symbol filter looked like this:

q
.gpu: use`kx.gpu;

T:.gpu.to t;

.gpu.select[T;enlist(=;sym;enlist`AAPL);`sym`bar!`sym`bar;(enlist`vwap)!enlist(wavg;`size;`price)]

The operation is recognizable, but standard qSQL has to be converted into parse-tree arguments. The where clause, grouping columns and aggregation each need to be expressed in functional form. If the CPU path uses ordinary qSQL, the application now has two versions of the same logical query to maintain.

Version 3.0 introduces virtual GPU tables that can be manipulated in the same way as an ordinary q table:

q
.gpu:use`kx.gpu;

T:.gpu.to t;

.gpu.from select vwap:size wavg price by sym,bar from T where sym=`AAPL

The query is the same qSQL a developer would write for an in-memory CPU table, but a virtual table abstraction runs the query directly on device. This is an important difference.

Before Version 3.0
Query form Functional select .gpu.select[T;c;b;a] Standard qSQL
select xxx from xxx
CPU-to-GPU port Rewrite the statement in functional form Write the statement directly against a GPU virtual table
Filters, groups and aggregates Build functional parse trees Write ordinary where, by and aggregate expressions
Result location On device until .gpu.from On device until .gpu.from
Application paths Separate CPU and GPU query definitions One definition, different data bindings

Virtual tables do not make CPU and GPU memory interchangeable – moving the data still requires explicit .gpu.to/.gpu.from commands, however, the same calculations no longer need to be expressed in two different forms.

Defining the Workloads Once

The benchmark defines each workload once, using the exact same analytical expression for both CPU and GPU execution:

q
// Same workload across both paths

system"S 7";  // Seed

ns:500;       // Num symbols
N:100000000;  // Num trades for VWAP
nt:4000000;   // Num trades for as-of join
nq:100000000; // Num quotes
n:500000;     // Num observations for the correlation
m:200;        // Num series for the correlation

syms:`$"S",/:string til ns;
day:2026.10.20D09:30;

// Trade table
t:([]sym:N?syms;time:day+asc N?`long$6.5*60*60*1e9;price:100+N?50f;size:1+N?1000);

// 5 minute bar
t:update bar:5 xbar `minute$time from t;

// aj trade table sorted by time
tr:`time xasc ([]sym:nt?syms;time:day+nt?`long$6.5*60*60*1e9;qty:nt?100);

// Quote table
qu:([]sym:`g#nq?syms;time:day+asc nq?`long$6.5*60*60*1e9;bid:100+nq?50f;ask:150+nq?50f);

// Apply attributes to our tables
tp:update `p#sym from `sym xasc t;
qp:update `p#sym from `sym xasc qu;

// Correlation block of random
x:(n;m)#(n*m)?1f;

// vwap
select vwap:size wavg price by sym,bar from tp;

// aj
select last bid from aj[`sym`time;tr;qp];

// For our tensor operations, we don't overload the .q namespace so we need to handle for it – 
// this requires loading in the GPU module prior to function definition as some operations 
// (mmu, flip, avg, sqrt) have a tensor equivalent:

.gpu:use`kx.gpu;

// correlation
sub:{$[112h=type x;.gpu.tensor.sub[x;y];x-\:y]};	// subtraction
mul:{$[112h=type x;.gpu.tensor.mul[x;y];x*y]};	// multiplication
dve:{$[112h=type x;.gpu.tensor.div[x;y];x%\:y]};	//  division

.q.mmu:{$[112h=type x;.gpu.tensor.mmu[x;y];x$y]}; // matrix multiplication

.q.flip:{$[112h=type x;.gpu.tensor.transpose x;flip x]};

.q.avg:{$[112h=type x;.gpu.tensor.avg[0;0b] x;avg x]};

.q.sqrt:{$[112h=type x;.gpu.tensor.sqrt x;sqrt x]};

qcor:{
  Z:sub[x;.q.avg x];
  S:.q.sqrt avg mul[Z;Z];
  Zs:dve[Z;S];
  dve[.q.mmu[.q.flip Zs; Zs];n*1f]
  };

This data is defined at the start and then prepared for execution on each processor. For CPU runs, tp, tr, and qp are ordinary q tables, while x is a q matrix.

For the GPU path, the same names are rebound to device-resident objects where a capital letter is used to differentiate the GPU-resident table from their in-memory counterparts. Note that only the grouped attribute is supported, therefore we are using the non-parted tables:

q
.gpu:use`kx.gpu;

TP:.gpu.to t;
TR:.gpu.to tr;
QP:.gpu.to qu;
X:.gpu.tensor.to x

The point of emphasis here is that regardless of whether the table lives on CPU vs GPU, the application’s analytical logic remains unchanged. We run the same qSQL statements and same qcor function, regardless of where the data is staged.

Three Workloads on an NVIDIA H100

The benchmark used an NVIDIA H100 80GB HBM3 for GPU evaluation and runs q with 16 secondary threads for the CPU baseline. Each function was called once as a warm-up, and a second time for execution timing. Reported GPU timings include bringing the result back from device with .gpu.from or .gpu.tensor.from, but exclude the one-off push of data to VRAM.

Grouped VWAP Over 100 Million Trades

The first workload calculated VWAP by symbol and five-minute bar across 100 million trades and 500 symbols – the CPU vs GPU runs execute the same statement:

q
// CPU run
q)\t select vwap:size wavg price by sym,bar from tp
679.8772

// GPU run (table already staged on device as TP)
q)\t .gpu.from select vwap:size wavg price by sym,bar from TP
46.973

CPU completed the query in ~679.9ms, whereas the H100 ran in ~47.0ms, a 14.5x speedup.

As-Of Join of 4 Million Trades and 100 Million Quotes

The second workload joins 4 million trades to the most recent quote from a 100-million-row quote table – the query run remains unchanged across both paths:

q
// CPU run
q)\t select last bid from aj[`sym`time;tr;qp]
164.1064

// GPU run (tables already staged on device as TR/QP)
q)\t .gpu.from select last bid from aj[`sym`time;TR;QP]
1.769262

CPU run in ~164.1ms with GPU in ~1.77ms, a 92.8x speedup. This is where virtual table dispatch becomes especially apparent – as-of joins are native to the way q developers express time-series alignment, and GPU module v3.0 preserves these existing definitions rather than previously requiring a rewrite in functional form.

Correlation Across 200 Series

This is where things get interesting. The third workload computed a 200-by-200 correlation matrix from 500,000 observations. Our benchmark function centers and normalizes the data, then uses matrix multiplication to build the result:

q
// CPU run
q)\t qcor x
5387.774

// GPU run (matrix already staged on device as X)
q)\t .gpu.tensor.from qcor X
11.99978

CPU executes this in ~5,387.8ms, whereas the H100 completed in ~12.0ms – 449x times faster on GPU than CPU. The size and shape of this workload gives the GPU substantially more parallel work than the table queries, making this type of work suitable for GPU acceleration. This is also a good example of why keeping intermediate values resident on device is important: the centered matrix, standard deviations, and normalized matrix remain GPU-resident until the final correlation matrix is returned.

Results

Query CPU time (ms) GPU time (ms) Speedup
VWAP 679.877211 46.973001 14.5x
As-of join 164.106428 1.769262 92.8x
Correlation matrix 5387.774199 11.999776 449.0x

The actual data returned from both calls was also compared before reporting the timings. The as-of join matched exactly; the maximum absolute differences for VWAP and correlation were approximately 7.7e-13 and 8.0e-14, respectively – within the expected range for floating-point execution differences across different hardware.

This is not all to claim that every q query becomes hundreds of times faster simply by moving a table. GPU performance depends heavily on the operation, data shape, transfer cost, and how long the data remains resident on device. This comparison does show the range of acceleration when a workload is highly parallel and the execution path avoids repeated host-to-device movement.

What Virtual Tables Mean for Application Design

Version 3.0 reduces code divergence between CPU and GPU execution. In the matrix multiplication benchmark, code ran over 400 times faster.

Integrating GPU acceleration previously meant identifying the expensive path, translating into GPU functional API form and maintaining that definition beside the CPU logic – but with virtual tables, the boundary moves closer to data placement:

  1. Define the analytic logic in typical q
  2. Decide which data should be GPU-resident
  3. Bind the query to the CPU or GPU object
  4. Return only the result that the downstream path needs

CPU and GPU execution no longer need to become separate application paths. The data location changes, while the query stays the same.

There are still architectural decisions that need to be made; moving data to VRAM has a cost, unsupported work remains on CPU, and small queries may not contain enough parallel work to justify device execution. What version 3.0 does not do is remove these tradeoffs. What it does is remove the need to maintain two different versions of the same analytical logic.

Read the KDB-X GPU Acceleration overview or watch the GPU Acceleration webinar.

Explore the GPU Acceleration tutorials and see the real-time TCA example.

Demo the world’s fastest database for vector, time-series, and real-time analytics

Start your journey to becoming an AI-first enterprise with 100x* more performant data and MLOps pipelines.

  • Process data at unmatched speed and scale
  • Build high-performance data-driven applications
  • Turbocharge analytics tools in the cloud, on premise, or at the edge

*Based on time-series queries running in real-world use cases on customer environments.

Book a demo with an expert

"*" indicates required fields

Step 1 of 2

This field is for validation purposes and should be left unchanged.


// social // social