CUDA 机器学习:实用应用

CUDA C/C++ 应用程序的结构,其中主机(CPU)代码管理设备(GPU)上的并行代码的执行。
现在我们已经介绍了基础知识,让我们探索 CUDA 如何应用于常见的机器学习任务。
-
矩阵乘法
矩阵乘法是许多机器学习算法中的基本操作,特别是在神经网络中。CUDA 可以显著加速此操作。以下是一个简单的实现:
__global__ void matrixMulKernel(float *A, float *B, float *C, int N)
{
int row = blockIdx.y * blockDim.y + threadIdx.y;
int col = blockIdx.x * blockDim.x + threadIdx.x;
float sum = 0.0f;
if (row < N && col < N) {
for (int i = 0; i < N; i++) {
sum += A[row * N + i] * B[i * N + col];
}
C[row * N + col] = sum;
}
}
// 主机函数,用于设置和启动内核
void matrixMul(float *A, float *B, float *C, int N)
{
dim3 threadsPerBlock(16, 16);
dim3 numBlocks((N + threadsPerBlock.x - 1) / threadsPerBlock.x,
(N + threadsPerBlock.y - 1) / threadsPerBlock.y);
matrixMulKernel<>(A, B, C, N);
}
此实现将输出矩阵划分为块,每个线程计算一个结果元素。虽然此基本版本对于大矩阵已经比 CPU 实现快,但仍有优化的余地,例如使用共享内存等技术。
-
卷积运算
卷积神经网络(CNN)严重依赖卷积运算。CUDA 可以显著加速这些计算。以下是一个简化的 2D 卷积内核:
__global__ void convolution2DKernel(float *input, float *kernel, float *output,
int inputWidth, int inputHeight,
int kernelWidth, int kernelHeight)
{
int x = blockIdx.x * blockDim.x + threadIdx.x;
int y = blockIdx.y * blockDim.y + threadIdx.y;
if (x < inputWidth && y < inputHeight) {
float sum = 0.0f;
for (int ky = 0; ky < kernelHeight; ky++) {
for (int kx = 0; kx = 0 && inputX = 0 && inputY < inputHeight) {
sum += input[inputY * inputWidth + inputX] *
kernel[ky * kernelWidth + kx];
}
}
}
output[y * inputWidth + x] = sum;
}
}
此内核执行 2D 卷积,每个线程计算一个输出像素。在实践中,更复杂的实现将使用共享内存来减少全局内存访问,并针对各种内核大小进行优化。
-
随机梯度下降(SGD)
SGD 是机器学习中的一种基础优化算法。CUDA 可以并行化多个数据点的梯度计算。以下是线性回归的一个简单示例:
__global__ void sgdKernel(float *X, float *y, float *weights, float learningRate, int n, int d)
{
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) {
float prediction = 0.0f;
for (int j = 0; j < d; j++) {
prediction += X[i * d + j] * weights[j];
}
float error = prediction - y[i];
for (int j = 0; j < d; j++) {
atomicAdd(&weights[j], -learningRate * error * X[i * d + j]);
}
}
}
void sgd(float *X, float *y, float *weights, float learningRate, int n, int d, int iterations)
{
int threadsPerBlock = 256;
int numBlocks = (n + threadsPerBlock - 1) / threadsPerBlock;
for (int iter = 0; iter < iterations; iter++) {
sgdKernel<<>>(X, y, weights, learningRate, n, d);
}
}
此实现并行更新每个数据点的权重。 atomicAdd 函数用于安全地处理权重的并发更新。
优化 CUDA 机器学习
虽然上面的示例演示了使用 CUDA 进行机器学习任务的基础知识,但还有几种优化技术可以进一步提高性能:
-
合并内存访问
GPU 在线程在 warp 中访问连续内存位置时达到峰值性能。确保您的数据结构和访问模式促进合并内存访问。
-
共享内存使用
共享内存比全局内存快得多。使用它来缓存块内线程经常访问的数据。

