CUDA
In this section:
Description
CUDA (Compute Unified Device Architecture) is a parallel computing platform and programming model developed by NVIDIA for processing on GPUs (Graphics Processing Units). It enables significant acceleration for scientific applications, machine learning, and image processing.
Available Versions
cuda/12.4
cuda/12.9 (default)
Note
CUDA versions are compatible with specific drivers. Check the documentation of the applications you plan to use to choose the appropriate version.
Environment Setup
When loading the CUDA module, several environment variables are defined:
module load cuda/12.9
# Important directories
echo $CUDA_HOME # Installation directory
echo $CUDA_PATH # Same as CUDA_HOME
echo $CUDA_ROOT # CUDA root
# PATH and LD_LIBRARY_PATH are automatically configured
echo $PATH
echo $LD_LIBRARY_PATH
Jobs using GPU must be submitted to the special gpu partition:
#SBATCH --partition=gpu
#SBATCH --gres=gpu:N # N = number of GPUs desired (1-4)
Compiling CUDA Code
The nvcc compiler is used to compile CUDA code.
#!/bin/bash
#SBATCH -J compile_cuda
#SBATCH -p gpu
#SBATCH --gres=gpu:1
#SBATCH -N 1
#SBATCH -n 1
#SBATCH -t 00:10:00
#SBATCH --mem=20G
export INPUT="vetor_add.cu"
export OUTPUT="vetor_add"
module load cuda/12.9
job-nanny nvcc -o vetor_add vetor_add.cu
Example CUDA Code
#include <stdio.h>
#include <cuda_runtime.h>
// CUDA Kernel
__global__ void add_vectors(float *a, float *b, float *c, int n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) {
c[i] = a[i] + b[i];
}
}
int main() {
int n = 1000000;
size_t size = n * sizeof(float);
// Allocate host memory
float *h_a = (float*)malloc(size);
float *h_b = (float*)malloc(size);
float *h_c = (float*)malloc(size);
// Initialize data
for (int i = 0; i < n; i++) {
h_a[i] = i * 1.0f;
h_b[i] = i * 2.0f;
}
// Allocate device memory
float *d_a, *d_b, *d_c;
cudaMalloc(&d_a, size);
cudaMalloc(&d_b, size);
cudaMalloc(&d_c, size);
// Copy data to device
cudaMemcpy(d_a, h_a, size, cudaMemcpyHostToDevice);
cudaMemcpy(d_b, h_b, size, cudaMemcpyHostToDevice);
// Configure and launch kernel
int threads = 256;
int blocks = (n + threads - 1) / threads;
add_vectors<<<blocks, threads>>>(d_a, d_b, d_c, n);
cudaDeviceSynchronize();
// Copy result back
cudaMemcpy(h_c, d_c, size, cudaMemcpyDeviceToHost);
// Verify result
printf("c[0] = %f\n", h_c[0]);
printf("c[%d] = %f\n", n-1, h_c[n-1]);
// Free memory
cudaFree(d_a);
cudaFree(d_b);
cudaFree(d_c);
free(h_a);
free(h_b);
free(h_c);
return 0;
}
CUDA Job Submission
#!/bin/bash
#SBATCH -J cuda_job
#SBATCH -p gpu
#SBATCH --gres=gpu:1
#SBATCH -N 1
#SBATCH -n 1
#SBATCH -t 00:30:00
#SBATCH --mem=40G
export INPUT="vetor_add"
export OUTPUT="cuda_output.txt"
module load cuda/12.9
job-nanny ./vetor_add > cuda_output.txt
Multi-GPU
#!/bin/bash
#SBATCH -J cuda_multi
#SBATCH -p gpu
#SBATCH --gres=gpu:4
#SBATCH -N 1
#SBATCH -n 4
#SBATCH -t 02:00:00
#SBATCH --mem=32G
export INPUT="programa_multi_gpu"
export OUTPUT="multi_gpu_output/"
module load cuda/12.9
# The program must manage multiple GPUs
job-nanny ./programa_multi_gpu
GPU Information
During execution, you can obtain information about the GPUs:
# In the submission script
nvidia-smi --query-gpu=name,memory.total,memory.free --format=csv
# Check GPUs available for the job
echo "CUDA_VISIBLE_DEVICES=$CUDA_VISIBLE_DEVICES"
# Simple test
deviceQuery
Compiling for Multiple Architectures
# Compile for multiple architectures
nvcc -arch=sm_80 -code=sm_80,sm_86,sm_89 programa.cu -o programa
# Or use the current architecture
nvcc -arch=native programa.cu -o programa
Using CUDA with Python (CuPy)
#!/bin/bash
#SBATCH -J cupy
#SBATCH -p gpu
#SBATCH --gres=gpu:1
#SBATCH -N 1
#SBATCH -n 1
#SBATCH -t 01:00:00
#SBATCH --mem=8G
export INPUT="cupy_script.py"
export OUTPUT="cupy_output/"
module load cuda/12.9
module load miniconda/24.4.0-libmamba
source activate cupy_env # Environment with cupy installed
job-nanny python cupy_script.py
Example with CuPy
import cupy as cp
import numpy as np
import time
# Create arrays on the GPU
size = 10000
a = cp.random.randn(size, size, dtype=cp.float32)
b = cp.random.randn(size, size, dtype=cp.float32)
# GPU operation
start = time.time()
c = cp.matmul(a, b)
cp.cuda.Stream.null.synchronize()
gpu_time = time.time() - start
print(f"Matrix multiplication {size}x{size} on GPU: {gpu_time:.3f}s")
# CPU comparison
a_cpu = cp.asnumpy(a)
b_cpu = cp.asnumpy(b)
start = time.time()
c_cpu = np.matmul(a_cpu, b_cpu)
cpu_time = time.time() - start
print(f"Multiplication on CPU: {cpu_time:.3f}s")
print(f"Speedup: {cpu_time/gpu_time:.2f}x")
Debugging and Profiling
# Use cuda-gdb for debugging
cuda-gdb ./programa
# Profiling with nvprof (older versions)
nvprof ./programa
# Profiling with Nsight Systems (newer versions)
nsys profile ./programa
# Profiling with Nsight Compute
ncu ./programa
References
CUDA documentation: https://docs.nvidia.com/cuda/
CUDA C++ Programming Guide: https://docs.nvidia.com/cuda/cuda-c-programming-guide/
CuPy Documentation: https://docs.cupy.dev/
See also
GPU Usage - General GPU usage on GridUnesp
Anaconda - Installing packages such as CuPy
Running Simulations - How to submit jobs