Writing Custom CUDA Kernels¶
Overview¶
When the GPU is idle waiting on the CPU (launch-bound) or no PyTorch op expresses your algorithm, write a CUDA kernel— a function that runs in parallel on GPU threads— and expose it through a CUDAExtension. The win: fusion (Ch 04-05), custom algorithms (flash-style attention, quantization), and elimination of host-device round-trips.
- Kernels:
__global__functions; grid/block launch config; threadIdx/blockIdx indexing. - Memory: registers → shared memory → global memory; coalescing matters.
- Compile with
torch.utils.cpp_extension.load(..., with_cuda=True)orCUDAExtension. - Wrap in
torch.autograd.Functionfor gradients.
Rule: correctness first, then profile with
ncu. A naive kernel is often slower than PyTorch's own.
-
Minimal CUDA Extension¶
// mycu.cpp
#include <torch/extension.h>
#include <cuda.h>
#include <cuda_runtime.h>
__global__ void add_kernel(const float* a, const float* b, float* out, int n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) out[i] = a[i] + b[i];
}
torch::Tensor cuda_add(torch::Tensor a, torch::Tensor b) {
auto out = torch::empty_like(a);
int n = a.numel();
int threads = 256;
int blocks = (n + threads - 1) / threads;
add_kernel<<<blocks, threads>>>(a.data_ptr<float>(), b.data_ptr<float>(),
out.data_ptr<float>(), n);
return out;
}
PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
m.def("add", &cuda_add);
}
from torch.utils.cpp_extension import load
cu = load(name="mycu", sources=["mycu.cpp"], with_cuda=True)
x = torch.randn(1024, device='cuda')
y = torch.randn(1024, device='cuda')
print(cu.add(x, y).shape)
-
Autograd for a Custom Kernel¶
import torch, mycu
class CudaAddFn(torch.autograd.Function):
@staticmethod
def forward(ctx, a, b):
return mycu.add(a.contiguous(), b.contiguous())
@staticmethod
def backward(ctx, g):
return g, g # d/da (a+b) = 1, d/db = 1 (elementwise)
x = torch.randn(8, device='cuda', requires_grad=True)
CudaAddFn.apply(x, torch.randn(8, device='cuda')).sum().backward()
print("grad ok:", x.grad.sum().item())
Always
.contiguous()beforedata_ptr— strided tensors will corrupt reads.
-
Launch Configuration Rules¶
threads = 256 # 1D typical; power-of-two
blocks = (n + threads - 1) // threads
kernel<<<blocks, threads>>>(...)
- Coalescing: consecutive threads → consecutive addresses. The #1 perf lever.
- Grid-stride loop: for huge tensors, loop within kernel to avoid blocks > 2^31.
__global__ void add_stride(const float* a, const float* b, float* o, int n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
int stride = gridDim.x * blockDim.x;
for (; i < n; i += stride)
o[i] = a[i] + b[i];
}
- Shared memory for reductions/tiling:
__shared__ float smem[256];. - Non-blocking: after launch, code proceeds; call
torch.cuda.synchronize()only in tests.
When a Custom Kernel Wins¶
| Situation | Verdict |
|---|---|
| Elementwise chains (fusion) | Triton/Inductor already fuses → skip hand CUDA |
| Custom math (quant, top-k, search) | hand CUDA wins |
| Attention/Flash patterns | use F.scaled_dot_product_attention first |
| Memory-bound reduction over huge arrays | hand CUDA + vectorized loads |
| Learning/experiments | torch.compile + Triton first |
Modern guidance: reach for Triton inside
torch.compileor write Triton kernels directly (tritonpython) before raw CUDA, unless you need low-level control (shared memory tuning, atomics).
-
Debugging Kernels¶
- Check launch bounds valid (blocks/threads positive).
- Use
cudaGetLastError()after launch in debug builds. compute-sanitizer --tool memcheck python test.pyfor OOB/race.- Reference-check vs a CPU/python implementation on random small inputs.
- Add
-lineinfo -Gfor source-levelncu/nsightattribution.
def reference_check(fn_cuda, fn_py, shape=(8, 8), seed=0):
torch.manual_seed(seed)
a = torch.randn(*shape, device='cuda')
b = torch.randn(*shape, device='cuda')
assert torch.allclose(fn_cuda(a, b), fn_py(a, b), atol=1e-5)
print("reference check passed ")
-
Key Takeaways¶
- Kernels =
__global__+ launch config +data_ptrI/O; wrap with autograd Function. - Coalescing + grid-stride loops are the two big perf levers.
.contiguous()inputs before passing pointers; check bounds.- Don't hand-write what Inductor/Triton/SDPA already fuse— verify need via profile.
- Debug with
compute-sanitizer+ reference checks; profile withncu.
-
Related Topics¶
- C++ Extensions
- [Practice & Pitfalls](/06-pytorch/09-custom-c++-and-cuda-extensions/(03-extensions-in-practice-pitfalls/)
- Kernel Fusion
- Autograd Wrapper