Skip to content
npcooleyPublic

About

Infrastructure for calling CUDA from R

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

18 Commits

Folders and files

Repository files navigation

Alternative Compute Framework: CUDA 0.1.1

Nicholas Cooley 2026-06-02

Infrastructure for calling NVIDIA’s CUDA framework from R

Lifecycle: deprecated

Introduction

This package is no longer under development. The capabilities presented here are now supported in ardea, which has recently been submitted to CRAN.

A brief introduction to CUDA, and why access to CUDA in R shouldn’t be as hard as it is now…

Brief CUDA API introduction

There is a split between the runtime and the driver APIs, and that distinction is both confusing and important.

General design philosophy

ACFcuda itself is relatively bare on R functions, with an emphasis of providing C level functions that can be used to divert intensive compute steps to non-CPU hardware. Library and function compilation, device discovery, and context construction are exposed in R; while general buffer management and function dispatch are only exposed in C. A simple R wrapper is present, but it is almost solely overhead checking to ensure argument compliance.

Users should expect to interact with CUDA devices through R’s .Call with an emphasis being placed on maintability and reliability for the time being.

Example

A brief code example for package usage is included below.

# write out a kernel function
cuda_mm_naive <- '
#include <stdint.h>

extern "C" __global__ void cuda_mm_naive(float* output,
                                         const uint32_t M,
                                         const uint32_t K,
                                         const uint32_t N,
                                         const float* A,
                                         const float* B)
{
    uint32_t row = blockIdx.x * blockDim.x + threadIdx.x;
    uint32_t col = blockIdx.y * blockDim.y + threadIdx.y;

    if (row < M && col < N) {
        float sum = 0.0f;
        for (uint32_t k = 0; k < K; k++) {
            sum += A[row + k * M] * B[k + col * K];
        }
        output[row + col * M] = sum;
    }
}
'

# write out our kernel function to a temporary file
# and compile it to a library
tmp01 <- tempfile(fileext = ".cu")
writeLines(text = cuda_mm_naive,
           con = tmp01)
tmp02 <- tempfile(fileext = ".ptx")
library(ACFcuda)
## ACFcuda 0.1.1 - CUDA GPU acceleration is available
library(microbenchmark)
# make device context
dvc_ctx <- cuda_make_context()

cuda_source_to_ptx(cuda_file = tmp01,
                   ptx_file = tmp02,
                   verbose = TRUE)
## character(0)
fun_ptr <- cuda_kernel_from_ptx(cuda_context = dvc_ctx,
                                ptx_file = tmp02,
                                kernel_name = "cuda_mm_naive")
set.seed(1986)
dim_set <- c(100, 250, 500, 1000, 2500)
res_vals <- vector(mode = "list",
                   length = length(dim_set))
fun_vec <- c("internal",
             "fun01")

for (a1 in seq_along(dim_set)) {
  dim_size <- dim_set[a1]
  var1 <- matrix(data = runif(dim_size^2),
                 nrow = dim_size,
                 ncol = dim_size)
  var2 <- matrix(data = runif(dim_size^2),
                 nrow = dim_size,
                 ncol = dim_size)
  dummy_vec <- matrix(data = numeric(),
                      nrow = dim_size,
                      ncol = dim_size)
  
  arg_types <- c("float",
                 "uint",
                 "uint",
                 "uint",
                 "float",
                 "float")
  # we can cheat here because these are symetrical matrices
  arg_list <- list(dummy_vec,
                   dim_size,
                   dim_size,
                   dim_size,
                   var1,
                   var2)
  
  # suppress the nanosecond timings warning...
  timing_res <- suppressWarnings(microbenchmark("internal" = var1 %*% var2,
                                                "fun01" = simple_cuda_wrapper(context_pointer = dvc_ctx,
                                                                              kernel_pointer = fun_ptr,
                                                                              arg_types = arg_types,
                                                                              arg_list = arg_list),
                                                times = 5))
  res_vals[[a1]] <- vapply(X = c("internal",
                                 "fun01"),
                           FUN = function(x) {
                             mean(timing_res$time[timing_res$expr == x])
                           },
                           FUN.VALUE = vector(mode = "numeric",
                                              length = 1))
}

res_vals <- do.call(rbind,
                    res_vals)
colset <- c("black",
            "blue")

par(mar = c(3,2.5,2,1),
    mgp = c(1.45, .55, 0))
plot(x = 1e-100,
     y = 1e-100,
     xlim = range(dim_set),
     ylim = range(unlist(res_vals)),
     xlab = "dim size (log)",
     ylab = "nanoseconds (log)",
     type = "n",
     log = "xy",
     main = "Matrix Multiplication")

for (a1 in seq(ncol(res_vals))) {
  points(x = dim_set,
         y = res_vals[, a1],
         pch = 20,
         col = colset[a1],
         cex = 2)
}
legend("topleft",
       legend = c("internal",
                  "cuda"),
       col = colset,
       pch = 20,
       cex = 1.5)

As should be expected (because compute is taking place with different precisions), values returned by compute on a CUDA devices are not going to be exactly the same as those generated on the CPU, but typically deviations here are within generally acceptable tolerances. Checking them for personal preferences is rarely a poor choice though. Unique implementations of the same operations should return matching values however.

dim_size <- 100
var1 <- matrix(data = runif(dim_size^2),
               nrow = dim_size,
               ncol = dim_size)
var2 <- matrix(data = runif(dim_size^2),
               nrow = dim_size,
               ncol = dim_size)
dummy_vec <- matrix(data = numeric(),
                    nrow = dim_size,
                    ncol = dim_size)

arg_types <- c("float",
               "uint",
               "uint",
               "uint",
               "float",
               "float")
# we can cheat here because these are symetrical matrices
arg_list <- list(dummy_vec,
                 dim_size,
                 dim_size,
                 dim_size,
                 var1,
                 var2)

res01 <- var1 %*% var2
res02 <- simple_cuda_wrapper(context_pointer = dvc_ctx,
                             kernel_pointer = fun_ptr,
                             arg_types = arg_types,
                             arg_list = arg_list)

layout(mat = matrix(data = 1:2,
                    nrow = 1))
plot(as.vector(res01),
     as.vector(res02),
     pch = 46,
     xlab = "CPU result",
     ylab = "GPU result",
     main = "CPU vs GPU")
plot(as.vector(res01) - as.vector(res02),
     pch = 46,
     xlab = "index",
     ylab = "delta between implementations",
     main = "implementation 1 vs 2")

About

Infrastructure for calling CUDA from R

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages