The upcoming release of G2CPU v2.1 reduces moving data between an instrument, a disk and the GPU to two calls in your loop. A new feature, zero copy import and export, replaces the chain of buffers, copies and synchronisation that used to sit between an acquisition and the GPU with a single pair of nodes: stage the data on one side, fetch it on the other. The same API runs on Windows, on Linux, and on x64 LabVIEW Real-Time targets with a PXIe GPU installed, and it scales to systems holding more than one GPU — a second GPU is a second set of loops rather than a different API, as Multiple GPUs, and Where It Runs sets out below.

The Starting Point: A Chain of Steps

Getting acquisition data efficiently onto the GPU has never been one operation. The driver fetches into its own buffer. That buffer is copied into a LabVIEW array. The array is handed to a GPU upload. Somewhere in there you also decide how many blocks to keep in flight, how to overlap the upload with the analysis, and how to know that a given block has landed on the device and is safe to use. Writing results back to disk is the same chain in reverse.

All of that is code you write, tune and debug. And every handoff in it is a copy, performed by the CPU at DRAM speed, on the critical path of the loop. Those copies rarely appear in benchmarks, because they sit outside the transfer being measured, but on a streaming system they are frequently the reason a pipeline that should keep up does not.

Nor can any of it run a step at a time. A pipeline that only just keeps up has to overlap everything: the driver filling the next buffer while the current one is copied, the copy running while the previous block uploads, the upload running while the GPU is still working on the block before it. That means asynchronous I/O from end to end — non-blocking driver fetches, asynchronous file reads and writes, several blocks in flight at once, and a thread per stage that can make progress on its own. Each stage needs its own pool of buffers to cycle through, and every boundary between them needs a way to signal that a block is finished and safe to hand on. Get the overlap wrong and the copies stop being the bottleneck only because the serialisation is worse.

Zero Copy 1.0, added in G2CPU 1.6.0 alongside asynchronous DMA, already removed those copies. What it did not remove was the work of orchestrating them. Version 1.0 exposed the mechanics directly: you registered each DMA region with CUDA yourself, decided when that registration happened and what to do with the block that paid for it, issued the upload or download at the right point in the loop, and stood up and managed the worker threads that let a transfer overlap the analysis. Completion tracking — knowing that a given block had actually landed on the device and was safe to touch — was yours as well.

Every one of those decisions was a place to get it wrong. Register a new region inline, in a loop sized for steady state, and the first pass through the driver's buffer pool can overrun a timeout. Upload too eagerly and you block on a transfer that has not finished; too late and the GPU sits idle waiting for data that is already in host memory. Get the worker count or the queue depth wrong and the pipeline either thrashes on synchronisation or leaves the PCIe bus half-used. The building blocks were all there in 1.6.0, but assembling them correctly was a project of its own.

The Upgrade: Stage and Fetch

Version 2.1 collapses the chain into two nodes inside the loop.

Stage Data puts a block into the transfer. Fetch Data takes it out on the other side. In between, G2CPU registers the DMA region with CUDA, queues the work, runs it on asynchronous workers, and guarantees that whatever Fetch Data hands back is complete and safe to use. The buffer management, the overlap and the completion ordering stop being yours to build. Init and Cleanup sit outside the loop and are called once each.

Set against Zero Copy 1.0, that is the whole of the orchestration collapsed into two calls. Registration happens the first time G2CPU sees a region and is cached for every reuse after it, and the one decision that genuinely belongs to you — absorb a new registration inline, or register it and let that block go by — is a single input rather than a mechanism you build. The transfer is issued when Stage Data is called and is complete before Fetch Data returns, so there is no point in your loop where you have to know whether a block is still in flight. The asynchronous workers are created by Init from a worker count you pass once, and the threads they run on are G2CPU's to manage, not yours to start, feed or shut down. What was a subsystem in 1.0 is a queue depth and a worker count in 2.1. Figures 1 and 2 are the same pipeline written both ways.

It is worth being precise about what zero copy means on each side, because the two are not identical.

On the import side there is no host copy at all. The instrument’s driver writes into its own DMA region, that region is registered with CUDA, and the GPU transfers directly out of it. The buffer the hardware filled is the buffer the GPU reads.

