본문 바로가기

SASS_Probe

reduce_sum_f32 분석

좋습니다. reduce_sum_f32부터는 이전 LDG → primitive → STG 단순 elementwise 패턴과 달라졌습니다. 핵심 결론은 이겁니다.

reduce_sum_f32는 register-only 누적이 아니라,
shared memory에 중간값을 반복적으로 materialize하면서 reduction한다.

관찰된 핵심 명령어는:

STS       shared memory store
LDS       shared memory load
FADD      누적
BAR.SYNC  thread block synchronization
BRA       reduction loop
STG       final global store

입니다. 특히 초기값을 shared memory에 저장하고 BAR.SYNC로 동기화한 뒤 reduction loop에 들어가는 구조가 보입니다.


reduce_sum_f32 분석

문서 위치 추천:

notes/03_reduction/reduce_sum_f32.md

1. CUDA 코드 구조

우리가 작성한 커널은 대략 이런 구조입니다.

__global__ void reduce_sum_f32_kernel(const float* __restrict__ x,
                                      float* __restrict__ y,
                                      int n) {
    extern __shared__ float sdata[];

    int tid = threadIdx.x;
    int i = blockIdx.x * blockDim.x + threadIdx.x;

    float v = 0.0f;

    if (i < n) {
        v = x[i];
    }

    sdata[tid] = v;
    __syncthreads();

    for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) {
        if (tid < stride) {
            sdata[tid] += sdata[tid + stride];
        }

        __syncthreads();
    }

    if (tid == 0) {
        y[blockIdx.x] = sdata[0];
    }
}

high-level 의미:

각 block이 x의 일부 구간을 shared memory로 가져온 뒤,
shared memory 안에서 tree reduction을 수행하고,
thread 0이 block sum을 y[blockIdx.x]에 저장한다.

2. 전체 SASS 역할 요약

SASS 흐름을 역할 단위로 압축하면 이렇게 됩니다.

R6 = blockIdx.x;
R7 = threadIdx.x;

R2 = blockIdx.x * blockDim.x + threadIdx.x;  // global i
R0 = 0.0f;

if (R2 < n) {
    R0 = x[R2];
}

sdata[tid] = R0;
barrier();

stride = blockDim.x >> 1;

while (stride != 0) {
    if (tid < stride) {
        lhs = sdata[tid];
        rhs = sdata[tid + stride];
        lhs = lhs + rhs;
        sdata[tid] = lhs;
    }

    barrier();
    stride >>= 1;
}

if (tid != 0) {
    exit;
}

R5 = sdata[0];
y[blockIdx.x] = R5;

이전 elementwise 커널과 결정적으로 다른 점은:

중간 결과가 register에만 머물지 않고 shared memory에 반복 저장된다.

입니다.


3. 초기 load + shared memory materialization

핵심 구간:

/*0010*/ S2R R6, SR_CTAID.X ;
/*0040*/ S2R R7, SR_TID.X ;
/*0050*/ IMAD R2, R6, c[0x0][0x0], R7 ;
/*0060*/ ISETP.GE.AND P0, PT, R2, c[0x0][0x170], PT ;
/*0070*/ @!P0 MOV R3, 0x4 ;
/*0080*/ @!P0 IMAD.WIDE R2, R2, R3, c[0x0][0x160] ;
/*0090*/ @!P0 LDG.E.CONSTANT R0, [R2.64] ;
/*00e0*/ STS [R7.X4], R0 ;
/*00f0*/ BAR.SYNC.DEFER_BLOCKING 0x0 ;

의미:

int tid = threadIdx.x;
int i = blockIdx.x * blockDim.x + tid;

float v = 0.0f;

if (i < n) {
    v = x[i];
}

sdata[tid] = v;
__syncthreads();

여기서 R0이 중요합니다.

R0 = 0.0f
if (i < n) R0 = x[i]
sdata[tid] = R0

즉 범위 밖 thread는 0.0f를 shared memory에 넣습니다. 범위 안 thread만 global load를 수행합니다. 이 부분은 @!P0 LDG로 predicate 처리되어 있습니다.


4. STS [R7.X4], R0 의미

/*00e0*/ STS [R7.X4], R0 ;

여기서:

R7 = threadIdx.x
R7.X4 = threadIdx.x * 4
R0 = v

따라서:

sdata[tid] = v;

입니다.

이게 첫 번째 중요한 materialization boundary입니다.

global memory x[i]
→ register R0
→ shared memory sdata[tid]

즉 x[i]를 읽은 뒤 바로 누적하지 않고 shared memory에 저장합니다.


5. BAR.SYNC 의미

/*00f0*/ BAR.SYNC.DEFER_BLOCKING 0x0 ;

CUDA 코드의:

__syncthreads();

에 대응합니다.

reduction에서 이 barrier는 필수입니다.

모든 thread가 sdata[tid]를 채우기 전까지,
다음 단계의 sdata[tid + stride] load를 하면 안 되기 때문입니다.

