Skip to main content

Runtime API vs Driver API

Every CUDA C++ example so far — <<<grid, block>>> launches, cudaMalloc, cudaMemcpy — has gone through the runtime API, the high-level interface linked in as cudart and initialized implicitly the first time a program touches the GPU. Underneath it sits the driver API (cuda.h, linked as cuda), a lower-level, explicit interface that the runtime itself is built on. Almost nothing in application code needs the driver API directly, but understanding what the runtime is hiding explains a class of errors ("invalid device context") that only make sense once you know a context exists at all.

Two APIs over one driver

Both APIs talk to the same underlying driver and the same GPU; they differ in how much the programmer has to manage explicitly.

ConceptRuntime APIDriver API
InitializationImplicit, on first CUDA callExplicit cuInit(0)
ContextImplicit primary context per device, created lazilyExplicit cuCtxCreate / cuCtxSetCurrent, or retain the primary context
Module loadingKernels compiled and linked in at build time via nvccExplicit cuModuleLoad of a cubin or PTX blob at run time
Kernel launchkernel<<<grid, block>>>(args...)cuLaunchKernel with an explicit void* args[] array
Symbol lookupKernel is a named C++ function; globals via cudaGetSymbolAddresscuModuleGetFunction / cuModuleGetGlobal by string name
Error typecudaError_tCUresult

Contexts

A CUDA context is the analog of a process on the GPU: it owns the device's address space, its loaded modules, and its allocations. The driver API makes context management explicit — code calls cuCtxCreate to create one and cuCtxSetCurrent to make it current on a thread. The runtime API hides all of this behind the primary context, described below.

Modules and cuLaunchKernel

The runtime API compiles a .cu file's __global__ functions into the executable (or a fatbinary embedded in it) at build time, and kernel<<<grid, block>>>(a, b, c) is nvcc-generated sugar over a launch that already knows the function's address and argument types. The driver API has no such compile-time link: it loads a cubin or PTX module explicitly with cuModuleLoad, looks up the kernel by name with cuModuleGetFunction, and launches it with cuLaunchKernel, which takes its arguments as an untyped void* args[] array instead of a typed parameter list:

// Runtime API
saxpy<<<grid, block>>>(n, a, d_x, d_y);

// Driver API — same launch, made explicit
CUfunction saxpyFn;
cuModuleGetFunction(&saxpyFn, module, "saxpy");
void* args[] = { &n, &a, &d_x, &d_y };
cuLaunchKernel(saxpyFn,
grid.x, grid.y, grid.z,
block.x, block.y, block.z,
0, stream, args, nullptr);

cuModuleGetFunction and cuLaunchKernel return CUresult, not cudaError_t, so checking them takes a macro built from cuGetErrorName/cuGetErrorString, not the runtime API's CUDA_CHECK. See Error Handling and Checking for what CUDA_CHECK does with the cudaError_t that runtime-API calls such as cudaMalloc and cudaMemcpy return.

The <<<>>> syntax is exactly this call with the function handle, argument packing, and error type resolved at compile time instead of at run time.

The primary context

The runtime API lazily creates and shares a single primary context per device, retained for the lifetime of the process (or until cudaDeviceReset). Every runtime call on that device operates through this one context. Driver-API code that calls cuCtxCreate creates a separate, non-primary context — and mixing that context with runtime-API calls on the same thread is the classic source of "invalid device context" errors, because the runtime keeps operating against the primary context while the driver-API code just made a different one current. cuDevicePrimaryCtxRetain is the interop path: it hands driver-API code a reference to the same primary context the runtime uses, instead of creating a rival one.

When you need the driver API

Use the runtime API

The honest recommendation is to write runtime-API code by default — it is simpler, less error-prone, and does everything most CUDA programs need. Reach for the driver API only for JIT-loading cubins or PTX at run time (plugin systems, compiler-generated kernels), for managing multiple isolated contexts deliberately, or when writing a language binding that needs the driver's finer-grained control.

Python has a modern driver-API wrapper

CUDA 12 added cuda.core / cuda.bindings on the Python side as a modern wrapper directly over the driver API, replacing the older ad hoc ctypes bindings some projects hand-rolled. See CUDA Python and CuPy.

See also