CUDA 算子笔记 2

§ 参考资料 共 1 条

Softmax Attention

输入 $\boldsymbol Q_{M\times d}$、$\boldsymbol K_{N\times d}$ 和 $\boldsymbol V_{N\times d}$,计算 Attention:

$$ \mathrm{Attention}(\boldsymbol Q,\boldsymbol K,\boldsymbol V)=\mathrm{softmax}\left(\frac{\boldsymbol Q\boldsymbol K^T}{\sqrt d}\right)\boldsymbol V $$

其中,Softmax 为逐行计算

例子:

Input:
Q (2x4): [[1.0, 0.0, 0.0, 0.0],
          [0.0, 1.0, 0.0, 0.0]]
K (3x4): [[1.0, 0.0, 0.0, 0.0],
          [0.0, 1.0, 0.0, 0.0],]
          [0.0, 0.0, 1.0, 0.0]]
V (3x4): [[1.0, 2.0, 3.0, 4.0],
          [5.0, 6.0, 7.0, 8.0],]
          [9.0, 10.0, 11.0, 12.0]]

Output:
output (2x4): [[4.29, 5.29, 6.29, 7.29],
               [5.0, 6.0, 7.0, 8.0]]

最简单的做法就是三个 Kernel:

  • 转置 + 矩阵乘法,计算 $\boldsymbol Q\boldsymbol K^T/\sqrt d$
  • Softmax,每一行作为一个 Block,用不到 Global Memory
  • 矩阵乘法

代码:

#include <cuda_runtime.h>

__device__ __forceinline__ constexpr int compute_offset(int row, int col,
                                                        int cols) {
  return row * cols + col;
}

// A: M x K
// B: K x N
// => output: AB, M x N
// A: M x K
// B: K x N
// => output: AB^T, M x N
template <const int block_size, const bool transpose, const bool norm>
__global__ void multiple_kernel(const float* A, const float* B, float* output,
                                int M, int N, int K) {
  __shared__ float tile_A[block_size][block_size];
  __shared__ float tile_B[block_size][block_size];

  const auto col = threadIdx.x + blockIdx.x * blockDim.x;
  const auto row = threadIdx.y + blockIdx.y * blockDim.y;

  const auto local_row = threadIdx.y;
  const auto local_col = threadIdx.x;

  float sum = 0.0f;

#pragma unroll
  for (int i = 0; i < K; i += block_size) {
    if (row < M && local_col + i < K) {
      tile_A[local_row][local_col] = A[compute_offset(row, local_col + i, K)];
    } else {
      tile_A[local_row][local_col] = 0.0f;
    }

    if constexpr (transpose) {
      if (col < N && local_row + i < K) {
        tile_B[local_row][local_col] = B[compute_offset(col, local_row + i, K)];
      } else {
        tile_B[local_row][local_col] = 0.0f;
      }
    } else {
      if (local_row + i < K && col < N) {
        tile_B[local_row][local_col] = B[compute_offset(local_row + i, col, N)];
      } else {
        tile_B[local_row][local_col] = 0.0f;
      }
    }

    __syncthreads();

#pragma unroll
    for (int j = 0; j < block_size; j++) {
      sum = fmaf(tile_A[local_row][j], tile_B[j][local_col], sum);
    }

    __syncthreads();
  }

  if (row < M && col < N) {
    if constexpr (norm) {
      output[compute_offset(row, col, N)] = sum * rsqrtf(static_cast<float>(K));
    } else {
      output[compute_offset(row, col, N)] = sum;
    }
  }
}

struct SoftmaxState {
  float max;
  float sum;
};

__device__ __forceinline__ SoftmaxState init_state() {
  return {-INFINITY, 0.0f};
}

__device__ __forceinline__ SoftmaxState mono_state(float value) {
  return {value, 1.0f};
}

__device__ __forceinline__ SoftmaxState combine(SoftmaxState a,
                                                SoftmaxState b) {
  if (a.max == -INFINITY) {
    return b;
  }
  if (b.max == -INFINITY) {
    return a;
  }

  SoftmaxState out;
  out.max = fmaxf(a.max, b.max);
  out.sum = a.sum * __expf(a.max - out.max) + b.sum * __expf(b.max - out.max);
  return out;
}

__device__ __forceinline__ SoftmaxState warp_reduce(SoftmaxState state) {
#pragma unroll
  for (int offset = 16; offset > 0; offset >>= 1) {
    SoftmaxState other;
    other.max = __shfl_down_sync(0xffffffff, state.max, offset);
    other.sum = __shfl_down_sync(0xffffffff, state.sum, offset);
    state = combine(state, other);
  }
  return state;
}

template <const int block_size>
__device__ __forceinline__ SoftmaxState block_reduce(SoftmaxState state) {
  __shared__ SoftmaxState warp_results[block_size];

  const auto lane = threadIdx.x & 31;
  const auto warp = threadIdx.x >> 5;
  const auto num_warps = (blockDim.x + 31) >> 5;

  state = warp_reduce(state);

  if (lane == 0) {
    warp_results[warp] = state;
  }

  __syncthreads();

  SoftmaxState block_state = init_state();
  if (warp == 0) {
    if (lane < num_warps) {
      block_state = warp_results[lane];
    }

    block_state = warp_reduce(block_state);
  }

  return block_state;
}

// softmax(input)
template <const int block_size>
__global__ void softmax_kernel(float* input, int M, int N) {
  const auto row = blockIdx.x;

  if (row >= M) {
    return;
  }

  auto state = init_state();

#pragma unroll
  for (int col = threadIdx.x; col < N; col += blockDim.x) {
    const int idx = compute_offset(row, col, N);
    state = combine(state, mono_state(input[idx]));
  }

  state = block_reduce<block_size>(state);

  __shared__ SoftmaxState final_state;
  if (threadIdx.x == 0) {
    final_state = state;
  }
  __syncthreads();

#pragma unroll
  for (int col = threadIdx.x; col < N; col += blockDim.x) {
    const int idx = compute_offset(row, col, N);
    input[idx] = __expf(input[idx] - final_state.max) / final_state.sum;
  }
}

// Q, K, V, output are device pointers
// Q: M x d
// K: N x d
// QK^T: M x N
// V: N x d
// output: M x d
extern "C" void solve(const float* Q, const float* K, const float* V,
                      float* output, int M, int N, int d) {
  auto total_items = M * N;
  float* attention;
  cudaMalloc(&attention, total_items * sizeof(float));
  cudaMemset(attention, 0, total_items * sizeof(float));

  // QK^T / sqrt(d)
  {
    constexpr auto block_size = 32;
    const auto grid_dim = dim3((N + block_size - 1) / block_size,
                               (M + block_size - 1) / block_size);
    const auto block_dim = dim3(block_size, block_size);

    multiple_kernel<block_size, true, true>
        <<<grid_dim, block_dim>>>(Q, K, attention, M, N, d);
  }

  // row-independent softmax(attention)
  {
    constexpr auto block_size = 256;
    const auto grid_dim = dim3(M);
    const auto block_dim = dim3(block_size);

    softmax_kernel<block_size><<<grid_dim, block_dim>>>(attention, M, N);
  }

  // softmax(attention) * V
  {
    constexpr auto block_size = 32;
    const auto grid_dim = dim3((d + 31) / 32, (M + 31) / 32);
    const auto block_dim = dim3(32, 32);

    multiple_kernel<block_size, false, false>
        <<<grid_dim, block_dim>>>(attention, V, output, M, d, N);
  }

  cudaDeviceSynchronize();
}