了解内存层次结构与 CUDA
此图说明了具有共享内存的多处理器系统的体系结构。每个处理器都有自己的缓存,允许快速访问经常使用的数据。处理器通过共享总线相互通信,该总线将它们连接到更大的共享内存空间。
例如,在矩阵乘法中:
__global__ void matrixMulSharedKernel(float *A, float *B, float *C, int N)
{
__shared__ float sharedA[TILE_SIZE][TILE_SIZE];
__shared__ float sharedB[TILE_SIZE][TILE_SIZE];
int bx = blockIdx.x; int by = blockIdx.y;
int tx = threadIdx.x; int ty = threadIdx.y;
int row = by * TILE_SIZE + ty;
int col = bx * TILE_SIZE + tx;
float sum = 0.0f;
for (int tile = 0; tile < (N + TILE_SIZE - 1) / TILE_SIZE; tile++) {
if (row < N && tile * TILE_SIZE + tx < N)
sharedA[ty][tx] = A[row * N + tile * TILE_SIZE + tx];
else
sharedA[ty][tx] = 0.0f;
if (col < N && tile * TILE_SIZE + ty < N)
sharedB[ty][tx] = B[(tile * TILE_SIZE + ty) * N + col];
else
sharedB[ty][tx] = 0.0f;
__syncthreads();
for (int k = 0; k < TILE_SIZE; k++)
sum += sharedA[ty][k] * sharedB[k][tx];
__syncthreads();
}
if (row < N && col < N)
C[row * N + col] = sum;
}
此优化版本使用共享内存来减少全局内存访问,从而显著提高大矩阵的性能。
-
异步操作
CUDA 支持异步操作,允许您重叠计算和数据传输。这在机器学习管道中特别有用,您可以在处理当前批次的同时准备下一个批次的数据。
cudaStream_t stream1, stream2;
cudaStreamCreate(&stream1);
cudaStreamCreate(&stream2);
// 异步内存传输和内核启动
cudaMemcpyAsync(d_data1, h_data1, size, cudaMemcpyHostToDevice, stream1);
myKernel<<>>(d_data1, ...);
cudaMemcpyAsync(d_data2, h_data2, size, cudaMemcpyHostToDevice, stream2);
myKernel<<>>(d_data2, ...);
cudaStreamSynchronize(stream1);
cudaStreamSynchronize(stream2);
-
张量核心
对于机器学习工作负载,NVIDIA 的张量核心(可在较新的 GPU 架构中使用)可以为矩阵乘法和卷积操作提供显著的加速。像 cuDNN 和 cuBLAS 这样的库可以自动利用可用的张量核心。
挑战和考虑
虽然 CUDA 为机器学习提供了巨大的好处,但了解潜在的挑战至关重要:
- 内存管理:GPU 内存与系统内存相比是有限的。高效的内存管理在处理大型数据集或模型时至关重要。
- 数据传输开销:在 CPU 和 GPU 之间传输数据可能会成为瓶颈。尽量减少传输并在可能的情况下使用异步操作。
- 精度:GPU 传统上擅长单精度(FP32)计算。虽然对双精度(FP64)的支持有所改善,但通常速度较慢。许多机器学习任务可以使用较低的精度(例如 FP16)工作,现代 GPU 可以非常高效地处理这些精度。
- 代码复杂性:编写高效的 CUDA 代码可能比编写 CPU 代码更复杂。利用 cuDNN、cuBLAS 和 TensorFlow 或 PyTorch 等框架可以帮助抽象掉其中的一些复杂性。
CUDA 编程结构
要有效地利用 CUDA,了解其编程结构至关重要,涉及编写内核(在 GPU 上运行的函数)和管理主机(CPU)和设备(GPU)之间的内存。
主机内存与设备内存
在 CUDA 中,内存分别为主机和设备管理。以下是用于内存管理的主要函数:
- cudaMalloc:在设备上分配内存。
- cudaMemcpy:在主机和设备之间复制数据。
- cudaFree:释放设备上的内存。
示例:合并两个数组
让我们看一个示例,它使用 CUDA 合并两个数组:
__global__ void sumArraysOnGPU(float *A, float *B, float *C, int N) {
int idx = threadIdx.x + blockIdx.x * blockDim.x;
if (idx < N) C[idx] = A[idx] + B[idx];
}
int main() {
int N = 1024;
size_t bytes = N * sizeof(float);
float *h_A, *h_B, *h_C;
h_A = (float*)malloc(bytes);
h_B = (float*)malloc(bytes);
h_C = (float*)malloc(bytes);
float *d_A, *d_B, *d_C;
cudaMalloc(&d_A, bytes);
cudaMalloc(&d_B, bytes);
cudaMalloc(&d_C, bytes);
cudaMemcpy(d_A, h_A, bytes, cudaMemcpyHostToDevice);
cudaMemcpy(d_B, h_B, bytes, cudaMemcpyHostToDevice);
int blockSize = 256;
int gridSize = (N + blockSize - 1) / blockSize;
sumArraysOnGPU<<>>(d_A, d_B, d_C, N);
cudaMemcpy(h_C, d_C, bytes, cudaMemcpyDeviceToHost);
cudaFree(d_A);
cudaFree(d_B);
cudaFree(d_C);
free(h_A);
free(h_B);
free(h_C);
return 0;
}
在此示例中,主机和设备上都分配了内存,数据被传输到设备,并启动内核执行计算。
结论
CUDA 是机器学习工程师加速模型和处理更大数据集的强大工具。通过了解 CUDA 内存模型,优化内存访问,并利用多个 GPU,您可以显著提高机器学习应用程序的性能。