GPUs wait on CPUs to fetch their data from SSDs, and the CPU software stack — not the flash — is the bottleneck; BaM maps NVMe queues and doorbells straight into GPU memory so a hundred thousand GPU threads can each demand their own 512-byte reads, hitting 45.8 million IOPs and matching a host-DRAM system that costs 21.7× more.
The argument in one glance
-
Problem
Terabyte datasets, an 80-gigabyte accelerator
Graph analytics, graph neural networks, and recommender systems want GPU compute over arrays spanning tens of gigabytes to tens of terabytes — with fine-grained, data-dependent access (which neighbor list you need depends on the traversal). GPU memory, despite a 53× improvement since 2006, caps at 80 GB.
-
What people did
Let the CPU feed the GPU, or buy enough DRAM
CPU-centric designs either pre-tile: the CPU partitions the dataset and pipelines chunks into GPU memory around kernel launches — or fault: memory-mapped files trigger GPU page faults serviced by the CPU’s handler. DRAM-only designs hold everything in host memory (128 GB–2 TB) or pool many GPUs’ memories.
-
Why it fell short
The CPU software stack is the bottleneck; DRAM is the wrong price
Tiling fetches unneeded bytes (I/O amplification) and synchronizes CPU and GPU around every chunk; fault-driven paths funnel a hundred thousand threads through a nearly-serial page-fault handler — prior GPU-initiated art managed ~823K IOPs on an A100. Even io_uring loses up to 36.4% of access latency to kernel software. And DRAM at tens of TB is absurd: a 10 TB pool needs 125 A100s, and preloading wastes it on bytes never touched.
-
This paper's idea
Move the storage control path onto the GPU
BaM (Big accelerator Memory) places NVMe submission/completion queues and I/O buffers in GPU memory, maps SSD doorbell registers into the CUDA address space, and gives kernels an mmap-like bam::array backed by a highly concurrent software cache. GPU threads request exactly the bytes they need, when they need them — no CPU in the loop. A prototype with 10 SSDs reaches 45.8M random-read IOPs (90% of PCIe Gen4 ×16), matches a 21.7×-more-expensive DRAM-only system on graph analytics, and beats CPU-initiated data analytics by 5.3×.
Why this problem is worth solving
The memory-capacity wall for accelerators is not a niche inconvenience — it decides which workloads GPUs can serve at all. Graphs with hundreds of millions of nodes, embedding tables for recommenders, GNN feature stores: all are arrays of tens of GB to tens of TB, accessed in patterns computed at runtime (the traversal decides which neighbor list is next). Against that, an A100 offers 80 GB.
The conventional escape routes each fail a different way. Host
DRAM costs more per GB than flash by an order of magnitude and
must be pre-loaded — for data-dependent workloads, much of what
is loaded is never touched. CPU-orchestrated tiling turns the
CPU into a data butler: it cannot know which bytes the GPU will
need, so it ships whole partitions (
fetching more bytes than the computation actually uses — e.g.,
reading a 128 KB block to consume one 512-byte neighbor
list
The quiet enabler is that storage hardware outgrew its software.
NVMe SSDs are simple queue machines —
submission queues (SQ) where software
writes commands, completion queues (CQ) where the controller posts
results, and doorbell registers that signal new work — all just
memory operations
What people did before, and where it broke
Proactive tiling. CPU code partitions the dataset, pipelines chunks to the GPU, launches kernels per chunk. Works for regular access; for data-dependent workloads it amplifies I/O (the CPU cannot predict need), multiplies kernel launches, and burns programmer effort on partition/overlap logic.
Memory-mapped files with GPU page faults. Elegant abstraction, serial reality: faults traverse CPU driver and OS machinery whose thread-level parallelism is orders of magnitude below the GPU’s request rate.
GPUfs / ActivePointers. Pioneering GPU-side file abstractions — but data transfers still route through CPU software, capping miss-handling throughput at a level BaM exceeds by 20.7× even while ActivePointers serves misses from fast CPU DRAM and BaM from actual SSDs.
NVIDIA GPUDirect Storage (GDS). Moves the data path peer-to-peer (SSD → GPU memory, no host bounce) but keeps the control path on the CPU — fine for large sequential transfers, collapsing at fine granularity: BaM saturates ~25 GB/s at 4 KB blocks where the GDS/CPU stack cannot.
The BaM system architecture
-
bam::array abstraction new
An mmap-like array over storage: GPU code indexes it as ordinary memory (val = array[tid]). The abstraction computes the cache-line offset; per-warp coalescing deduplicates — one thread probes the cache per unique line on behalf of its warp.
-
GPU software cache new
All virtual and physical memory pre-allocated at startup — no OS memory management, no legacy critical sections; a lock exists only for line insertion/eviction. Coalesces concurrent misses to the same line into one storage request, exploits the GPU’s enormous memory and atomics bandwidth, and lets applications configure line size (512 B for scattered graph access, larger for locality-rich workloads).
-
User-level GPU I/O stack new
NVMe SQ/CQ pairs and I/O buffers live in GPU memory; SSD doorbell registers are mapped into the CUDA address space (custom Linux driver + GPUDirect RDMA/Async). Thousands of threads enqueue concurrently via a ticket/turn protocol with minimal critical sections and batched doorbell rings.
-
Peer-to-peer data path new
The SSD controller fetches commands from GPU-resident queues and DMAs data directly into GPU memory — the CPU is on neither the control nor the data path. The design is storage-agnostic: Optane, enterprise flash, or consumer NVMe.
-
Prototype
Off-the-shelf everything: A100-80GB, a PCIe expansion chassis holding up to 10 SSDs, a custom (SmartIO-style) driver. No new hardware — the contribution is the software architecture the existing hardware was waiting for.
The idea
Latency tolerance is a parallelism budget — and the GPU is rich. The foundational arithmetic is Little’s Law: throughput × latency = required outstanding requests. Fifty-one million 512-byte accesses per second against Optane’s 11 µs needs 561 requests in flight; against a consumer SSD’s 324 µs, 16,524. For a CPU-driven control path those are absurd numbers; for a GPU running hundreds of thousands of threads, they are a rounding error — if the software lets threads issue requests without serializing. BaM’s whole design flows from this: don’t reduce storage latency, hide it under thread parallelism, which the GPU has in three orders of magnitude more than the CPU.
Queues that thousands of threads can share. The NVMe protocol was designed for a few CPU cores; its doorbell registers are write-only and must be updated with monotonic tails — a natural critical section that would strangle GPU-scale concurrency. BaM’s queue protocol dissolves it: an atomic ticket counter assigns each thread a slot in a virtual queue (slot = ticket mod queue size, turn = ticket div queue size); a turn_counter array lets each physical slot act as an ordered sub-queue; threads copy commands into their slots in parallel and set bits in a mark bit-vector; one winning thread takes the lock, advances the tail past all consecutively marked entries, and rings the doorbell once for the whole batch. Completion handling mirrors this, lock-free polling included. The result: enqueue/dequeue at GPU thread counts with critical sections measured in a handful of instructions — the difference between 823 K and 45.8 M IOPs.
A cache built for a million concurrent probes. OS memory management supports every legacy need and pays in critical sections. BaM’s cache pre-allocates everything at startup, reduces locking to line insertion/eviction, and puts the GPU’s strengths — massive memory bandwidth, huge atomic throughput — to work on metadata. Warp-level coalescing collapses duplicate probes; miss coalescing collapses duplicate storage requests. Hot data serves at 430 GB/s — GPU-memory speeds, 11.2× ActivePointers’ cache.
Fine granularity is the point, not a detail. Configurable cache lines down to 512 bytes mean a graph traversal fetches a neighbor list, not the megabyte around it. This is where the economics flip: data-dependent workloads on tiled or big-block systems drown in amplification; BaM’s on-demand 512-byte reads cut traffic so much that four SSDs at PCIe bandwidth keep up with host DRAM on end-to-end graph analytics — because the DRAM system must first spend minutes loading files it may barely use.
One neighbor-list read, from GPU thread to flash and back
SQ/CQ = NVMe submission/completion queues (resident in GPU memory); doorbell = SSD register signaling new work (mapped into CUDA address space). Cache line = 512 B here. The walkthrough follows Figure 2's numbered path with the queue protocol of §3.4.
Step 1 of 6
A thread indexes the array
A graph-traversal thread needs node 87,234,101’s neighbor list: val = array[offset]. The bam::array abstraction converts the index to a cache-line offset. Its warp-mates needing the same line coalesce — one thread probes the cache for all of them.
bam::array: mmap-like, storage-backed warp coalescer: 1 probe per unique line
Why it works
- Storage latency × target throughput = required in-flight requests (Little’s Law) → the GPU’s thread count covers even consumer-SSD latencies effortlessly → latency tolerance comes from parallelism the accelerator already has, not from faster (pricier) media.
- The CPU contributed nothing essential to the control path → moving queues, buffers, and doorbells into GPU space deletes the page-fault handler, kernel crossings (36.4% of latency under io_uring), and CPU-GPU synchronization in one stroke.
- NVMe’s serialization points (tails, doorbells) are batchable → the ticket/turn/mark protocol lets command-copying parallelize and reduces the critical section to a tail bump plus one doorbell per batch → queue throughput scales to GPU thread counts.
- On-demand 512-byte access eliminates amplification → four SSDs of PCIe bandwidth outperform pre-loading into host DRAM end-to-end → capacity moves to flash at 4.3–21.8× lower cost per GB with performance parity.
- Everything is off-the-shelf (A100 features, NVMe spec, a small driver) → the design is a software architecture, adoptable without new silicon — and storage-medium-agnostic, so cost/performance is a dial (consumer 980pro: 2.7–3.2× slower than Optane, far cheaper).
What it costs
- GPU memory spent on infrastructure. Queues, I/O buffers, and the software cache all live in the GPU’s scarce 80 GB.
- Cache logic in every access. Software cache probes and queue protocol instructions tax each miss; the design amortizes but cannot eliminate them.
- Trust model. User-level access to raw NVMe queues bypasses kernel file-system enforcement; the paper points to emerging frameworks for securing accelerator-centric storage rather than solving isolation itself.
- Ordering subtleties. GPUDirect RDMA write ordering over PCIe requires care (a read must follow writes to guarantee visibility) — handled, but the kind of detail that makes such systems delicate.
- Read-centric evaluation. Graph/data analytics are read-heavy; writes reach 10.6 M IOPs but endurance and write-path behavior under sustained load get less attention.
The evaluation
The prototype is deliberately mundane hardware: an A100-80GB, dual EPYC host, and a PCIe expansion chassis with up to 10 SSDs of three classes (Intel Optane P5800X, Samsung enterprise, Samsung 980pro consumer). Workloads: microbenchmarks, BFS and connected-components graph analytics on five large graphs (GAP-kron, GAP-urand, Friendster, MOLIERE_2016, uk-2007-05), and I/O-bound data analytics.
Feeding a GPU from storage: four architectures
| Property | CPU-centric (tiling / faults) | GDS (P2P data, CPU control) | DRAM-only (host memory) | BaM |
|---|---|---|---|---|
| Control path | ||||
| Fine-grained throughput | Sub-million IOPs | DRAM-speed once loaded | ||
| End-to-end applications | — | Reference for graph analytics (with file-load time included) | ||
| Media flexibility | — | — | DRAM only — a 10 TB pool would need 125 A100s |
Check your understanding
1. BaM makes no attempt to reduce SSD latency (11–324 µs, thousands of times slower than HBM). Predict why the system still saturates the storage.
2. NVMe doorbells are write-only registers requiring monotonically newer tail values — a textbook critical section. How does BaM let thousands of threads enqueue without strangling on that lock?
3. Four SSDs delivering ~25 GB/s match a host-DRAM system with far higher bandwidth on end-to-end BFS (1.00×) and beat it on CC (1.49×). What two effects close the bandwidth gap?
4. BaM’s software cache pre-allocates all its memory at startup instead of using OS-style dynamic memory management. What does this buy at GPU scale?