SASS에서 반복적으로 보이는 BAR.SYNC.DEFER_BLOCKING이 바로 block-level synchronization 지점입니다. 초기 shared store 뒤와 loop 내부 store 뒤에 barrier가 나타납니다.


6. reduction loop 구조

핵심 loop 구간:

/*0130*/ ISETP.GE.AND P1, PT, R7, R3, PT ;
/*0140*/ @!P1 LEA R2, R3, R0, 0x2 ;
/*0150*/ @!P1 LDS R4, [R7.X4] ;
/*0160*/ SHF.R.U32.HI R3, RZ, 0x1, R3 ;
/*0170*/ @!P1 LDS R5, [R2] ;
/*0180*/ @!P1 FADD.FTZ R4, R4, R5 ;
/*0190*/ @!P1 STS [R7.X4], R4 ;
/*01a0*/ BAR.SYNC.DEFER_BLOCKING 0x0 ;
/*01b0*/ ISETP.NE.AND P1, PT, R3, RZ, PT ;
/*01c0*/ @P1 BRA 0x130 ;

이건 CUDA의 이 부분에 해당합니다.

for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) {
    if (tid < stride) {
        sdata[tid] += sdata[tid + stride];
    }

    __syncthreads();
}

역할로 풀면:

if (tid < stride) {
    lhs = sdata[tid];
    rhs = sdata[tid + stride];
    lhs = lhs + rhs;
    sdata[tid] = lhs;
}

barrier();
stride >>= 1;

if (stride != 0) {
    goto loop;
}

LDS → LDS → FADD → STS가 reduction update의 핵심입니다.


7. shared memory accumulator update

reduction의 핵심 구간은 이것입니다.

/*0150*/ @!P1 LDS R4, [R7.X4] ;
/*0170*/ @!P1 LDS R5, [R2] ;
/*0180*/ @!P1 FADD.FTZ R4, R4, R5 ;
/*0190*/ @!P1 STS [R7.X4], R4 ;

의미:

float lhs = sdata[tid];
float rhs = sdata[tid + stride];
lhs = lhs + rhs;
sdata[tid] = lhs;

여기서 중요한 결론:

accumulator는 장기적으로 register에 유지되지 않는다.
각 stride마다 shared memory에서 읽고,
FADD 후 다시 shared memory에 저장된다.

즉 이 naive reduction은:

register accumulation

이라기보다:

shared memory materialized accumulation

입니다.


8. stride update

SASS에는 이런 명령이 보입니다.

/*00c0*/ USHF.R.U32.HI UR4, URZ, 0x1, UR4 ;
/*0160*/ SHF.R.U32.HI R3, RZ, 0x1, R3 ;

역할은 대략:

stride = blockDim.x >> 1;
stride >>= 1;

입니다.

처음에 blockDim.x를 읽고 절반으로 줄여 초기 stride를 만듭니다.

UR4 = blockDim.x >> 1

loop 안에서는:

R3 = R3 >> 1

로 다음 stride를 만듭니다.

USHF는 uniform register 쪽 shift, SHF는 일반 register 쪽 shift로 보면 됩니다.


9. tid < stride predicate

loop 안 조건은:

if (tid < stride)

입니다.

SASS에서는 반대 조건으로 predicate를 만듭니다.

/*0130*/ ISETP.GE.AND P1, PT, R7, R3, PT ;

의미:

P1 = (tid >= stride);

그 다음 실제 작업 명령어들은 모두 @!P1이 붙습니다.

@!P1 LDS ...
@!P1 LDS ...
@!P1 FADD ...
@!P1 STS ...

즉:

if (!(tid >= stride)) {
    // tid < stride
    sdata[tid] += sdata[tid + stride];
}

입니다.

여기서도 앞에서 봤던 패턴이 반복됩니다.

source 조건: tid < stride
SASS 조건: tid >= stride를 만든 뒤 @!P1로 실행

10. 최종 store

loop 종료 후:

/*01d0*/ @P0 EXIT ;
/*01e0*/ LDS R5, [RZ] ;
/*01f0*/ IMAD.MOV.U32 R3, RZ, RZ, 0x4 ;
/*0200*/ IMAD.WIDE.U32 R2, R6, R3, c[0x0][0x168] ;
/*0210*/ STG.E [R2.64], R5 ;
/*0220*/ EXIT ;

이 부분은 CUDA의:

if (tid == 0) {
    y[blockIdx.x] = sdata[0];
}

입니다.

앞에서:

/*00b0*/ ISETP.NE.AND P0, PT, R7, RZ, PT ;

로 P0 = (tid != 0)을 만들어둡니다.

그래서 loop가 끝난 뒤:

@P0 EXIT

즉:

if (tid != 0) return;

이 됩니다.

thread 0만 계속 진행해서:

R5 = sdata[0];
y[blockIdx.x] = R5;

를 수행합니다. 최종 결과는 shared memory에서 한 번 더 load된 뒤 global memory로 store됩니다.


