karpathy--llm.c
813 行
28 KiB
Plaintext
813 行
28 KiB
Plaintext
/*
|
|
Kernels for attention forward pass.
|
|
|
|
Compile example:
|
|
nvcc -O3 --use_fast_math attention_forward.cu -o attention_forward -lcublas
|
|
|
|
version 1 is naive port from CPU code to kernel, parallelize over batch, time, heads only
|
|
./attention_forward 1
|
|
|
|
version 2 is a naive implementation of flash attention, taken, adapted from
|
|
https://github.com/tspeterkim/flash-attention-minimal
|
|
and with help from
|
|
https://github.com/leloykun/flash-hyperbolic-attention-minimal
|
|
sadly, this flash attention version seems about 3X slower than the naive version
|
|
./attention_forward 2
|
|
|
|
version 3 is a cuBLAS + softmax version, similar to the PyTorch implementation
|
|
cuBLAS is used both to calculate the QK^T and the final weighted sum
|
|
the softmax is calculated using a custom, efficient kernel as well
|
|
this turns out to be ~20X faster than (1) nice
|
|
./attention_forward 3
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <cublas_v2.h>
|
|
#include <cuda_runtime.h>
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// CUDA utils
|
|
|
|
#define CEIL_DIV(M, N) (((M) + (N)-1) / (N))
|
|
|
|
// error checking
|
|
void cudaCheck(cudaError_t error, const char *file, int line) {
|
|
if (error != cudaSuccess) {
|
|
printf("[CUDA ERROR] at file %s:%d:\n%s\n", file, line,
|
|
cudaGetErrorString(error));
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
};
|
|
#define cudaCheck(err) (cudaCheck(err, __FILE__, __LINE__))
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// CPU code reference
|
|
|
|
void attention_forward_cpu(float* out, float* preatt, float* att,
|
|
float* inp,
|
|
int B, int T, int C, int NH) {
|
|
// input is (B, T, 3C) Q,K,V
|
|
// preatt, att are (B, NH, T, T)
|
|
// output is (B, T, C)
|
|
int C3 = C*3;
|
|
int hs = C / NH; // head size
|
|
float scale = 1.0 / sqrtf(hs);
|
|
|
|
for (int b = 0; b < B; b++) {
|
|
for (int t = 0; t < T; t++) {
|
|
for (int h = 0; h < NH; h++) {
|
|
float* query_t = inp + b * T * C3 + t * C3 + h * hs;
|
|
float* preatt_bth = preatt + b*NH*T*T + h*T*T + t*T;
|
|
float* att_bth = att + b*NH*T*T + h*T*T + t*T;
|
|
|
|
// pass 1: calculate query dot key and maxval
|
|
float maxval = -10000.0f; // TODO something better
|
|
for (int t2 = 0; t2 <= t; t2++) {
|
|
float* key_t2 = inp + b * T * C3 + t2 * C3 + h * hs + C; // +C because it's key
|
|
|
|
// (query_t) dot (key_t2)
|
|
float val = 0.0f;
|
|
for (int i = 0; i < hs; i++) {
|
|
val += query_t[i] * key_t2[i];
|
|
}
|
|
val *= scale;
|
|
if (val > maxval) {
|
|
maxval = val;
|
|
}
|
|
|
|
preatt_bth[t2] = val;
|
|
}
|
|
// pad with -INFINITY outside of autoregressive region for debugging comparisons
|
|
for (int t2 = t+1; t2 < T; t2++) {
|
|
preatt_bth[t2] = -INFINITY;
|
|
}
|
|
|
|
// pass 2: calculate the exp and keep track of sum
|
|
float expsum = 0.0f;
|
|
for (int t2 = 0; t2 <= t; t2++) {
|
|
float expv = expf(preatt_bth[t2] - maxval);
|
|
expsum += expv;
|
|
att_bth[t2] = expv;
|
|
}
|
|
float expsum_inv = expsum == 0.0f ? 0.0f : 1.0f / expsum;
|
|
|
|
// pass 3: normalize to get the softmax
|
|
for (int t2 = 0; t2 < T; t2++) {
|
|
if (t2 <= t) {
|
|
att_bth[t2] *= expsum_inv;
|
|
} else {
|
|
// causal attention mask. not strictly necessary to set to zero here
|
|
// only doing this explicitly for debugging and checking to PyTorch
|
|
att_bth[t2] = 0.0f;
|
|
}
|
|
}
|
|
|
|
// pass 4: accumulate weighted values into the output of attention
|
|
float* out_bth = out + b * T * C + t * C + h * hs;
|
|
for (int i = 0; i < hs; i++) { out_bth[i] = 0.0f; }
|
|
for (int t2 = 0; t2 <= t; t2++) {
|
|
float* value_t2 = inp + b * T * C3 + t2 * C3 + h * hs + C*2; // +C*2 because it's value
|
|
float att_btht2 = att_bth[t2];
|
|
for (int i = 0; i < hs; i++) {
|
|
out_bth[i] += att_btht2 * value_t2[i];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// GPU kernels
|
|
|
|
__global__ void attention_query_key_kernel1(float* preatt, float* inp,
|
|
int B, int T, int C, int NH) {
|
|
int idx = blockIdx.x * blockDim.x + threadIdx.x;
|
|
int total_threads = B * NH * T * T;
|
|
|
|
if (idx < total_threads) {
|
|
int t2 = idx % T;
|
|
int t = (idx / T) % T;
|
|
if (t2 > t) {
|
|
// autoregressive mask
|
|
preatt[idx] = -INFINITY;
|
|
return;
|
|
}
|
|
int h = (idx / (T * T)) % NH;
|
|
int b = idx / (NH * T * T);
|
|
|
|
int C3 = C*3;
|
|
int hs = C / NH; // head size
|
|
float* query_t = inp + b * T * C3 + t * C3 + h * hs;
|
|
float* key_t2 = inp + b * T * C3 + t2 * C3 + h * hs + C; // +C because it's key
|
|
|
|
// (query_t) dot (key_t2)
|
|
float val = 0.0f;
|
|
for (int i = 0; i < hs; i++) {
|
|
val += query_t[i] * key_t2[i];
|
|
}
|
|
val *= 1.0 / sqrtf(hs);
|
|
|
|
preatt[idx] = val;
|
|
}
|
|
}
|
|
|
|
__global__ void attention_softmax_kernel1(float* att, float* preatt,
|
|
int B, int T, int NH) {
|
|
int idx = blockIdx.x * blockDim.x + threadIdx.x;
|
|
int total_threads = B * T * NH;
|
|
|
|
if (idx < total_threads) {
|
|
int h = idx % NH;
|
|
int t = (idx / NH) % T;
|
|
int b = idx / (NH * T);
|
|
|
|
float* preatt_bth = preatt + b*NH*T*T + h*T*T + t*T;
|
|
float* att_bth = att + b*NH*T*T + h*T*T + t*T;
|
|
|
|
// find maxval
|
|
float maxval = -10000.0f; // TODO something better
|
|
for (int t2 = 0; t2 <= t; t2++) {
|
|
if (preatt_bth[t2] > maxval) {
|
|
maxval = preatt_bth[t2];
|
|
}
|
|
}
|
|
|
|
// calculate the exp and keep track of sum
|
|
float expsum = 0.0f;
|
|
for (int t2 = 0; t2 <= t; t2++) {
|
|
float expv = expf(preatt_bth[t2] - maxval);
|
|
expsum += expv;
|
|
att_bth[t2] = expv;
|
|
}
|
|
float expsum_inv = expsum == 0.0f ? 0.0f : 1.0f / expsum;
|
|
|
|
// normalize to get the softmax
|
|
for (int t2 = 0; t2 < T; t2++) {
|
|
if (t2 <= t) {
|
|
att_bth[t2] *= expsum_inv;
|
|
} else {
|
|
// causal attention mask. not strictly necessary to set to zero here
|
|
// only doing this explicitly for debugging and checking to PyTorch
|
|
att_bth[t2] = 0.0f;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// warp-level reduction for finding the maximum value
|
|
__device__ float warpReduceMax(float val) {
|
|
for (int offset = 16; offset > 0; offset /= 2) {
|
|
val = fmaxf(val, __shfl_down_sync(0xFFFFFFFF, val, offset));
|
|
}
|
|
return val;
|
|
}
|
|
|
|
// warp-level reduction for summing values
|
|
__device__ float warpReduceSum(float val) {
|
|
for (int offset = 16; offset > 0; offset /= 2) {
|
|
val += __shfl_down_sync(0xFFFFFFFF, val, offset);
|
|
}
|
|
return val;
|
|
}
|
|
|
|
__global__ void softmax_forward_kernel4(float* out, float* inp, int N, int C) {
|
|
// out is (N, C) just like inp. Each row of inp will get softmaxed.
|
|
// same as kernel3, but can handle any block size (multiple of 32)
|
|
// each row of C elements is handled by block_size threads
|
|
// furthermore, each block_size threads get executed in warps of 32 threads
|
|
|
|
// special reduction operations warpReduceMax/warpReduceSum are used for intra-warp reductions
|
|
// shared memory is used for inter-warp reduction
|
|
extern __shared__ float shared[];
|
|
int idx = blockIdx.x;
|
|
int tid = threadIdx.x;
|
|
int warpId = threadIdx.x / 32; // warp index within a block
|
|
int laneId = threadIdx.x % 32; // thread index within a warp
|
|
|
|
// the number of warps per block. recall that blockDim.x is block_size
|
|
int warpsPerBlock = blockDim.x / 32;
|
|
|
|
// shared[] must be allocated to have 2 * warpsPerBlock elements
|
|
// first half for max values, the second half for sum values
|
|
float* maxvals = shared;
|
|
float* sumvals = &shared[warpsPerBlock];
|
|
|
|
// one row of inp, i.e. inp[idx, :] of shape (C,)
|
|
float* x = inp + idx * C;
|
|
|
|
// first, thread coarsening by directly accessing global memory in series
|
|
float maxval = -INFINITY;
|
|
for (int i = tid; i < C; i += blockDim.x) {
|
|
maxval = fmaxf(maxval, x[i]);
|
|
}
|
|
// now within-warp reductions for maxval
|
|
maxval = warpReduceMax(maxval);
|
|
|
|
// the 0th thread of each warp writes the maxval of that warp to shared memory
|
|
if (laneId == 0) maxvals[warpId] = maxval;
|
|
__syncthreads();
|
|
|
|
// now the 0th thread reduces the maxvals in shared memory, i.e. across warps
|
|
if (tid == 0) {
|
|
float val = maxvals[tid];
|
|
for (int i = 1; i < warpsPerBlock; i++) {
|
|
val = fmaxf(val, maxvals[i]);
|
|
}
|
|
// store the final max in the first position
|
|
maxvals[0] = val;
|
|
}
|
|
__syncthreads();
|
|
// broadcast the max to all threads
|
|
float offset = maxvals[0];
|
|
|
|
// compute expf and write the result to global memory
|
|
for (int i = tid; i < C; i += blockDim.x) {
|
|
// subtract max for numerical stability
|
|
out[idx * C + i] = expf(x[i] - offset);
|
|
}
|
|
|
|
// okay now we calculated exp(x - max(x))
|
|
// step 2: sum all the values and divide by the sum
|
|
|
|
// thread coarsening for sum
|
|
x = out + idx * C;
|
|
float sumval = 0.0f;
|
|
for (int i = tid; i < C; i += blockDim.x) {
|
|
sumval += x[i];
|
|
}
|
|
// within-warp reduction for sumval
|
|
sumval = warpReduceSum(sumval);
|
|
|
|
// write sumval to shared memory
|
|
if (laneId == 0) sumvals[warpId] = sumval;
|
|
__syncthreads();
|
|
|
|
// inter-thread reduction of sum
|
|
if (tid == 0) {
|
|
float val = sumvals[tid];
|
|
for (int i = 1; i < warpsPerBlock; ++i) {
|
|
val += sumvals[i];
|
|
}
|
|
sumvals[0] = val;
|
|
}
|
|
__syncthreads();
|
|
// broadcast the sum to all threads
|
|
float sum = sumvals[0];
|
|
|
|
// divide the whole row by the sum
|
|
for (int i = tid; i < C; i += blockDim.x) {
|
|
out[idx * C + i] = x[i] / sum;
|
|
}
|
|
}
|
|
|
|
__global__ void attention_value_kernel1(float* out, float* att, float* inp,
|
|
int B, int T, int C, int NH) {
|
|
int idx = blockIdx.x * blockDim.x + threadIdx.x;
|
|
int total_threads = B * T * NH;
|
|
|
|
if (idx < total_threads) {
|
|
int h = idx % NH;
|
|
int t = (idx / NH) % T;
|
|
int b = idx / (NH * T);
|
|
|
|
int C3 = C*3;
|
|
int hs = C / NH; // head size
|
|
|
|
float* out_bth = out + b * T * C + t * C + h * hs;
|
|
float* att_bth = att + b*NH*T*T + h*T*T + t*T;
|
|
|
|
for (int i = 0; i < hs; i++) { out_bth[i] = 0.0f; }
|
|
for (int t2 = 0; t2 <= t; t2++) {
|
|
float* value_t2 = inp + b * T * C3 + t2 * C3 + h * hs + C*2; // +C*2 because it's value
|
|
float att_btht2 = att_bth[t2];
|
|
for (int i = 0; i < hs; i++) {
|
|
out_bth[i] += att_btht2 * value_t2[i];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
__global__
|
|
void attention_forward_kernel2(
|
|
const float* Q,
|
|
const float* K,
|
|
const float* V,
|
|
const int N,
|
|
const int d,
|
|
const int Tc,
|
|
const int Tr,
|
|
const int Bc,
|
|
const int Br,
|
|
const float softmax_scale,
|
|
float* l,
|
|
float* m,
|
|
float* O
|
|
) {
|
|
int tx = threadIdx.x;
|
|
int bx = blockIdx.x; int by = blockIdx.y; // batch and head index
|
|
|
|
// Offset into Q,K,V,O,l,m - different for each batch and head
|
|
int qkv_offset = (bx * gridDim.y * N * d) + (by * N * d); // gridDim.y = nh
|
|
int lm_offset = (bx * gridDim.y * N) + (by * N); // offset for l and m
|
|
|
|
// Define SRAM for Q,K,V,S
|
|
extern __shared__ float sram[];
|
|
int tile_size = Bc * d; // size of Qi, Kj, Vj
|
|
float* Qi = sram;
|
|
float* Kj = &sram[tile_size];
|
|
float* Vj = &sram[tile_size * 2];
|
|
float* S = &sram[tile_size * 3];
|
|
|
|
for (int j = 0; j < Tc; j++) {
|
|
|
|
// Load Kj, Vj to SRAM
|
|
for (int x = 0; x < d; x++) {
|
|
Kj[(tx * d) + x] = K[qkv_offset + (tile_size * j) + (tx * d) + x];
|
|
Vj[(tx * d) + x] = V[qkv_offset + (tile_size * j) + (tx * d) + x];
|
|
}
|
|
__syncthreads(); // such that the inner loop can use the correct Kj, Vj
|
|
|
|
for (int i = 0; i < Tr; i++) {
|
|
// if past the end of the sequence, break
|
|
if (i * Br + tx >= N) {
|
|
break;
|
|
}
|
|
|
|
// Load Qi to SRAM, l and m to registers
|
|
for (int x = 0; x < d; x++) {
|
|
Qi[(tx * d) + x] = Q[qkv_offset + (tile_size * i) + (tx * d) + x];
|
|
}
|
|
float row_m_prev = m[lm_offset + (Br * i) + tx];
|
|
float row_l_prev = l[lm_offset + (Br * i) + tx];
|
|
|
|
// S = QK^T, row_m = rowmax(S)
|
|
// S[tx][y] = Sum_{x = 0}^{d-1} {Qi[tx][x] * Kj[y][x]}
|
|
// row_m = Max_{y = 0}^{Bc-1} S[tx][y]
|
|
// with causal masking
|
|
float row_m = -INFINITY;
|
|
for (int y = 0; y < Bc; y++) {
|
|
if (j * Bc + y >= N) {
|
|
break;
|
|
}
|
|
float sum = 0;
|
|
for (int x = 0; x < d; x++) {
|
|
sum += Qi[(tx * d) + x] * Kj[(y * d) + x];
|
|
}
|
|
sum *= softmax_scale;
|
|
if (i * Br + tx < j * Bc + y)
|
|
sum = -INFINITY;
|
|
S[(Bc * tx) + y] = sum;
|
|
|
|
if (sum > row_m)
|
|
row_m = sum;
|
|
}
|
|
|
|
// implement softmax with causal masking
|
|
// P = exp(S - row_m), row_l = rowsum(P)
|
|
// P[tx][y] = exp(S[tx][y] - row_m)
|
|
float row_l = 0;
|
|
for (int y = 0; y < Bc; y++) {
|
|
if (j * Bc + y >= N) {
|
|
break;
|
|
}
|
|
if (i * Br + tx < j * Bc + y)
|
|
S[(Bc * tx) + y] = 0;
|
|
else
|
|
S[(Bc * tx) + y] = __expf(S[(Bc * tx) + y] - row_m);
|
|
row_l += S[(Bc * tx) + y];
|
|
}
|
|
|
|
// Compute new m and l
|
|
float row_m_new = max(row_m_prev, row_m);
|
|
float row_l_new = (__expf(row_m_prev - row_m_new) * row_l_prev) + (__expf(row_m - row_m_new) * row_l);
|
|
|
|
// Write O, l, m to HBM
|
|
for (int x = 0; x < d; x++) {
|
|
float pv = 0; // Pij * Vj
|
|
for (int y = 0; y < Bc; y++) {
|
|
if (j * Bc + y >= N) {
|
|
break;
|
|
}
|
|
pv += S[(Bc * tx) + y] * Vj[(y * d) + x];
|
|
}
|
|
O[qkv_offset + (tile_size * i) + (tx * d) + x] = (1 / row_l_new) \
|
|
* ((row_l_prev * __expf(row_m_prev - row_m_new) * O[qkv_offset + (tile_size * i) + (tx * d) + x]) \
|
|
+ (__expf(row_m - row_m_new) * pv));
|
|
}
|
|
m[lm_offset + (Br * i) + tx] = row_m_new;
|
|
l[lm_offset + (Br * i) + tx] = row_l_new;
|
|
}
|
|
__syncthreads(); // otherwise, thread can use the wrong Kj, Vj in inner loop
|
|
}
|
|
}
|
|
|
|
__global__ void permute_kernel(float* q, float* k, float* v,
|
|
const float* inp,
|
|
int B, int N, int NH, int d) {
|
|
// okay so now, this kernel wants Q,K,V to all be of shape (B, NH, N, d)
|
|
// but instead, we have a single tensor QKV (inp) of shape (B, N, 3, NH, d)
|
|
int idx = blockIdx.x * blockDim.x + threadIdx.x;
|
|
|
|
// Q[b][nh_][n][d_] = inp[b][n][0][nh_][d_]
|
|
|
|
if (idx < B * NH * N * d) {
|
|
int b = idx / (NH * N * d);
|
|
int rest = idx % (NH * N * d);
|
|
int nh_ = rest / (N * d);
|
|
rest = rest % (N * d);
|
|
int n = rest / d;
|
|
int d_ = rest % d;
|
|
|
|
int inp_idx = \
|
|
(b * N * 3 * NH * d)
|
|
+ (n * 3 * NH * d)
|
|
+ (0 * NH * d)
|
|
+ (nh_ * d)
|
|
+ d_;
|
|
|
|
q[idx] = inp[inp_idx];
|
|
k[idx] = inp[inp_idx + NH * d];
|
|
v[idx] = inp[inp_idx + 2 * (NH * d)];
|
|
}
|
|
}
|
|
|
|
__global__ void unpermute_kernel(float* inp, float *out, int B, int N, int NH, int d) {
|
|
// out has shape (B, nh, N, d) but we need to unpermute it to (B, N, nh, d)
|
|
int idx = blockIdx.x * blockDim.x + threadIdx.x;
|
|
|
|
// out[b][n][nh_][d_] <- inp[b][nh_][n][d_]
|
|
if (idx < B * NH * N * d) {
|
|
int b = idx / (NH * N * d);
|
|
int rest = idx % (NH * N * d);
|
|
int nh_ = rest / (N * d);
|
|
rest = rest % (N * d);
|
|
int n = rest / d;
|
|
int d_ = rest % d;
|
|
|
|
int other_idx = (b * NH * N * d) + (n * NH * d) + (nh_ * d) + d_;
|
|
out[other_idx] = inp[idx];
|
|
}
|
|
}
|
|
|
|
__global__ void scale_kernel(float* inp, float scale, int B, int NH, int T) {
|
|
// scales the pre-softmax attention scores by scale
|
|
// and sets the autoregressive locations to -INFINITY
|
|
int idx = blockIdx.x * blockDim.x + threadIdx.x;
|
|
if (idx < B * NH * T * T) {
|
|
int rest = idx % (NH * T * T);
|
|
rest = rest % (T * T);
|
|
int t2 = rest / T;
|
|
int t = rest % T;
|
|
if (t > t2) {
|
|
inp[idx] = -INFINITY;
|
|
} else {
|
|
inp[idx] *= scale;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// kernel launcher
|
|
|
|
void attention_forward1(float* out, float* preatt, float* att,
|
|
float* inp,
|
|
int B, int T, int C, int NH,
|
|
const int block_size) {
|
|
// attention calculation
|
|
int total_threads = B * NH * T * T;
|
|
int num_blocks = CEIL_DIV(total_threads, block_size);
|
|
attention_query_key_kernel1<<<num_blocks, block_size>>>(preatt, inp, B, T, C, NH);
|
|
// softmax and value accumulation
|
|
total_threads = B * T * NH;
|
|
num_blocks = CEIL_DIV(total_threads, block_size);
|
|
attention_softmax_kernel1<<<num_blocks, block_size>>>(att, preatt, B, T, NH);
|
|
attention_value_kernel1<<<num_blocks, block_size>>>(out, att, inp, B, T, C, NH);
|
|
}
|
|
|
|
|
|
void attention_forward2(float* out,
|
|
float* inp,
|
|
int B, int T, int C, int NH,
|
|
const int block_size) {
|
|
// TODO there should be no mallocs inside any of these functions!
|
|
// not fixing this because we don't intend to use attention_forward2,
|
|
// it seems to be way too slow as is
|
|
|
|
// these are hardcoded to 32 for now
|
|
const int Bc = 32;
|
|
const int Br = 32;
|
|
// renaming these to be consistent with the kernel
|
|
// const int B = B;
|
|
const int nh = NH;
|
|
const int N = T;
|
|
const int d = C / NH;
|
|
// more
|
|
const int Tc = ceil((float) N / Bc);
|
|
const int Tr = ceil((float) N / Br);
|
|
const float softmax_scale = 1.0 / sqrt(d);
|
|
// create some temporary memory
|
|
float* l;
|
|
float* m;
|
|
cudaCheck(cudaMalloc(&l, B * nh * N * sizeof(float)));
|
|
cudaCheck(cudaMalloc(&m, B * nh * N * sizeof(float)));
|
|
cudaCheck(cudaMemset(l, 0, B * nh * N * sizeof(float)));
|
|
cudaCheck(cudaMemset(m, -10000.0f, B * nh * N * sizeof(float)));
|
|
|
|
// calculate SRAM size needed per block, ensure we have enough shared memory
|
|
int col_tile_size = Bc * d; // size of Kj, Vj
|
|
int row_tile_size = Br * d; // size of Qi
|
|
const int sram_size =
|
|
(2 * col_tile_size * sizeof(float)) // SRAM size for Kj, Vj
|
|
+ (row_tile_size * sizeof(float)) // SRAM size for Qi
|
|
+ (Bc * Br * sizeof(float)); // SRAM size for S
|
|
int max_sram_size;
|
|
cudaDeviceGetAttribute(&max_sram_size, cudaDevAttrMaxSharedMemoryPerBlock, 0);
|
|
if (sram_size > max_sram_size) {
|
|
printf("Max shared memory: %d, requested shared memory: %d \n", max_sram_size, sram_size);
|
|
printf("SRAM size exceeds maximum shared memory per block\n");
|
|
printf("Try decreasing col_tile_size or row_tile_size further\n");
|
|
exit(1);
|
|
}
|
|
|
|
// grid and block dims
|
|
dim3 grid_dim(B, nh); // batch_size x num_heads
|
|
dim3 block_dim(Br); // Br threads per block
|
|
|
|
// okay so now, this kernel wants Q,K,V to all be of shape (B, nh, N, d)
|
|
// but instead, we have a single tensor QKV (inp) of shape (B, N, 3, nh, d)
|
|
// so we have to permute the tensor using a kernel with block_size
|
|
float *q, *k, *v;
|
|
cudaCheck(cudaMalloc(&q, B * T * C * sizeof(float)));
|
|
cudaCheck(cudaMalloc(&k, B * T * C * sizeof(float)));
|
|
cudaCheck(cudaMalloc(&v, B * T * C * sizeof(float)));
|
|
int total_threads = B * N * nh * d;
|
|
int num_blocks = CEIL_DIV(total_threads, block_size);
|
|
permute_kernel<<<num_blocks, block_size>>>(q, k, v, inp, B, N, nh, d);
|
|
|
|
// now actually call the flash attention kernel
|
|
attention_forward_kernel2<<<grid_dim, block_dim, sram_size>>>(
|
|
q, k, v,
|
|
N, d, Tc, Tr, Bc, Br, softmax_scale,
|
|
l, m, out
|
|
);
|
|
|
|
// out has shape (B, nh, N, d) but we need to unpermute it to (B, N, nh, d)
|
|
unpermute_kernel<<<num_blocks, block_size>>>(out, q, B, N, nh, d);
|
|
cudaCheck(cudaMemcpy(out, q, B * T * C * sizeof(float), cudaMemcpyDeviceToDevice));
|
|
|
|
// free memory
|
|
cudaCheck(cudaFree(l));
|
|
cudaCheck(cudaFree(m));
|
|
cudaCheck(cudaFree(q));
|
|
cudaCheck(cudaFree(k));
|
|
cudaCheck(cudaFree(v));
|
|
}
|
|
|
|
void attention_forward3(float* out, float* vaccum, float* qkvr, float* preatt, float* att,
|
|
float* inp,
|
|
int B, int T, int C, int NH,
|
|
const int block_size) {
|
|
// inp is (B, T, 3C) QKV
|
|
// preatt, att are (B, NH, T, T)
|
|
// output is (B, T, C)
|
|
int HS = C / NH; // head size
|
|
|
|
// permute and separate inp from (B, T, 3, NH, HS) to 3X (B, NH, T, HS)
|
|
float *q, *k, *v;
|
|
q = qkvr + 0 * B * T * C;
|
|
k = qkvr + 1 * B * T * C;
|
|
v = qkvr + 2 * B * T * C;
|
|
int total_threads = B * NH * T * HS;
|
|
int num_blocks = CEIL_DIV(total_threads, block_size);
|
|
permute_kernel<<<num_blocks, block_size>>>(q, k, v, inp, B, T, NH, HS);
|
|
|
|
// batched matrix multiply with cuBLAS
|
|
cublasHandle_t handle;
|
|
cublasStatus_t stat = cublasCreate(&handle);
|
|
const float alpha = 1.0f;
|
|
const float beta = 0.0f;
|
|
stat = cublasSgemmStridedBatched(handle,
|
|
CUBLAS_OP_T, CUBLAS_OP_N,
|
|
T, T, HS,
|
|
&alpha,
|
|
k, HS, T * HS,
|
|
q, HS, T * HS,
|
|
&beta,
|
|
preatt, T, T * T,
|
|
B * NH);
|
|
if (stat != CUBLAS_STATUS_SUCCESS) {
|
|
printf("cublasSgemm failed\n");
|
|
exit(1);
|
|
}
|
|
|
|
// multiply all elements of preatt elementwise by scale
|
|
float scale = 1.0 / sqrtf(HS);
|
|
total_threads = B * NH * T * T;
|
|
num_blocks = CEIL_DIV(total_threads, block_size);
|
|
scale_kernel<<<num_blocks, block_size>>>(preatt, scale, B, NH, T);
|
|
|
|
// softmax. preatt is (B, NH, T, T) but we view it as (B * NH * T, T) and use the softmax kernel
|
|
int softmax_block_size = 256;
|
|
int grid_size = B * NH * T;
|
|
size_t shared_mem_size = 2 * softmax_block_size / 32 * sizeof(float);
|
|
softmax_forward_kernel4<<<grid_size, softmax_block_size, shared_mem_size>>>(att, preatt, B * NH * T, T);
|
|
|
|
// new approach: first cuBLAS another batched matmul
|
|
// y = att @ v # (B, nh, T, T) @ (B, nh, T, hs) -> (B, nh, T, hs)
|
|
stat = cublasSgemmStridedBatched(handle,
|
|
CUBLAS_OP_N, CUBLAS_OP_N,
|
|
HS, T, T,
|
|
&alpha,
|
|
v, HS, T * HS,
|
|
att, T, T * T,
|
|
&beta,
|
|
vaccum, HS, T * HS,
|
|
B * NH);
|
|
if (stat != CUBLAS_STATUS_SUCCESS) {
|
|
printf("cublasSgemm failed\n");
|
|
exit(1);
|
|
}
|
|
|
|
// now unpermute
|
|
// y = y.transpose(1, 2).contiguous().view(B, T, C) # re-assemble all head outputs side by side
|
|
num_blocks = CEIL_DIV(B * T * C, block_size);
|
|
unpermute_kernel<<<num_blocks, block_size>>>(vaccum, out, B, T, NH, HS);
|
|
|
|
// cleanups
|
|
cublasDestroy(handle);
|
|
}
|
|
|
|
// kernel version dispatch
|
|
void attention_forward(int kernel_num,
|
|
float* out, float* vaccum, float* qkvr, float* preatt, float* att,
|
|
float* inp,
|
|
int B, int T, int C, int NH,
|
|
const int block_size) {
|
|
switch (kernel_num) {
|
|
case 1:
|
|
attention_forward1(out, preatt, att, inp, B, T, C, NH, block_size);
|
|
break;
|
|
case 2:
|
|
attention_forward2(out, inp, B, T, C, NH, block_size);
|
|
break;
|
|
case 3:
|
|
attention_forward3(out, vaccum, qkvr, preatt, att, inp, B, T, C, NH, block_size);
|
|
break;
|
|
default:
|
|
printf("Invalid kernel number\n");
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// random utils
|
|
|
|
float* make_random_float(int N) {
|
|
float* arr = (float*)malloc(N * sizeof(float));
|
|
for (int i = 0; i < N; i++) {
|
|
arr[i] = ((float)rand() / RAND_MAX) * 2.0 - 1.0;
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
int main(int argc, char **argv) {
|
|
srand(0);
|
|
|
|
int B = 8;
|
|
int T = 1024;
|
|
int C = 768;
|
|
int NH = 12;
|
|
|
|
int deviceIdx = 0;
|
|
cudaCheck(cudaSetDevice(deviceIdx));
|
|
|
|
// create host memory of random numbers
|
|
float* out = (float*)malloc(B * T * C * sizeof(float));
|
|
float* preatt = (float*)malloc(B * NH * T * T * sizeof(float));
|
|
float* att = (float*)malloc(B * NH * T * T * sizeof(float));
|
|
float* inp = make_random_float(B * T * 3 * C);
|
|
|
|
// move to GPU
|
|
float* d_out;
|
|
float* d_vaccum;
|
|
float* d_qkvr;
|
|
float* d_preatt;
|
|
float* d_att;
|
|
float* d_inp;
|
|
cudaCheck(cudaMalloc(&d_out, B * T * C * sizeof(float)));
|
|
cudaCheck(cudaMalloc(&d_vaccum, B * T * C * sizeof(float)));
|
|
cudaCheck(cudaMalloc(&d_qkvr, B * T * 3 * C * sizeof(float)));
|
|
cudaCheck(cudaMalloc(&d_preatt, B * NH * T * T * sizeof(float)));
|
|
cudaCheck(cudaMalloc(&d_att, B * NH * T * T * sizeof(float)));
|
|
cudaCheck(cudaMalloc(&d_inp, B * T * 3 * C * sizeof(float)));
|
|
cudaCheck(cudaMemcpy(d_inp, inp, B * T * 3 * C * 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);
|
|
|
|
// first check the correctness of the kernel
|
|
attention_forward_cpu(out, preatt, att, inp, B, T, C, NH);
|
|
attention_forward(kernel_num, d_out, d_vaccum, d_qkvr, d_preatt, d_att, d_inp, B, T, C, NH, 256);
|
|
|
|
// compare the output
|
|
float* out_gpu = (float*)malloc(B * T * C * sizeof(float));
|
|
cudaCheck(cudaMemcpy(out_gpu, d_out, B * T * C * sizeof(float), cudaMemcpyDeviceToHost));
|
|
for (int i = 0; i < B * T * C; i++) {
|
|
// print the first few comparisons
|
|
if (i < 5) {
|
|
printf("%f %f\n", out[i], out_gpu[i]);
|
|
}
|
|
// ensure correctness for all elements
|
|
if (fabs(out[i] - out_gpu[i]) > 1e-4) {
|
|
printf("Mismatch at %d: %f vs %f\n", i, out[i], out_gpu[i]);
|
|
exit(1);
|
|
}
|
|
}
|
|
printf("Results match!\n");
|
|
|
|
// time the kernel at different block sizes
|
|
int block_sizes[] = {32, 64, 128, 256, 512};
|
|
|
|
for (int j = 0; j < sizeof(block_sizes) / sizeof(int); j++) {
|
|
int block_size = block_sizes[j];
|
|
|
|
int repeat_times = 10;
|
|
cudaEvent_t start, stop;
|
|
cudaCheck(cudaEventCreate(&start));
|
|
cudaCheck(cudaEventCreate(&stop));
|
|
cudaCheck(cudaEventRecord(start, 0));
|
|
for (int i = 0; i < repeat_times; i++) {
|
|
attention_forward(kernel_num, d_out, d_vaccum, d_qkvr, d_preatt, d_att, d_inp, B, T, C, NH, block_size);
|
|
}
|
|
cudaCheck(cudaEventRecord(stop, 0));
|
|
cudaCheck(cudaEventSynchronize(start));
|
|
cudaCheck(cudaEventSynchronize(stop));
|
|
float elapsed_time;
|
|
cudaCheck(cudaEventElapsedTime(&elapsed_time, start, stop));
|
|
|
|
printf("block_size %4d | time %f ms\n", block_size, elapsed_time);
|
|
}
|
|
|
|
// free memory
|
|
free(out);
|
|
free(preatt);
|
|
free(att);
|
|
free(inp);
|
|
free(out_gpu);
|
|
cudaCheck(cudaFree(d_out));
|
|
cudaCheck(cudaFree(d_vaccum));
|
|
cudaCheck(cudaFree(d_qkvr));
|
|
cudaCheck(cudaFree(d_preatt));
|
|
cudaCheck(cudaFree(d_att));
|
|
cudaCheck(cudaFree(d_inp));
|
|
|
|
return 0;
|
|
} |