Naive Softmax Kernel

cudasoftmaxreductionattention

Softmax는 row의 score들을 probability로 바꾼다.

y_i = exp(x_i - max(x)) / sum_j exp(x_j - max(x))

Transformer attention에서는 score row마다 softmax를 적용한다.

세 단계

stable softmax는 보통 세 단계로 본다.

1. row maximum 찾기
2. sum(exp(x - max)) 계산
3. normalize

이 세 단계는 row 전체 값을 필요로 한다. 그래서 softmax는 단순 elementwise kernel이 아니다.

Naive row-wise kernel

가장 단순한 버전은 thread 하나가 row 하나를 맡는다.

__global__ void softmax_naive_kernel(
    const float* x,
    float* y,
    int rows,
    int cols
) {
    int row = blockIdx.x * blockDim.x + threadIdx.x;

    if (row < rows) {
        float max_val = x[row * cols];
        for (int col = 1; col < cols; col++) {
            max_val = fmaxf(max_val, x[row * cols + col]);
        }

        float sum = 0.0f;
        for (int col = 0; col < cols; col++) {
            sum += expf(x[row * cols + col] - max_val);
        }

        for (int col = 0; col < cols; col++) {
            y[row * cols + col] = expf(x[row * cols + col] - max_val) / sum;
        }
    }
}

왜 naive인가

이 kernel은 이해하기 쉽지만 row 하나를 thread 하나가 모두 처리한다. row가 길면 parallelism을 충분히 쓰지 못한다.

나중에는 여러 thread가 같은 row를 나누어 읽고, max와 sum을 reduction으로 계산한다.

확인

  • softmax가 elementwise가 아닌 이유는 무엇인가?
  • x - max를 한 뒤 exp를 계산하는가?
  • thread 하나가 row 하나를 맡는 naive 방식의 한계는 무엇인가?