11. register 흐름

R6

R6 = blockIdx.x
R6 = output block index

최종 store에서:

y[blockIdx.x]

주소 계산에 쓰입니다.

R7

R7 = threadIdx.x

대부분의 shared memory index로 쓰입니다.

STS [R7.X4], R0
LDS R4, [R7.X4]
STS [R7.X4], R4

즉:

sdata[tid]

입니다.

R2

초기: global index i
이후: x[i] address
loop 내부: sdata[tid + stride] address
최종: y[blockIdx.x] address

R2는 이 커널에서 의미가 가장 많이 바뀝니다.

R0

초기: 0.0f
조건부 load 후: x[i]
loop 내부: tid * 4 offset

초기에는 v 역할, 이후 loop에서는 주소 offset 계산에 재사용됩니다.

R3

초기: 4
loop: stride
최종: 4

R4

loop 초기: blockDim.x
loop 내부: sdata[tid] value
FADD 후: updated partial sum

R5

loop 내부: sdata[tid + stride]
최종: sdata[0]

12. 기존 elementwise와 비교

이전 커널들은 대략 이런 형태였습니다.

copy_global:
    LDG → STG

add_f32:
    LDG → LDG → FADD → STG

relu_f32:
    LDG → FMNMX → STG

clamp_f32:
    LDG → FSETP/FSEL → FSETP/FSEL → STG

하지만 reduce_sum_f32는 다릅니다.

reduce_sum_f32:
    LDG
    STS
    BAR.SYNC

    loop:
        LDS
        LDS
        FADD
        STS
        BAR.SYNC
        BRA

    LDS
    STG

즉 핵심 변화는:

global memory → register → shared memory → register → shared memory → ... → global memory

입니다.


13. materialization 관점 결론

이 커널은 우리가 세운 분석 프레임에서 매우 중요합니다.

CUDA source expression:
    sdata[tid] += sdata[tid + stride]

compiler graph rewrite:
    shared memory 기반 tree reduction loop

SASS primitive:
    LDS
    LDS
    FADD.FTZ
    STS
    BAR.SYNC
    BRA

intermediate materialization:
    있음.
    partial sum이 각 stride마다 shared memory에 저장됨.

특히 이 부분:

LDS R4, [R7.X4]
LDS R5, [R2]
FADD.FTZ R4, R4, R5
STS [R7.X4], R4

는 그대로:

sdata[tid] = sdata[tid] + sdata[tid + stride];

입니다.

그리고 STS가 있기 때문에 중간 partial sum은 register-only가 아니라 shared memory에 materialize됩니다.


14. 패턴 문서에 추가

notes/patterns/reduction_shared.md를 만들고 아래를 추가하면 됩니다.

# Pattern: Shared Memory Tree Reduction

## CUDA

```cpp
sdata[tid] = v;
__syncthreads();

for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) {
    if (tid < stride) {
        sdata[tid] += sdata[tid + stride];
    }

    __syncthreads();
}

Observed SASS role pattern

STS [tid * 4], v
BAR.SYNC

loop:
    ISETP.GE predicate, tid, stride
    @active LDS lhs, [tid * 4]
    @active LDS rhs, [(tid + stride) * 4]
    @active FADD partial, lhs, rhs
    @active STS [tid * 4], partial
    BAR.SYNC
    stride >>= 1
    if stride != 0: BRA loop

Meaning

if (tid < stride) {
    sdata[tid] = sdata[tid] + sdata[tid + stride];
}

Observed in

  • kernels/03_reduction/reduce_sum_f32.cu

Notes

  • partial sum이 register에 장기 유지되지 않는다.
  • 각 reduction step마다 shared memory에 저장된다.
  • __syncthreads()는 BAR.SYNC.DEFER_BLOCKING으로 나타난다.
  • 이 naive reduction은 shared memory materialization이 명확히 보이는 baseline이다.
---

## 15. 핵심 결론

`reduce_sum_f32`에서 얻은 결론은 이겁니다.

```text
단순 elementwise 커널은 대체로 LDG → primitive → STG 구조다.

하지만 naive reduction은:
LDG → STS → BAR.SYNC → LDS/LDS/FADD/STS 반복 → LDS → STG
구조로 내려간다.

그리고 가장 중요한 관찰:

partial sum은 register에 계속 누적되는 것이 아니라,
각 stride마다 shared memory에 저장된다.

이 커널은 앞으로 softmax_small_f32 분석의 기준선이 됩니다.
softmax의 max reduction, sum reduction도 결국 이와 비슷한 shared memory reduction 골격을 가질 가능성이 큽니다.

'SASS_Probe' 카테고리의 다른 글

online_softmax_f32 분석  (0) 2026.06.14
softmax_small_f32 분석  (0) 2026.06.14
clamp_f32 분석  (0) 2026.06.14
relu_f32 분석  (0) 2026.06.14
fma_contract_f32: FFMA Contraction Pattern Experiment  (0) 2026.06.13