Files
2026-07-13 12:37:59 +08:00

227 lines
9.5 KiB
Plaintext

/*
Kernels for the AdamW optimizer.
References:
* https://pytorch.org/docs/stable/generated/torch.optim.AdamW.html
* https://github.com/nvidia/apex/blob/master/csrc/multi_tensor_adam.cu
Compile example:
nvcc -lcublas -lcublasLt adamw.cu -o adamw
nvcc -O3 --use_fast_math -lcublas -lcublasLt adamw.cu -o adamw
./adamw
TODO(general):
amsgrad=True
TODO(perf):
dtype
thread coarsening/ILP
*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <cuda_runtime.h>
#include "common.h"
// ----------------------------------------------------------------------------
// CPU code reference
void adamw_cpu(float* params_memory, const float* grads_memory, float* m_memory, float* v_memory, int t, long num_parameters, float learning_rate=1e-3, float beta1=0.9, float beta2=0.999, float eps=1e-8, float weight_decay=0.0) {
// adapted from: train_gpt2.c
for (int i = 0; i < num_parameters; i++) {
float param = params_memory[i];
float grad = grads_memory[i];
// update the first moment (momentum)
float m = beta1 * m_memory[i] + (1.0f - beta1) * grad;
// update the second moment (RMSprop)
float v = beta2 * v_memory[i] + (1.0f - beta2) * grad * grad;
// bias-correct both moments
float m_hat = m / (1.0f - powf(beta1, t));
float v_hat = v / (1.0f - powf(beta2, t));
// update
m_memory[i] = m;
v_memory[i] = v;
params_memory[i] -= learning_rate * (m_hat / (sqrtf(v_hat) + eps) + weight_decay * param);
}
}
// ----------------------------------------------------------------------------
// GPU kernels
// utility functions
// Implements linear interpolation using only two floating-point operations (as opposed to three in a naive implementation).
// Reference: https://developer.nvidia.com/blog/lerp-faster-cuda
__device__ inline float lerp(float start, float end, float weight) {
return fma(weight, end, fma(-weight, start, start));
}
// naive fused kernel
__global__ void adamw_kernel1(float* params_memory, const float* grads_memory, float* m_memory, float* v_memory, long num_parameters,
float learning_rate, float beta1, float beta2, float beta1_correction, float beta2_correction, float eps, float weight_decay) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i >= num_parameters) return; // guard
// update the first moment (momentum)
m_memory[i] = beta1 * m_memory[i] + (1.0f - beta1) * grads_memory[i];
// update the second moment (RMSprop)
v_memory[i] = beta2 * v_memory[i] + (1.0f - beta2) * grads_memory[i] * grads_memory[i];
float m_hat = m_memory[i] / beta1_correction;
float v_hat = v_memory[i] / beta2_correction;
params_memory[i] -= learning_rate * (m_hat / (sqrtf(v_hat) + eps) + weight_decay * params_memory[i]);
}
// Slightly more optimized AdamW kernel by:
// * loading data that is accessed more than once into registers,
// * using optimized linear interpolation for the moment updates.
__global__ void adamw_kernel2(float* params_memory, const float* grads_memory, float* m_memory, float* v_memory, long num_parameters,
float learning_rate, float beta1, float beta2, float beta1_correction, float beta2_correction, float eps, float weight_decay) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i >= num_parameters) return; // guard
float grad = grads_memory[i];
float m = m_memory[i];
float v = v_memory[i];
// update the first moment (momentum)
m = lerp(grad, m, beta1);
m_memory[i] = m;
// update the second moment (RMSprop)
v = lerp(grad * grad, v, beta2);
v_memory[i] = v;
m /= beta1_correction; // m_hat
v /= beta2_correction; // v_hat
params_memory[i] -= learning_rate * (m / (sqrtf(v) + eps) + weight_decay * params_memory[i]);
}
// ----------------------------------------------------------------------------
// kernel launcher
// version 1: naive dispatch to naive kernel
void adamw_dispatch1(float* params_memory, const float* grads_memory, float* m_memory, float* v_memory, long num_parameters,
float learning_rate, float beta1, float beta2, float beta1_correction, float beta2_correction, float eps, float weight_decay) {
unsigned int block_size = 512;
unsigned int num_blocks = ceil_div(num_parameters, (long) block_size);
adamw_kernel1<<<num_blocks, block_size>>>(params_memory, grads_memory, m_memory, v_memory, num_parameters,
learning_rate, beta1, beta2, beta1_correction, beta2_correction, eps, weight_decay);
cudaCheck(cudaGetLastError());
}
// version 2: naive dispatch to slightly optimized kernel
void adamw_dispatch2(float* params_memory, const float* grads_memory, float* m_memory, float* v_memory, long num_parameters,
float learning_rate, float beta1, float beta2, float beta1_correction, float beta2_correction, float eps, float weight_decay) {
unsigned int block_size = 512;
unsigned int num_blocks = ceil_div(num_parameters, (long) block_size);
adamw_kernel2<<<num_blocks, block_size>>>(params_memory, grads_memory, m_memory, v_memory, num_parameters,
learning_rate, beta1, beta2, beta1_correction, beta2_correction, eps, weight_decay);
cudaCheck(cudaGetLastError());
}
void adamw(int kernel_num,
float* params_memory, const float* grads_memory, float* m_memory, float* v_memory, int t, long num_parameters,
float learning_rate=1e-3, float beta1=0.9, float beta2=0.999, float eps=1e-8, float weight_decay=0.0) {
// calculate the m_hat and v_hat correction terms once as they are the same for every param/thread
float beta1_correction = 1.0f - powf(beta1, t);
float beta2_correction = 1.0f - powf(beta2, t);
switch (kernel_num) {
case 1:
adamw_dispatch1(params_memory, grads_memory, m_memory, v_memory, num_parameters,
learning_rate, beta1, beta2, beta1_correction, beta2_correction, eps, weight_decay);
break;
case 2:
adamw_dispatch2(params_memory, grads_memory, m_memory, v_memory, num_parameters,
learning_rate, beta1, beta2, beta1_correction, beta2_correction, eps, weight_decay);
break;
default:
printf("Invalid kernel number\n");
exit(1);
}
}
// ----------------------------------------------------------------------------
int main(int argc, char **argv) {
setup_main();
const long num_parameters = 1048576;
const int t = 10;
const float learning_rate = 1e-3f;
const float beta1 = 0.9f;
const float beta2 = 0.999f;
const float eps = 1e-8f;
const float weight_decay = 0.0f;
// create random data on host (to be used for the CPU reference implementation)
float* params_memory = make_random_float(num_parameters);
float* grads_memory = make_random_float(num_parameters);
float* m_memory = make_random_float(num_parameters);
float* v_memory = make_random_float_01(num_parameters);
// move to GPU
float* d_params_memory;
float* d_grads_memory;
float* d_m_memory;
float* d_v_memory;
cudaCheck(cudaMalloc(&d_params_memory, num_parameters * sizeof(float)));
cudaCheck(cudaMalloc(&d_grads_memory, num_parameters * sizeof(float)));
cudaCheck(cudaMalloc(&d_m_memory, num_parameters * sizeof(float)));
cudaCheck(cudaMalloc(&d_v_memory, num_parameters * sizeof(float)));
cudaCheck(cudaMemcpy(d_params_memory, params_memory, num_parameters * sizeof(float), cudaMemcpyHostToDevice));
cudaCheck(cudaMemcpy(d_grads_memory, grads_memory, num_parameters * sizeof(float), cudaMemcpyHostToDevice));
cudaCheck(cudaMemcpy(d_m_memory, m_memory, num_parameters * sizeof(float), cudaMemcpyHostToDevice));
cudaCheck(cudaMemcpy(d_v_memory, v_memory, num_parameters * sizeof(float), cudaMemcpyHostToDevice));
// read kernel_num from command line
int kernel_num = 1;
if (argc > 1) {
kernel_num = atoi(argv[1]);
}
printf("Using kernel %d\n", kernel_num);
// calculate the CPU reference (using default hyperparams)
clock_t start = clock();
adamw_cpu(params_memory, grads_memory, m_memory, v_memory, t, num_parameters);
clock_t end = clock();
// TODO: measure runtime with multiple runs
double elapsed_time_cpu = (double)(end - start) / CLOCKS_PER_SEC;
// calculate the GPU version (using default hyperparams)
adamw(kernel_num, d_params_memory, d_grads_memory, d_m_memory, d_v_memory, t, num_parameters);
// compare
printf("Checking correctness...\n");
printf("parameters:\n");
validate_result(d_params_memory, params_memory, "params_memory", num_parameters);
printf("first moment:\n");
validate_result(d_m_memory, m_memory, "m_memory", num_parameters);
printf("second moment:\n");
validate_result(d_v_memory, v_memory, "v_memory", num_parameters);
printf("All results match.\n\n");
// now benchmark the kernel
int repeat_times = 1000;
float elapsed_time = benchmark_kernel(repeat_times, adamw, kernel_num,
d_params_memory, d_grads_memory, d_m_memory, d_v_memory, t, num_parameters,
learning_rate, beta1, beta2, eps, weight_decay);
printf("time gpu %.4f ms\n", elapsed_time);
printf("time cpu %.4f ms\n", elapsed_time_cpu);
// cleanup
free(params_memory);
free(grads_memory);
free(m_memory);
free(v_memory);
cudaCheck(cudaFree(d_params_memory));
cudaCheck(cudaFree(d_grads_memory));
cudaCheck(cudaFree(d_m_memory));
cudaCheck(cudaFree(d_v_memory));
return 0;
}