On the export side, G2CPU allocates a DMA region and hands it to your file I/O. The region is pinned and registered once when it is created and then reused, so the file API writes into memory the GPU can already reach and the LabVIEW array in the middle disappears. The operating system’s file cache is still in the path; what zero copy removes here is the copy inside LabVIEW, not the one inside the file system. For a bulk transfer that is the difference between one host copy and two, and it is a copy that no longer competes with your analysis for CPU and memory bandwidth.

The PCIe transfer itself remains in both cases, because it must.

Figure 1: Zero Copy 1.0 (G2CPU 1.6.0) — registration, upload and thread management on the diagram

Figure 2: Zero copy in v2.1 (G2CPU 1.7.1) — the same pipeline as two nodes

One Pattern, Four Paths

The feature splits into two categories, and the difference between them is simply who allocates the memory.

  • Import covers the case where the DMA region belongs to something else. An NI driver allocates its own acquisition buffers, and G2CPU takes them as they are and uses them directly.
  • Export covers the opposite case. G2CPU allocates the region and hands it out to another toolchain, whether that is file I/O, TDMS, or your own code, which fills it or drains it in place.

Each category has an upload variant that moves data into GPU memory and a download variant that moves it out, which gives four paths in total:

  • Import upload — a driver buffer goes straight into the GPU. Acquisition from a VST, digitiser or DAQ device.
  • Import download — GPU results go straight into a driver buffer. Generation and playback.
  • Export upload — G2CPU hands out a region, you fill it, the GPU picks it up. File and TDMS reads.
  • Export download — GPU results land in a region you consume in place. File and TDMS writes. 

All four look the same on the block diagram. Init once, Stage Data and Fetch Data inside the loop, Cleanup at the end. Init sets the queue depth and the number of asynchronous workers, and nothing else needs configuring for the common case. The palette is fifteen VIs on one naming pattern, G2CPU <Import|Export> DMA <Upload|Download> - <Init|Stage Data|Fetch Data|Cleanup>, so learning one path is learning all of them. Figure 3 is the whole palette.

Figure 3: The zero copy palette

Three of the four paths, on real block diagrams. The import upload path comes first, because it is the purest case of the four; the two export paths then cover the disk in both directions. The fourth — a driver buffer fed from the GPU, for generation and playback — is the mirror of the first and works the same way.

Streaming a VST Straight Into the GPU

This is the path with no host copy anywhere in it. niRFSA Fetch IQ returns the acquisition buffer as an external data value reference, Import DMA Upload - Stage Data takes that reference directly, and Fetch Data returns the G2CPU array. There is no LabVIEW array in between, nothing to copy, and nothing to synchronise by hand. The GPU reads from the memory the instrument wrote into. The demo behind Figure 4 runs on a PXIe-5840 VST and a PXIe-8881 controller, with an NVIDIA RTX PRO 4000 Blackwell SFF Edition in a RADX Technologies PXIe-GPU-BWRTXPro4kSFF-24GB module, so the instrument and the card that processes its data sit in the same chassis, with no host workstation between them. It runs at the VST's full rate, in 32 MS blocks: 1.25 GS/s of complex IQ — 1.25 GHz of bandwidth, arriving as 5 GB/s of data, close to what the PXIe backplane can carry. At that rate a single host copy is not a tuning detail — it is the difference between a pipeline that sustains the acquisition and one that falls behind it.

One limit is worth stating plainly, because it decides how the first few iterations behave. Registering a DMA region CUDA has not seen before is slow — slow enough that, paid inline in a loop sized for steady state, it can push a Stage call past its timeout and break an acquisition. Drivers cycle through a fixed set of buffers, so this is a startup cost that amortises to nothing after the first pass through the pool, but it is real while it lasts. The Upload on New DMA Registration input puts that decision in your hands rather than hiding it: absorb the registration inline, or register the region and let that block go by so the acquisition keeps its cadence.

Figure 4: Streaming from a VST directly into GPU memory

Reading a File Straight Into the GPU

