GPU-Resident Queue API#
The uPCIe CUDA backend supports GPU-resident NVMe queue pairs via this API. A GPU-resident queue pair lives entirely in CUDA device memory and can be passed as a kernel argument so that CUDA threads submit and reap NVMe commands without any host involvement after launch.
Note
This API is experimental and may change without notice.
API Overview#
Symbol |
Where |
Description |
|---|---|---|
|
Host (C/C++) |
Allocate a GPU-resident queue pair and register it with the NVMe controller |
|
Host (C/C++) |
Deregister the queue pair and free device memory |
|
Device (CUDA kernel) |
Submit one NVMe command per thread and reap its completion cooperatively |
struct xnvme_cuda_queue is an opaque type — its internals are not part of the
public API. xnvme_cuda_queue_create() allocates and initializes the queue
pair in CUDA device memory, so the returned pointer can be passed directly to a
kernel without any additional copy. Its opts take XNVME_QUEUE_SQ_HOSTMEM
to place the submission queue in host memory instead; the other queue options
do not apply to a GPU-issued queue. This is in contrast to the command array, which
is prepared on the host and must be copied to device memory with
cudaMemcpy() before the kernel launch.
Host-Side Setup#
Open a device on the upcie-cuda backend, allocate a data buffer, and
prepare one xnvme_spec_sqe per thread on the host. PRP entries require
physical addresses, so use xnvme_buf_vtophys
to resolve them before copying the commands to device memory and launching the
kernel:
int
main(int argc, char **argv)
{
struct xnvme_opts opts = {0};
struct xnvme_dev *dev = NULL;
union xnvme_spec_sqe *h_cmds = NULL, *d_cmds = NULL;
struct xnvme_cuda_queue *gpu_queue = NULL;
uint64_t phys;
uint32_t nsid;
size_t qdepth, lba_nbytes;
char *buf = NULL;
int *h_errors = NULL, *d_errors = NULL;
cudaError_t cerr;
int err = 0;
xnvme_opts_set_defaults(&opts);
if (argc < 2) {
err = -EINVAL;
xnvme_cli_perr("Usage: %s <pci-id>", err);
return err;
}
opts.be = "upcie-cuda";
dev = xnvme_dev_open(argv[1], &opts);
if (!dev) {
err = -errno;
xnvme_cli_perr("xnvme_dev_open()", err);
return err;
}
/*
* qdepth is both the CUDA block dimension and the NVMe queue depth.
* Pass it as batch_size to the kernel to make all threads active.
* To submit a partial batch, pass a smaller value as batch_size;
* threads with tid >= batch_size participate in barriers but do not
* submit or reap a command.
*/
qdepth = 64;
lba_nbytes = xnvme_dev_get_geo(dev)->lba_nbytes;
nsid = xnvme_dev_get_nsid(dev);
buf = (char *)xnvme_buf_alloc(dev, qdepth * lba_nbytes);
if (!buf) {
err = -errno;
xnvme_cli_perr("xnvme_buf_alloc()", err);
goto exit;
}
/* Prepare one command per thread with physical PRP addresses */
h_cmds = (union xnvme_spec_sqe *)calloc(qdepth, sizeof(*h_cmds));
if (!h_cmds) {
err = -errno;
xnvme_cli_perr("calloc(h_cmds)", err);
goto exit;
}
for (size_t i = 0; i < qdepth; i++) {
xnvme_buf_vtophys(dev, buf + i * lba_nbytes, &phys);
h_cmds[i].common.opcode = XNVME_SPEC_NVM_OPC_READ;
h_cmds[i].nvm.slba = i;
h_cmds[i].common.nsid = nsid;
h_cmds[i].common.dptr.prp.prp1 = phys;
h_cmds[i].nvm.nlb = 0; /* nlb is zero-based; 0 means 1 LBA */
}
/* Copy commands to device memory before passing to the kernel */
cerr = cudaMalloc((void **)&d_cmds, qdepth * sizeof(*d_cmds));
if (cerr) {
xnvme_cli_perr("cudaMalloc(d_cmds), cudaError_t: %d", cerr);
err = -ENOMEM;
goto exit;
}
cudaMemcpy(d_cmds, h_cmds, qdepth * sizeof(*d_cmds), cudaMemcpyHostToDevice);
/* Allocate a per-thread error buffer so the kernel can report failures */
cerr = cudaMalloc((void **)&d_errors, qdepth * sizeof(int));
if (cerr) {
xnvme_cli_perr("cudaMalloc(d_errors), cudaError_t: %d", cerr);
err = -ENOMEM;
goto exit;
}
cudaMemset(d_errors, 0, qdepth * sizeof(int));
err = xnvme_cuda_queue_create(dev, qdepth, 0, &gpu_queue);
if (err) {
xnvme_cli_perr("xnvme_cuda_queue_create()", err);
goto exit;
}
xnvme_cuda_io_kernel<<<1, qdepth>>>(gpu_queue, d_cmds, qdepth, d_errors);
cerr = cudaGetLastError();
if (cerr != cudaSuccess) {
fprintf(stderr, "kernel launch failed: %s\n", cudaGetErrorString(cerr));
err = -EIO;
goto exit;
}
cerr = cudaDeviceSynchronize();
if (cerr != cudaSuccess) {
fprintf(stderr, "kernel failed: %s\n", cudaGetErrorString(cerr));
err = -EIO;
goto exit;
}
h_errors = (int *)calloc(qdepth, sizeof(int));
if (!h_errors) {
err = -errno;
xnvme_cli_perr("calloc(h_errors)", err);
goto exit;
}
cudaMemcpy(h_errors, d_errors, qdepth * sizeof(int), cudaMemcpyDeviceToHost);
for (size_t i = 0; i < qdepth; i++) {
if (h_errors[i]) {
fprintf(stderr, "thread %zu failed: %d\n", i, h_errors[i]);
err = h_errors[i];
}
}
if (!err) {
printf("OK: %zu GPU-submitted NVMe reads completed successfully\n", qdepth);
}
exit:
if (gpu_queue) {
xnvme_cuda_queue_destroy(dev, gpu_queue);
}
cudaFree(d_errors);
cudaFree(d_cmds);
free(h_errors);
free(h_cmds);
xnvme_buf_free(dev, buf);
xnvme_dev_close(dev);
return err;
}
Device-Side Dispatch#
Inside a CUDA kernel, use xnvme_cuda_cmd_io() to submit one NVMe command
per active thread. The commands are fully prepared on the host, so the kernel
only needs to index into the command array and call the helper.
__global__ static void
xnvme_cuda_io_kernel(struct xnvme_cuda_queue *qp, union xnvme_spec_sqe *cmds, size_t batch_size,
int *errors)
{
size_t tid = threadIdx.x;
/* kernels cannot return values to the host directly, populate err array */
errors[tid] = xnvme_cuda_cmd_io(qp, &cmds[tid], tid, batch_size);
}
The queue depth passed to xnvme_cuda_queue_create() must equal the CUDA
block dimension. The batch_size argument to xnvme_cuda_cmd_io() controls
how many threads actively submit commands and can be anything up to the queue
depth. Threads with tid >= batch_size participate in the required barriers
but do not submit or reap a command.