733 lines
24 KiB
Plaintext
733 lines
24 KiB
Plaintext
/*
|
|
Kernels for softmax forward pass.
|
|
|
|
Compile example:
|
|
nvcc -O3 --use_fast_math -lcublas -lcublasLt softmax_forward.cu -o softmax_forward
|
|
|
|
version 1 is naive port from CPU code to kernel: parallelizes over B,T, loops over C
|
|
./softmax_forward 1
|
|
|
|
version 2 is a fused kernel that parallelizes over all of B,T,C
|
|
./softmax_forward 2
|
|
|
|
version 3 uses intra-warp reductions for maxval and sumval, must use block_size=32
|
|
./softmax_forward 3
|
|
|
|
version 4 uses both intra-warp reductions and shared memory for inter-warp reductions
|
|
so it can tolerate any block_size % 32 == 0. this is hopefully the most efficient version
|
|
./softmax_forward 4
|
|
|
|
version 5 is naive port from CPU code (softmax_online) to kernel: parallelizes over B,T, loops over C
|
|
./softmax_forward 5
|
|
|
|
version 6 is softmax_online that parallelizes over all of B,T,C
|
|
./softmax_forward 6
|
|
|
|
version 7 is softmax optimized for very large C.
|
|
./softmax_forward 7
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <assert.h>
|
|
#include <cuda_runtime.h>
|
|
#include <cooperative_groups.h>
|
|
#include <cooperative_groups/reduce.h>
|
|
#include "common.h"
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// CPU code reference
|
|
|
|
void softmax_forward_cpu(float* out, const float* inp, int N, int C) {
|
|
// inp is (N, C)
|
|
// out is (N, C), each row of inp will get softmaxed
|
|
for (int i = 0; i < N; i++) {
|
|
const float* inp_row = inp + i * C;
|
|
float* out_row = out + i * C;
|
|
|
|
float maxval = -INFINITY;
|
|
for (int j = 0; j < C; j++) {
|
|
if (inp_row[j] > maxval) {
|
|
maxval = inp_row[j];
|
|
}
|
|
}
|
|
// Note: since we want to ensure that the CUDA-kernels are accurate,
|
|
// we do this accumulation in higher precision, so we can be assured
|
|
// that our ground-truth is of high quality.
|
|
double sum = 0.0;
|
|
for (int j = 0; j < C; j++) {
|
|
out_row[j] = expf(inp_row[j] - maxval);
|
|
sum += out_row[j];
|
|
}
|
|
float norm = 1.f / (float)sum;
|
|
for (int j = 0; j < C; j++) {
|
|
out_row[j] *= norm;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// online version of softmax on CPU from the paper "Online normalizer calculation for softmax"
|
|
void softmax_forward_online_cpu(float* out, const float* inp, int N, int C) {
|
|
// inp is (N, C)
|
|
// out is (N, C), each row of inp will get softmaxed
|
|
for (int i = 0; i < N; i++) {
|
|
const float* inp_row = inp + i * C;
|
|
float* out_row = out + i * C;
|
|
|
|
float maxval = -INFINITY;
|
|
float sum = 0.0f;
|
|
for (int j = 0; j < C; j++) {
|
|
float maxval_prev = maxval;
|
|
if (inp_row[j] > maxval) {
|
|
maxval = inp_row[j];
|
|
sum = sum * expf(maxval_prev - maxval) + expf(inp_row[j] - maxval);
|
|
} else {
|
|
sum += expf(inp_row[j] - maxval);
|
|
}
|
|
}
|
|
|
|
for (int j = 0; j < C; j++) {
|
|
out_row[j] = expf(inp_row[j] - maxval) / sum;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// GPU kernels
|
|
|
|
__global__ void softmax_forward_kernel1(float* out, const float* inp, int N, int C) {
|
|
// inp is (N, C)
|
|
// out is (N, C), each row of inp will get softmaxed
|
|
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
|
if (i < N) {
|
|
const float* inp_row = inp + i * C;
|
|
float* out_row = out + i * C;
|
|
|
|
float maxval = -INFINITY;
|
|
for (int j = 0; j < C; j++) {
|
|
if (inp_row[j] > maxval) {
|
|
maxval = inp_row[j];
|
|
}
|
|
}
|
|
double sum = 0.0;
|
|
for (int j = 0; j < C; j++) {
|
|
out_row[j] = expf(inp_row[j] - maxval);
|
|
sum += out_row[j];
|
|
}
|
|
for (int j = 0; j < C; j++) {
|
|
out_row[j] /= (float)sum;
|
|
}
|
|
}
|
|
}
|
|
|
|
__global__ void softmax_forward_kernel2(float* out, const float* inp, int N, int C) {
|
|
// inp is (N, C)
|
|
// in each row of C elements, first calculates maxval, then returns expf(val - maxval)
|
|
extern __shared__ float shared[];
|
|
int idx = blockIdx.x; // ranges [0, N)
|
|
int tid = threadIdx.x; // ranges [0, block_size)
|
|
int block_size = blockDim.x;
|
|
const float* x = inp + idx * C; // idx-th row of inp
|
|
// thread coarsening
|
|
float maxval = -INFINITY;
|
|
for (int i = tid; i < C; i += block_size) {
|
|
maxval = fmaxf(maxval, x[i]);
|
|
}
|
|
shared[tid] = maxval;
|
|
// reductions
|
|
for (int stride = block_size / 2; stride >= 1; stride /= 2) {
|
|
__syncthreads();
|
|
if (tid < stride) {
|
|
shared[tid] = fmaxf(shared[tid], shared[tid + stride]);
|
|
}
|
|
}
|
|
__syncthreads();
|
|
float offset = shared[0];
|
|
// compute expf and write the result to global memory
|
|
for (int i = tid; i < C; i += block_size) {
|
|
out[idx * C + i] = expf(x[i] - offset);
|
|
}
|
|
__syncthreads();
|
|
// thread coarsening again, for the sum
|
|
x = out + idx * C; // idx-th row of out
|
|
float sumval = 0.0f;
|
|
for (int i = tid; i < C; i += block_size) {
|
|
sumval += x[i];
|
|
}
|
|
shared[tid] = sumval;
|
|
// reductions
|
|
for (int stride = block_size / 2; stride >= 1; stride /= 2) {
|
|
__syncthreads();
|
|
if (tid < stride) {
|
|
shared[tid] += shared[tid + stride];
|
|
}
|
|
}
|
|
// broadcast the sum to all threads in the block
|
|
__syncthreads();
|
|
float sum = shared[0];
|
|
// divide the input values by the sum
|
|
for (int i = tid; i < C; i += block_size) {
|
|
out[idx * C + i] = x[i] / sum;
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
__global__ void softmax_forward_kernel3(float* out, const float* inp, int N, int C) {
|
|
// kernel must use block size of 32
|
|
extern __shared__ float shared[];
|
|
int idx = blockIdx.x;
|
|
int tid = threadIdx.x;
|
|
const float* x = inp + idx * C;
|
|
|
|
// Thread coarsening and within-warp reduction for maxval
|
|
float maxval = -INFINITY;
|
|
for (int i = tid; i < C; i += blockDim.x) {
|
|
maxval = fmaxf(maxval, x[i]);
|
|
}
|
|
maxval = warpReduceMax(maxval);
|
|
|
|
// Broadcast maxval within the warp
|
|
float offset = __shfl_sync(0xFFFFFFFF, maxval, 0);
|
|
|
|
// Compute expf and write the result to global memory
|
|
for (int i = tid; i < C; i += blockDim.x) {
|
|
out[idx * C + i] = expf(x[i] - offset);
|
|
}
|
|
|
|
// Thread coarsening and within-warp reduction for sumval
|
|
x = out + idx * C;
|
|
float sumval = 0.0f;
|
|
for (int i = tid; i < C; i += blockDim.x) {
|
|
sumval += x[i];
|
|
}
|
|
// No need to broadcast sumval since all threads in the warp will have the same value
|
|
// (due to the fact that we're using __shfl_xor_sync)
|
|
sumval = warpReduceSum(sumval);
|
|
|
|
// Divide the input values by the sum
|
|
for (int i = tid; i < C; i += blockDim.x) {
|
|
out[idx * C + i] = x[i] / sumval;
|
|
}
|
|
}
|
|
|
|
__global__ void softmax_forward_kernel4(float* out, const 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 warpsPerBlock elements
|
|
// those will be used for max and sum values
|
|
float* max_or_sum_storage = shared;
|
|
|
|
// one row of inp, i.e. inp[idx, :] of shape (C,)
|
|
const 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) max_or_sum_storage[warpId] = maxval;
|
|
__syncthreads();
|
|
|
|
// now the 0th thread of the block reduces the max values in shared memory, i.e. across warps
|
|
if (tid == 0) {
|
|
float val = max_or_sum_storage[tid];
|
|
for (int i = 1; i < warpsPerBlock; i++) {
|
|
val = fmaxf(val, max_or_sum_storage[i]);
|
|
}
|
|
// store the final max in the first position
|
|
max_or_sum_storage[0] = val;
|
|
}
|
|
__syncthreads();
|
|
// broadcast the max to all threads
|
|
float offset = max_or_sum_storage[0];
|
|
|
|
// compute expf and write the result to global memory
|
|
for (int i = tid; i < C; i += blockDim.x) {
|
|
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) max_or_sum_storage[warpId] = sumval;
|
|
__syncthreads();
|
|
|
|
// inter-thread reduction of sum
|
|
if (tid == 0) {
|
|
float val = max_or_sum_storage[tid];
|
|
for (int i = 1; i < warpsPerBlock; ++i) {
|
|
val += max_or_sum_storage[i];
|
|
}
|
|
max_or_sum_storage[0] = val;
|
|
}
|
|
__syncthreads();
|
|
// broadcast the sum to all threads
|
|
float sum = max_or_sum_storage[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 softmax_forward_online_kernel1(float* out, const float* inp, int N, int C) {
|
|
// inp is (N, C)
|
|
// out is (N, C), each row of inp will get softmaxed
|
|
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
|
if (i < N) {
|
|
const float* inp_row = inp + i * C;
|
|
float* out_row = out + i * C;
|
|
|
|
float maxval = -INFINITY;
|
|
double sum = 0.0;
|
|
for (int j = 0; j < C; j++) {
|
|
float maxval_prev = maxval;
|
|
float current_val = inp_row[j];
|
|
if (current_val > maxval) {
|
|
maxval = current_val;
|
|
sum = sum * expf(maxval_prev - maxval) + expf(current_val - maxval);
|
|
}
|
|
else {
|
|
sum += expf(current_val - maxval);
|
|
}
|
|
}
|
|
|
|
for (int j = 0; j < C; j++) {
|
|
out_row[j] = expf(inp_row[j] - maxval) / sum;
|
|
}
|
|
}
|
|
}
|
|
|
|
// struct for the reduction operation, guarantees 8-byte alignment
|
|
struct __align__(8) SumMax
|
|
{
|
|
float maxval;
|
|
float sum;
|
|
};
|
|
|
|
// forceinline helps avoid function call overhead
|
|
__device__ __forceinline__ SumMax reduce_sum_max_op(SumMax a, SumMax b) {
|
|
bool a_bigger = (a.maxval > b.maxval);
|
|
SumMax bigger_m = a_bigger ? a : b;
|
|
SumMax smaller_m = a_bigger ? b : a;
|
|
SumMax res;
|
|
res.maxval = bigger_m.maxval;
|
|
res.sum = bigger_m.sum + smaller_m.sum * expf(smaller_m.maxval - bigger_m.maxval);
|
|
return res;
|
|
}
|
|
|
|
__global__ void softmax_forward_online_kernel2(float* out, const float* inp, int N, int C) {
|
|
namespace cg = cooperative_groups;
|
|
cg::thread_block block = cg::this_thread_block();
|
|
cg::thread_block_tile<32> warp = cg::tiled_partition<32>(block);
|
|
int idx = blockIdx.x * warp.meta_group_size() + warp.meta_group_rank();
|
|
if (idx >= N) {
|
|
return;
|
|
}
|
|
|
|
// one row of inp, i.e. inp[idx, :] of shape (C,)
|
|
const float* x = inp + idx * C;
|
|
|
|
// base case for the reduction
|
|
SumMax sm_partial;
|
|
sm_partial.maxval = -INFINITY;
|
|
sm_partial.sum = 0.0f;
|
|
|
|
// first, thread coarsening by directly accessing global memory in series
|
|
for (int i = warp.thread_rank(); i < C; i += warp.size()) {
|
|
sm_partial = reduce_sum_max_op(sm_partial, { x[i], 1.0f });
|
|
}
|
|
|
|
// second, the reduction
|
|
SumMax sm_total = cg::reduce(warp, sm_partial, reduce_sum_max_op);
|
|
|
|
// divide the whole row by the sum
|
|
for (int i = warp.thread_rank(); i < C; i += warp.size()) {
|
|
// the below is equivalent to
|
|
// out[idx * C + i] = expf(x[i] - sm_total.maxval) / sm_total.sum;
|
|
// but uses special instruction that bypasses the cache
|
|
__stcs(out + idx * C + i, expf(x[i] - sm_total.maxval) / sm_total.sum);
|
|
}
|
|
}
|
|
|
|
__global__ void softmax_forward_kernel7(float* out, const float* inp, int N, int C) {
|
|
// out is (N, C) just like inp. Each row of inp will get softmaxed.
|
|
// same as kernel4, but optimised for very large Cs with advanced unrolling
|
|
|
|
// The trick is to read into a register array (all indices known at compile time)
|
|
// and always read UNROLL_FACTOR values to maximise memory level parallelism
|
|
// even if we would be out of bounds, we set the index to min(C-1, idx)
|
|
// so we just do some unnecessary reads (obviously bad for small C)
|
|
// the writes are in a separate loop with a conditional check for out of bounds
|
|
// making it separate is necessary to convince the compiler to do the right thing
|
|
const int UNROLL_FACTOR = 8;
|
|
const int warpsPerBlock = blockDim.x / 32;
|
|
|
|
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
|
|
|
|
// 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];
|
|
|
|
if (tid >= C) {
|
|
maxvals[warpId] = -INFINITY;
|
|
sumvals[warpId] = 0.0f;
|
|
return;
|
|
}
|
|
|
|
const float* x = inp + idx * C; // input
|
|
float* y = out + idx * C; // output
|
|
|
|
// first, thread coarsening by directly accessing global memory in series
|
|
float maxval = -INFINITY;
|
|
for (int i = tid; i < C; i += blockDim.x * UNROLL_FACTOR) {
|
|
#pragma unroll
|
|
for (int u = 0; u < UNROLL_FACTOR; u++) {
|
|
maxval = fmaxf(maxval, x[min(C - 1, i + u*blockDim.x)]);
|
|
}
|
|
}
|
|
|
|
// 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];
|
|
#pragma unroll
|
|
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
|
|
// + thread coarsening for sum
|
|
float sumval = 0.0f;
|
|
for (int i = tid; i < C; i += blockDim.x * UNROLL_FACTOR) {
|
|
float reg_array[UNROLL_FACTOR];
|
|
#pragma unroll
|
|
for (int u = 0; u < UNROLL_FACTOR; u++) {
|
|
reg_array[u] = __ldcs(&x[min(C - 1, i + u*blockDim.x)]);
|
|
}
|
|
#pragma unroll
|
|
for (int u = 0; u < UNROLL_FACTOR; u++) {
|
|
if (i + u*blockDim.x < C) {
|
|
float output = expf(reg_array[u] - offset);
|
|
y[min(C - 1, i + u*blockDim.x)] = output; // compiler likes redundant min()?!
|
|
sumval += output; // combined into the same loop unlike kernel3
|
|
}
|
|
}
|
|
}
|
|
|
|
// okay now we calculated exp(x - max(x))
|
|
// step 2: sum all the values and divide by the sum
|
|
|
|
// 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];
|
|
#pragma unroll
|
|
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 * UNROLL_FACTOR) {
|
|
float reg_array[UNROLL_FACTOR];
|
|
#pragma unroll
|
|
for (int u = 0; u < UNROLL_FACTOR; u++) {
|
|
reg_array[u] = y[min(C - 1, i + u*blockDim.x)];
|
|
}
|
|
#pragma unroll
|
|
for (int u = 0; u < UNROLL_FACTOR; u++) {
|
|
if (i + u*blockDim.x < C) {
|
|
y[i + u*blockDim.x] = reg_array[u] / sum;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
__global__ void softmax_forward_online_kernel8(float* out, const float* inp, int N, int C) {
|
|
// online softmax paper: http://arxiv.org/abs/1805.02867
|
|
// online softmax reduces loops from 3 to 2
|
|
// which is done by calculating sumval and maxval in one loop
|
|
const int warpsPerBlock = blockDim.x / warpSize;
|
|
int tid = threadIdx.x;
|
|
|
|
if (tid >= C) {
|
|
return;
|
|
}
|
|
|
|
int warpId = tid / warpSize;
|
|
int laneId = tid % warpSize;
|
|
// one warp one row
|
|
int row = blockIdx.x * warpsPerBlock + warpId;
|
|
|
|
if (row >= N) {
|
|
return;
|
|
}
|
|
|
|
const float* x = inp + row * C;
|
|
float* const y = out + row * C;
|
|
|
|
// merge calculating maxval and sumval in one loop
|
|
// which is an arithmetic improvment from online softmax over normal softmax
|
|
float maxval = -INFINITY, sumval = 0.0f, bigger;
|
|
for (int i = laneId; i < C; i += warpSize) {
|
|
// when updating the maxval, dynamically updates the previous sumval by
|
|
// multiplying e^{previous_maxval - current_maxval}
|
|
bigger = fmaxf(maxval, x[i]);
|
|
sumval = sumval * expf(maxval - bigger) + expf(x[i] - bigger);
|
|
maxval = bigger;
|
|
}
|
|
|
|
// use warp functions instead of cooperative groups for better readibility
|
|
// calculate the warp wised maxval and sumval
|
|
float offsetMaxval, offsetSumval;
|
|
for (int offset = warpSize / 2; offset > 0; offset >>= 1) {
|
|
__syncwarp();
|
|
offsetMaxval = __shfl_down_sync(0xFFFFFFFF, maxval, offset);
|
|
offsetSumval = __shfl_down_sync(0xFFFFFFFF, sumval, offset);
|
|
if (offsetMaxval > maxval) {
|
|
sumval *= expf(maxval - offsetMaxval);
|
|
maxval = offsetMaxval;
|
|
} else {
|
|
offsetSumval *= expf(offsetMaxval - maxval);
|
|
}
|
|
sumval += offsetSumval;
|
|
}
|
|
|
|
// sync the warp wised maxval and sumval
|
|
// which are also the maxval and sumval of one row in C
|
|
maxval = __shfl_sync(0xFFFFFFFF, maxval, 0);
|
|
sumval = __shfl_sync(0xFFFFFFFF, sumval, 0);
|
|
|
|
for (int i = laneId; i < C; i += warpSize) {
|
|
y[i] = expf(x[i] - maxval) / sumval;
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// kernel launcher
|
|
|
|
void softmax_forward1(float* out, const float* inp, int N, int C, const int block_size) {
|
|
const int grid_size = ceil_div(N, block_size);
|
|
softmax_forward_kernel1<<<grid_size, block_size>>>(out, inp, N, C);
|
|
cudaCheck(cudaGetLastError());
|
|
}
|
|
|
|
void softmax_forward2(float* out, const float* inp, int N, int C, const int block_size) {
|
|
int grid_size = N;
|
|
size_t shared_mem_size = block_size * sizeof(float);
|
|
softmax_forward_kernel2<<<grid_size, block_size, shared_mem_size>>>(out, inp, N, C);
|
|
}
|
|
|
|
void softmax_forward3(float* out, const float* inp, int N, int C, int block_size) {
|
|
block_size = 32; // awkward but ok. this one only works with block size 32
|
|
int grid_size = N;
|
|
size_t shared_mem_size = block_size * sizeof(float);
|
|
softmax_forward_kernel3<<<grid_size, block_size, shared_mem_size>>>(out, inp, N, C);
|
|
}
|
|
|
|
void softmax_forward4(float* out, const float* inp, int N, int C, int block_size) {
|
|
int grid_size = N;
|
|
// for each warp in the block we need a float that will be used for both maxval and sumval
|
|
size_t shared_mem_size = block_size / 32 * sizeof(float);
|
|
softmax_forward_kernel4<<<grid_size, block_size, shared_mem_size>>>(out, inp, N, C);
|
|
}
|
|
|
|
void softmax_forward_online1(float* out, const float* inp, int N, int C, int block_size) {
|
|
const int grid_size = ceil_div(N, block_size);
|
|
softmax_forward_online_kernel1 <<<grid_size, block_size >>> (out, inp, N, C);
|
|
cudaCheck(cudaGetLastError());
|
|
}
|
|
|
|
void softmax_forward_online2(float* out, const float* inp, int N, int C, int block_size) {
|
|
const int grid_size = ceil_div(N * 32, block_size);
|
|
softmax_forward_online_kernel2 <<<grid_size, block_size >>> (out, inp, N, C);
|
|
cudaCheck(cudaGetLastError());
|
|
}
|
|
|
|
void softmax_forward7(float* out, const float* inp, int N, int C, int block_size) {
|
|
int grid_size = N;
|
|
size_t shared_mem_size = 2 * block_size / 32 * sizeof(float);
|
|
softmax_forward_kernel7<<<grid_size, block_size, shared_mem_size>>>(out, inp, N, C);
|
|
}
|
|
|
|
void softmax_forward_online8(float* out, const float* inp, int N, int C, int block_size) {
|
|
const int grid_size = ceil_div(N * 32, block_size);
|
|
softmax_forward_online_kernel8<<<grid_size, block_size>>>(out, inp, N, C);
|
|
cudaCheck(cudaGetLastError());
|
|
}
|
|
|
|
// kernel version dispatch
|
|
void softmax_forward(int kernel_num, float* out, const float* inp, int N, int C, const int block_size) {
|
|
switch (kernel_num) {
|
|
case 1:
|
|
softmax_forward1(out, inp, N, C, block_size);
|
|
break;
|
|
case 2:
|
|
softmax_forward2(out, inp, N, C, block_size);
|
|
break;
|
|
case 3:
|
|
softmax_forward3(out, inp, N, C, block_size);
|
|
break;
|
|
case 4:
|
|
softmax_forward4(out, inp, N, C, block_size);
|
|
break;
|
|
case 5:
|
|
softmax_forward_online1(out, inp, N, C, block_size);
|
|
break;
|
|
case 6:
|
|
softmax_forward_online2(out, inp, N, C, block_size);
|
|
break;
|
|
case 7:
|
|
softmax_forward7(out, inp, N, C, block_size);
|
|
break;
|
|
case 8:
|
|
softmax_forward_online8(out, inp, N, C, block_size);
|
|
break;
|
|
default:
|
|
printf("Invalid kernel number\n");
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
int main(int argc, char **argv) {
|
|
srand(0);
|
|
|
|
int B = 8;
|
|
int T = 1024;
|
|
int V = 50257;
|
|
|
|
int deviceIdx = 0;
|
|
cudaCheck(cudaSetDevice(deviceIdx));
|
|
|
|
// create host memory of random numbers
|
|
float* out = (float*)malloc(B * T * V * sizeof(float));
|
|
float* inp = make_random_float(B * T * V);
|
|
|
|
// make the input less uniformly random: Otherwise, all probabilities will be basically zero,
|
|
// and the tests are not actually meaningful.
|
|
const int* outliers = make_random_int(B * T * 3, V);
|
|
for(int k = 0; k < 3; ++k) {
|
|
for(int j = 0; j < B * T; ++j) {
|
|
inp[j * V + outliers[j*3 + k]] *= 20;
|
|
}
|
|
}
|
|
|
|
// move to GPU
|
|
float* d_out;
|
|
float* d_inp;
|
|
cudaCheck(cudaMalloc(&d_out, B * T * V * sizeof(float)));
|
|
cudaCheck(cudaMalloc(&d_inp, B * T * V * sizeof(float)));
|
|
cudaCheck(cudaMemcpy(d_inp, inp, B * T * V * 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);
|
|
|
|
int block_sizes[] = {32, 64, 128, 256, 512, 1024};
|
|
|
|
softmax_forward_cpu(out, inp, B * T, V);
|
|
{
|
|
float max_el = -INFINITY;
|
|
for(int i = 0; i < B * T * V; ++i) {
|
|
max_el = max(max_el, out[i]);
|
|
}
|
|
assert(max_el > 1e-4);
|
|
printf("Largest output is: %f\n", max_el);
|
|
}
|
|
|
|
// first check the correctness of the kernel
|
|
for (int j = 0; j < sizeof(block_sizes) / sizeof(int); j++) {
|
|
int block_size = block_sizes[j];
|
|
printf("Checking block size %d.\n", block_size);
|
|
softmax_forward(kernel_num, d_out, d_inp, B * T, V, block_size);
|
|
validate_result(d_out, out, "out", B * T * V, 1e-4f);
|
|
}
|
|
|
|
printf("All results match. Starting benchmarks.\n\n");
|
|
|
|
// time the kernel at different block sizes
|
|
for (int j = 0; j < sizeof(block_sizes) / sizeof(int); j++) {
|
|
int block_size = block_sizes[j];
|
|
|
|
int repeat_times = 100;
|
|
float elapsed_time = benchmark_kernel(repeat_times, softmax_forward,
|
|
kernel_num, d_out, d_inp, B * T, V, block_size
|
|
);
|
|
|
|
printf("block_size %4d | time %.4f ms | per token %.2f µs\n", block_size, elapsed_time, elapsed_time * 1'000 / (B*T));
|
|
}
|
|
|
|
// free memory
|
|
free(out);
|
|
free(inp);
|
|
free((void*)outliers);
|
|
cudaCheck(cudaFree(d_out));
|
|
cudaCheck(cudaFree(d_inp));
|
|
|
|
return 0;
|
|
} |