Export DMA Upload turns a file read into a GPU load. Stage Data hands you a DMA region wrapped in a data value reference, TDMS Advanced Asynchronous Read (Data Ref) writes into it, and releasing the reference starts the upload. Release is LabVIEW’s own primitive rather than a G2CPU call, so there is no extra node in the loop, and the point at which the buffer stops being yours to write is the point LabVIEW already understands.

Staging and fetching are decoupled by the queue, so the read of the next block overlaps the upload of the current one without you arranging it. The file system, the PCIe bus and the GPU stay busy at the same time — Figure 5 is the whole loop.

Figure 5: Reading a file directly into GPU memory

Writing GPU Results Straight to Disk

Export DMA Download is the mirror image. Stage Data takes the G2CPU array, Fetch Data returns a data value reference over the downloaded block, and TDMS Advanced Asynchronous Write (Data Ref) writes that block from where it already sits. No result array is copied into a separate file buffer on the way out.

This is the path for continuous recording, where the write loop has to keep pace with the analysis loop indefinitely. Every download VI releases the G2CPU array passed into it, so device memory comes back as soon as the data has left the GPU rather than at the end of the iteration. Figure 6 is that loop on the diagram.

Figure 6: Writing GPU results directly to disk

Multiple GPUs, and Where It Runs

The device ID is associated with the while loop the pipeline runs in, and Init, Stage Data and Fetch Data all take it from there — there is no device input to wire on any of them. That ID determines where the helper loops run and where the resulting array lives, so a second GPU is a second loop rather than a different API. Two acquisitions can feed two cards independently, each in its own loop, on the same block diagram. Because the ID follows the loop, every call in a given pipeline is on the same device by construction.

The API is identical on Windows, on Linux, and on x64 LabVIEW Real-Time targets with a PXIe GPU such as the RADX Technologies PXIe-GPU modules. Code written against one moves to the others unchanged.

Why a GPU

Streaming acquisition is often treated as an FPGA problem, and for fixed, narrow processing it still is. The difference shows up when the analysis gets large. A GPU pipeline built this way keeps up with the stream at either end of the range: 300 FFTs per second at a four-million-point transform size (Figure 7), and 156 million per second at eight points (Figure 8), both on the same 1.25 GS/s (5GB/s) acquisition and both batched rather than run one transform at a time — with the algorithm expressed in LabVIEW rather than compiled into fabric. Both numbers were measured on a PXIe-5840 VST and a PXIe-8881 controller, with the RADX PXIe-GPU module doing the maths. And the comparison is closer than it looks on the bench: the card sits in the same chassis as the instrument, so choosing CUDA over fabric no longer means moving the processing out to a host workstation. Zero copy is what keeps the data path from becoming the limit before the maths does.

Figure 7: Four-million-point FFTs at 300 FFT/s, sustained on a 1.25 GS/s acquisition

Figure 8: Eight-point FFTs at 156 MFFT/s — the same pipeline, the same 1.25 GS/s stream

How to Get Ready

Zero copy import and export slots into an existing asynchronous DMA pipeline without restructuring it. Update to v2.1, replace the transfer at the boundary that costs you most, and leave the rest of the diagram alone. If your data comes from an NI driver, start with the import path. If it comes from or goes to disk, start with export. The built-in benchmarking tools will confirm what the change bought you.

The requirements are modest: LabVIEW 2020 or later, 64-bit only, with CUDA 12.8 and ArrayFire 3.10 installed on the target. If you are on G2CPU v1.7.1 today, v2.1 is an update rather than a new purchase: the same licence carries over, with nothing to repurchase.

Why This Matters

The copies removed are only half of it. The other half is the code that no longer exists: the intermediate arrays, the buffer pools, the overlap logic and the synchronisation that every high-rate LabVIEW GPU application used to grow its own version of. In v2.1 that is two nodes in the loop, on four paths that all look alike, on every platform LabVIEW runs a GPU on.

G2CPU v2.1 will be available in the coming weeks.

Want this on your own bench? Talk to RADX Technologies. Every demo in this post runs on one of their in-chassis PXIe-GPU modules: 1.25 GHz of bandwidth straight into CUDA, with no host workstation anywhere in the path. Drop one into your PXIe chassis, update G2CPU, and the pipeline you have just read about is two nodes away.