gpumode · 강의 아카이브
《GPU Mode》 L002 2024 · JAN · 20 High priority transcript · available

Lecture 2 · Ch 1-3 PMPP book

Programming Massively Parallel Processors 첫 세 챕터를 한 시간 안에 다시 깔아주는 reading-group 버전. Andreas Köpf 가 “병렬 사고가 시퀀셜 사고와 어떻게 다른가” 부터 시작해서 vector add → RGB→grayscale → mean filter 의 세 커널로 grid/block/thread 와 host↔device 메모리 모델을 한 번에 보여주는 강의. PMPP 책을 처음 펴는 사람을 위한 안내선이자, 이미 책을 읽은 사람을 위한 코드-우선 요약.

grid / block / thread host ↔ device cudaMalloc / Memcpy __global__ · __device__ · __host__ RGB→grayscale mean filter PTX PMPP Ch.1–3
A
Speaker
Andreas Köpf
Open Assistant · GPU Mode 운영진 · PMPP 리딩 그룹 호스트
강의 번호
L002
스피커
Andreas Köpf
학습 우선순위
High · 정독
다시 볼 때
세 커널을 직접 빌드
§ 01강의가 풀려는 문제· Why this lecture exists

“책 첫 세 챕터” 라는 가장 흔한 진입 장벽을 한 시간으로 압축

GPU Mode 의 reading-group 흐름은 Programming Massively Parallel Processors (Hwu, Kirk, El Hajj 의 PMPP) 를 같이 읽는 데서 시작했다. L002 는 그 책의 처음 세 챕터를 한 시간으로 깐다 — 처음 책을 펴는 사람에게는 가이드라인을 주고, 이미 읽은 사람에게는 코드 위주의 요약을 준다.

강의가 답하려는 질문은 명확하다.

  1. 병렬 사고가 시퀀셜 사고와 어떻게 다른가 — “문제를 어떻게 thread 들에 나눠 줄 것인가” 의 첫 결정.
  2. host(CPU) 와 device(GPU) 메모리는 어떻게 분리돼 있고, 어떻게 옮기는가cudaMalloc, cudaMemcpy 의 첫 사용.
  3. grid · block · thread 의 좌표계가 왜 그런 식으로 설계됐는가blockIdx · blockDim · threadIdx 로 indexing 하는 그 익숙한 한 줄의 의미.

이 세 질문을 추상으로 답하지 않고 — Andreas 가 깐 방식은 세 개의 커널을 차례로 보여주는 것이다. vector_addition.cu (1D, 일반 CUDA C 프로그램), RGB→grayscale (2D, PyTorch extension), mean filter (한 thread 가 여러 입력을 읽는 stencil). 각 커널이 직전 커널이 답하지 못한 새 차원을 하나씩 추가한다.

이 강의의 인지적 역할

L001 이 “이미 도는 코드를 어떻게 보는가”(profiling) 의 강의였다면, L002 는 그 코드 안에 어떤 객체들이 들어 있는가의 강의다. thread, block, grid, host pointer, device pointer, __global__, kernel launch <<<…>>> — 이 단어들이 처음 등장해서 자기 자리를 잡는다.

“병렬 알고리즘은 시퀀셜 알고리즘을 그냥 N등분 한 게 아니다. 독립적으로 실행될 수 있는 일의 단위로 다시 분해해야 한다.”Andreas Köpf · 학습 노트
§ 02병렬 사고가 다른 이유· embarrassingly parallel

“각 thread 는 출력 하나만 본다” — 가장 단순한 분해 패턴

PMPP 가 제일 먼저 못 박는 사실. 한 thread 가 한 출력 element 를 만든다는 분해가 GPU 의 자연스러운 첫 번째 패턴이다. vector add, RGB→grayscale 모두 이 모델 안에 들어간다 — 각 thread 가 자기 좌표에 해당하는 output 하나만 책임진다. 입력끼리 의존성이 없는 (embarrassingly parallel) 문제일 때 이 분해가 그대로 통한다.

그런데 이 framing 안에서 두 가지가 빠르게 따라 나온다.

FIG · embarrassingly parallel 의 분해한 출력 = 한 thread
t0out[0]
t1out[1]
t2out[2]
t3out[3]
t4out[4]
t5out[5]
t6out[6]
t7out[7]
t8out[8]
t9out[9]
t10out[10]
t11out[11]
t12out[12]
t13out[13]
t14out[14]
t15out-of-range
N 이 block 크기로 나눠 떨어지지 않으면 마지막 block 에 out-of-range thread 가 생긴다 — 그래서 모든 커널 첫 줄이 if (i < n) … 의 boundary check 를 넣는다. 빠뜨리면 segfault, 넣어두면 가만히 NOP.

강의에서 Andreas 가 명시적으로 말한 점 — “모든 알고리즘이 embarrassingly parallel 한 건 아니다. 의존성 있는 알고리즘 (예: prefix sum, reduction) 은 다른 분해가 필요하다.” 이건 L009 Reductions 가 본격 다루는 주제로 넘어간다. L002 의 framing 으로는 “일단 의존성 없는 케이스부터 손에 익힌다” 가 답.

§ 03host ↔ device 메모리 모델· cudaMalloc / cudaMemcpy

CPU 와 GPU 는 별개의 주소공간을 가진다 — 모든 CUDA 프로그램의 첫 5줄이 옮긴다

GPU 는 자체 DRAM 을 가지고 있다. CPU 의 가상주소를 GPU 가 그대로 dereference 할 수 없다. 그래서 모든 “naive CUDA” 프로그램의 첫 5줄은 device 쪽 메모리를 잡고 (cudaMalloc), CPU 데이터를 옮기고 (cudaMemcpy H→D), 커널을 launch 하고, 결과를 다시 받고 (D→H), free 의 의식이다. 이 의식의 의미를 한 번 잡고 가야 그 다음의 모든 강의가 의미를 갖는다.

FIG · CUDA 프로그램의 5단계 의식vector_addition.cu 의 흐름
L0 · 호스트 CPU 메모리에 입력 준비float A[n], B[n]; — 일반 C 변수 stack/heap
L1 · device alloc device 쪽 buffer 잡기cudaMalloc(&A_d, n * sizeof(float)) cudaMalloc
L2 · 복사 H→D CPU 데이터 → GPU DRAMcudaMemcpy(A_d, A, size, cudaMemcpyHostToDevice) cudaMemcpy
L3 · 커널 launch GPU 위에서 grid 가 실행vecAddKernel<<<blocks, threads>>>(A_d, B_d, C_d, n) <<<…>>>
L4 · 복사 D→H 결과를 다시 CPU 로cudaMemcpy(C, C_d, size, cudaMemcpyDeviceToHost) cudaMemcpy
L5 · free device 쪽 buffer 해제cudaFree(A_d) cudaFree
변수명에 _d_h 를 붙이는 관행이 PMPP 책에서 강조된다 — device pointerhost pointer 를 같은 함수 안에서 섞으면 거의 항상 segfault. 이름으로 자기를 보호한다.

이 흐름이 PyTorch 안에서는 거의 보이지 않는다 — x.cuda() 한 줄이 L1+L2 를, op 호출이 L3 을 자동으로 한다. 하지만 강의 후반의 RGB→grayscale 과 mean filter 가 PyTorch extension 으로 깔리는 이유 중 하나가 이 의식을 손에서 빼주기 때문이다 — data_ptr<float>() 가 device pointer 를 그대로 돌려준다. 직접 짠 vector_add 와의 비교가 학습의 핵심.

실전 hint

처음 짠 CUDA 코드가 “실행은 되는데 결과가 다 0” 으로 나오면 거의 항상 L4(D→H 복사) 를 잊었거나, L2 를 잊고 device 쪽이 초기화 안 된 상태로 launch 한 경우. 이 의식의 한 단계가 빠지면 결과가 silent 하게 망가진다.

§ 04grid · block · thread 의 좌표계· indexing

thread 가 자기 자리를 어떻게 알아내는가 — 한 줄짜리 표준식

CUDA 의 좌표계는 3차원 grid 안에 3차원 block, 그 block 안에 3차원 thread 의 nested 구조다. 처음 보면 복잡한 듯하지만, 하나의 직관적인 설계 결정에서 나온다 — thread 들은 동시에 실행되니까 launch shape 을 만들 때부터 각 thread 가 자기 좌표를 직접 계산할 수 있게 한다. 그게 다.

1D 인덱싱의 표준식은 다음 한 줄.

// PMPP Ch.2 Figure 2.6 의 표준식
int i = blockIdx.x * blockDim.x + threadIdx.x;

해석은 분해된다 — blockIdx.x 는 “나는 몇 번째 block 안에 있는가”, blockDim.x 는 “block 하나의 thread 수”, threadIdx.x 는 “block 안에서 내 자리”. 셋을 곱하고 더하면 전역 thread index. 이 한 줄이 vector add, grayscale, mean filter 의 첫 줄에 모두 똑같이 등장한다.

1D launch — vector add

gridcdiv(n, 256) blocks
block256 threads (x 만)
thread indexi = bIdx.x * bDim.x + tIdx.x
guardif (i < n) …

2D launch — grayscale

grid(cdiv(W,16), cdiv(H,16))
blockdim3(16, 16) — 256 threads
row, colbIdx.y * bDim.y + tIdx.y, …
guardif (row < H && col < W) …

왜 block 이라는 중간 단계가 있는가 — 이게 PMPP 가 빠르게 답하는 다음 질문. block 안의 thread 들끼리만 fast 한 통신 (shared memory + __syncthreads) 이 보장된다. 다른 block 의 thread 와는 통신하지 않는다. block 단위로 SM(streaming multiprocessor) 위에 스케줄되기 때문이다. block 사이의 실행 순서나 동시성에 대해 어떤 가정도 할 수 없다 — “CUDA 가 block 들을 SM 들에 임의로 배정한다” 가 PMPP 의 명제.

“block 단위는 통신/동기화의 단위, grid 단위는 분배/스케줄의 단위. 이 두 층이 GPU 프로그램의 전부다.”학습 노트 · L002 §04
§ 05vector_addition.cu· 첫 커널

한 페이지로 다 깐 first kernel — host loop · launch · 복사 · free

강의의 첫 코드. PyTorch 의식 (load_inline, ninja) 없이, 그냥 nvcc vector_addition.cu 한 줄로 빌드되는 가장 작은 CUDA C 프로그램. 모든 것이 한 페이지에 들어 있어 “CUDA 프로그램이 무엇으로 구성되는가” 를 처음 본다.

// vector_addition.cu — 강의 repo 그대로 (요약)
__global__ void vecAddKernel(float* A, float* B,
                              float* C, int n) {
  int i = threadIdx.x + blockDim.x * blockIdx.x;
  if (i < n) {
    C[i] = A[i] + B[i];
  }
}

void vecAdd(float* A, float* B,
            float* C, int n) {
  float *A_d, *B_d, *C_d;
  size_t size = n * sizeof(float);

  cudaMalloc(&A_d, size);
  cudaMalloc(&B_d, size);
  cudaMalloc(&C_d, size);

  cudaMemcpy(A_d, A, size, cudaMemcpyHostToDevice);
  cudaMemcpy(B_d, B, size, cudaMemcpyHostToDevice);

  const unsigned int numThreads = 256;
  unsigned int numBlocks = cdiv(n, numThreads);

  vecAddKernel<<<numBlocks, numThreads>>>(
      A_d, B_d, C_d, n);
  gpuErrchk(cudaPeekAtLastError());
  gpuErrchk(cudaDeviceSynchronize());

  cudaMemcpy(C, C_d, size, cudaMemcpyDeviceToHost);
  cudaFree(A_d); cudaFree(B_d); cudaFree(C_d);
}

코드의 모든 사실을 풀어보자.

  • __global__ — “이 함수는 GPU 위에서 실행되고, host 에서 launch 된다” 는 qualifier. (§08 에서 다른 두 qualifier 와 같이 본다.)
  • <<<blocks, threads>>> — CUDA C 의 특수 문법. C++ 의 일부가 아니다. nvcc 가 이걸 cudaLaunchKernel 의 호출로 lower 한다.
  • cdiv(n, 256) — ceiling division. n 이 256 으로 나눠 떨어지지 않아도 모든 element 를 cover 하기 위해 한 block 더 띄운다. 그 마지막 block 의 일부 thread 는 i >= n guard 에서 NOP 처리된다.
  • cudaPeekAtLastError + cudaDeviceSynchronize — launch 가 비동기라서 (L001 §02 와 같은 이야기), error check 도 sync 도 명시적으로 해야 한다.
왜 이걸 PyTorch extension 으로 안 쌌나

강의의 의도다 — vector add 만큼은 “CUDA 프로그램이 무엇으로 구성되는가” 의 표본을 보여주려고 일부러 raw nvcc 빌드. 다음 두 커널 (RGB→grayscale, mean filter) 부터는 PyTorch extension 으로 깐다. 두 형태의 차이를 보고 “PyTorch 가 무엇을 자동화해 주는가” 를 명시적으로 이해하라는 의도.

§ 06RGB → grayscale· 2D 인덱싱 + PyTorch extension

이미지 한 장이 1D 가 아닌 이유 — dim3 가 들어오는 자리

두 번째 커널은 HxW 짜리 RGB 이미지를 grayscale 로 변환. 한 출력 pixel 마다 한 thread 를 띄운다. 입력은 (H, W, 3) bytes, 출력은 (H, W) bytes. 같은 “한 thread = 한 출력” 패턴이지만 — 좌표가 2D 가 된다.

2D launch 의 핵심은 dim3.

dim3 threads_per_block(16, 16);   // 256 threads, 16x16
dim3 number_of_blocks(
    cdiv(width,  16),
    cdiv(height, 16));

rgb_to_grayscale_kernel<<<
    number_of_blocks, threads_per_block, 0,
    torch::cuda::getCurrentCUDAStream()
>>>(
    result.data_ptr<unsigned char>(),
    image.data_ptr<unsigned char>(),
    width, height
);

kernel 안의 indexing 도 두 줄로 길어진다.

int col = blockIdx.x * blockDim.x + threadIdx.x;
int row = blockIdx.y * blockDim.y + threadIdx.y;
if (col < width && row < height) {
  int outOff = row * width + col;
  int inOff  = (row * width + col) * 3;
  unsigned char r = input[inOff + 0];
  unsigned char g = input[inOff + 1];
  unsigned char b = input[inOff + 2];
  output[outOff] = (unsigned char)
      (0.21f*r + 0.71f*g + 0.07f*b);
}

여기서 새로운 디테일이 두 가지 들어온다.

  • data_ptr<T>() — PyTorch tensor 가 들고 있는 device pointer 를 그대로 돌려준다. cudaMalloc · cudaMemcpy 의식이 사라졌다는 뜻. 입력은 이미 GPU 위에 있고, 출력은 torch::empty(...) 로 만들어 놓고 그 ptr 만 넘긴다.
  • torch::cuda::getCurrentCUDAStream() — PyTorch 가 자기 stream 위에서 모든 op 을 진행하니, 우리 커널도 같은 stream 에 끼워야 동기화가 자연스럽게 맞는다. <<<…, 0, stream>>> 의 4번째 인자가 stream.

그리고 흥미로운 사실 — Y = 0.21·R + 0.71·G + 0.07·B 는 Rec. 601 luma 공식이다. 인간 시각이 채널마다 민감도가 다르다는 사실에서 나온다. 이건 PMPP 책이 직접 깔지 않고 강의에서 Andreas 가 짚는 부분.

PyTorch extension vs raw nvcc

같은 커널이지만 build 방식이 다르다. raw nvcc 면 main() 안에서 직접 cudaMalloc·cudaMemcpy 를 쓴다. PyTorch extension 이면 torch::Tensor rgb_to_grayscale(torch::Tensor) 를 export 하고 — Python 에서 module.rgb_to_grayscale(img) 가 그대로 부른다. 같은 GPU 코드의 두 packaging.

강의에서 이 시점에 흘러가는 부수적인 메시지 — PyTorch 와 함께 쓰는 CUDA 코드의 표준 패턴이 이미 여기서 다 나타난다. data_ptr, current stream, C10_CUDA_KERNEL_LAUNCH_CHECK(). 이 세 줄은 L010 의 production-ready CUDA library 강의까지 그대로 이어진다.

§ 07mean filter· 한 thread 가 여러 입력을 읽는다

stencil 의 첫 형태 — 같은 입력을 여러 thread 가 또 읽는다

세 번째 커널은 mean filter — 각 출력 pixel 이 자기 주변 (2r+1)×(2r+1) window 의 평균이다. “한 출력당 한 thread” 의 패턴은 그대로지만, 한 thread 가 여러 입력을 읽는다는 새 차원이 추가된다. 이게 stencil 패턴의 가장 단순한 형태이고, 이후 모든 convolution, attention 의 부모 형태다.

// mean_filter_kernel.cu — 강의 repo (요약)
__global__ void mean_filter_kernel(
    unsigned char* output, unsigned char* input,
    int width, int height, int radius)
{
  int col = blockIdx.x * blockDim.x + threadIdx.x;
  int row = blockIdx.y * blockDim.y + threadIdx.y;
  int channel = threadIdx.z;
  int baseOff = channel * height * width;

  if (col < width && row < height) {
    int pixVal = 0, pixels = 0;
    for (int br = -radius; br <= radius; ++br) {
      for (int bc = -radius; bc <= radius; ++bc) {
        int r2 = row + br, c2 = col + bc;
        if (r2 >= 0 && r2 < height &&
            c2 >= 0 && c2 < width) {
          pixVal += input[baseOff + r2 * width + c2];
          pixels += 1;
        }
      }
    }
    output[baseOff + row * width + col] =
      (unsigned char)(pixVal / pixels);
  }
}

새로 들어온 디테일들.

  • threadIdx.z 가 처음 등장 — block 이 3D 다 (dim3(16, 16, channels)). RGB 의 세 채널을 z 축으로 분리. radius 가 작을 때 channel 별로 thread 를 더 늘려 occupancy 를 채우는 trick.
  • boundary handling — 이미지 가장자리는 window 가 잘리니 실제로 읽은 pixel 수만큼만 평균. 단순한 zero-padding 보다 결과가 자연스럽다.
  • 같은 입력이 여러 thread 에 의해 다시 읽힌다 — radius=1 이면 한 입력 pixel 이 9개의 출력 pixel 계산에 등장한다. 이게 다음 강의의 큰 주제다.
왜 이게 다음 강의의 출발점인가

같은 입력을 N번 다시 읽는다는 건 data reuse 가 있다는 뜻. 이걸 global memory 에서 매번 다시 가져오면 메모리 bandwidth 가 낭비된다. 한 번 가져와서 on-chip 에 잡아 두고 N번 재사용하는 게 답 — 이게 shared memory 와 tiling이다. PMPP Ch.4–5 의 본론이고 L004, L005 의 주제.

강의에서의 설명 흐름

Andreas 는 mean filter 를 “naive 한 형태” 로 보여주고 멈춘다 — “이걸 더 빠르게 만드는 방법은 다음 강의에서.” 이 의도적 cliffhanger 가 GPU Mode 시리즈가 PMPP 를 따라 가는 이유.

“naive 한 첫 커널을 끝내고도 다 못 본 게 아니다 — naive 한 첫 커널이 있어야 그 다음에 ‘무엇을 더 빠르게 만드는가’ 의 표적이 잡힌다.”학습 노트 · L002 §07
§ 08__global__ · __device__ · __host__· 함수 qualifier

같은 함수가 어디서 도느냐를 컴파일 시점에 정한다 — PTX 까지의 길

CUDA C 의 모든 함수는 셋 중 하나다 — host 에서만 도는 일반 함수 (__host__, 기본값), GPU 위에서만 도는 함수 (__device__), host 에서 launch 되어 GPU 에서 도는 entry-point 함수 (__global__). 이 qualifier 가 nvcc 에게 “이 함수의 binary 를 어디에 둘 것인가” 를 알려준다.

qualifier
호출 위치
실행 위치
return type
__host__
CPU
CPU
자유
__device__
GPU (device or global)
GPU
자유 (inlined 자주)
__global__
CPU (<<<…>>>)
GPU
반드시 void
__host__ __device__
CPU 또는 GPU
호출하는 쪽
자유

이 qualifier 들이 PMPP 책의 figure 와 직접 매칭된다. __global__ 함수는 GPU entry point — host 코드가 kernel<<<…>>>() 로 부른다. __device__ 함수는 GPU 안에서만 도는 helper. 둘 다 nvcc 가 PTX 로 컴파일해서 GPU executable 안에 넣는다. __host__ 는 일반 g++ 처럼 처리.

PTX 의 의미가 처음 들어오는 자리

강의에서 Andreas 가 짧게 짚는 사실 — PTX (Parallel Thread Execution) 는 NVIDIA 의 가상 ISA. nvcc 는 CUDA C 를 PTX 로, PTX 는 다시 SASS (각 SM 의 실제 머신코드) 로 lower 한다. PTX 는 arch 독립이라서 forward compatibility 의 단위. 강의에서 본격적으로 PTX 를 까지는 않지만, “여기까지가 PTX 단계” 라는 좌표를 잡아둔다. L001 §06 의 lowering 사다리에서 같은 그림이 다시 등장.

“CUDA C 가 일반 C++ 와 다른 이유의 90% 는 이 세 qualifier 와 <<<…>>> 문법이다. 나머지는 거의 같다.”학습 노트 · L002 §08
§ 09data shape 따라 다른 커널· 한 커널이 모두를 답하지 못한다

GPU 강의들이 매번 “for shape X 에서는 …” 으로 다시 시작하는 이유

강의 후반에 Andreas 가 짧게 던진 한 마디가 시리즈 전체의 운영 원칙이다 — “데이터 모양이 매우 spontaneous 해서, 다른 모양에는 다른 커널이 최적일 수 있다.” 이 한 줄이 GPU Mode 시리즈가 같은 연산 (matmul, attention, reduction) 을 여러 강의에 걸쳐 반복하는 이유.

small N · big batch
launch overhead 지배
large N · 1 batch
memory bandwidth 지배
tall-skinny matmul
tile shape 다르게
odd shape (모듈러 X)
tail effect / padding
tensor-core 정렬 모양
peak 가까이

한 커널이 모든 모양에 답하려면 — autotune 이 답이다. Triton 의 @triton.autotune · CUTLASS 의 instance 기반 generation 이 그 표준 패턴이다. 그리고 PyTorch 자체가 op 마다 여러 구현을 들고 있고 dispatcher 가 모양에 따라 다른 걸 부른다 — L006“dispatcher 통과 후 어떤 CUDA kernel 에 도달하는가” 가 이 사실의 다음 형태.

왜 PMPP 가 단일 사이즈로 시작하나

PMPP 의 첫 세 챕터는 의도적으로 “단순한 사이즈, 명확한 분해” 의 정석만 보여준다. 모양에 따라 다르다는 사실은 책 후반과 후속 강의의 표적. 이 강의의 cliffhanger 중 하나.

§ 10기억할 메모와 코드· key takeaways · repo

다시 열었을 때 5분 안에 손으로 잡혀야 할 것

PMPP Ch.1–3 을 다시 펼치기 전에 머릿속에 있어야 하는 사실들과 — 직접 손에 박아야 하는 코드 자료.

5단계 의식
cudaMalloc → cudaMemcpy H→D → kernel launch → cudaMemcpy D→H → cudaFree. PyTorch extension 이 자동으로 해주는 단계들.
grid · block · thread
3계층 좌표계. block 안에서만 fast 통신(shared memory). block 사이는 어떤 가정도 못 함.
표준 indexing 한 줄
i = blockIdx.x * blockDim.x + threadIdx.x + if (i < n) … guard. 모든 1D 커널의 첫 두 줄.
dim3
block · grid 의 모양을 1D/2D/3D 로 띄우는 도구. image 면 16×16, volume 이면 8×8×8 같이.
__global__ vs __device__
전자는 host 에서 launch, 후자는 GPU 안에서 호출되는 helper. __host__ __device__ 양쪽 다 컴파일.
data_ptr<T>()
PyTorch extension 의 핵심 한 줄. tensor 의 device pointer 를 그대로 cast. cudaMalloc 의식을 우회.
getCurrentCUDAStream
PyTorch 가 쓰는 stream 위에 우리 커널을 올리는 4번째 launch 인자. 빠뜨리면 동기화가 어긋난다.
stencil 의 첫 형태
한 thread 가 여러 입력을 읽으면 data reuse 가 생긴다 → 다음 강의의 shared memory tiling 의 출발점.
참고서 PMPP (4th edition) · Ch.1 Introduction, Ch.2 Heterogeneous Data Parallel Computing, Ch.3 Multidimensional Grids and Data

손에 새기기 — 실습 시퀀스

  1. vector_addition.cu 빌드 · 실행make 또는 nvcc vector_addition.cu -o vec. n=1000 의 결과를 직접 print 로 확인. 5단계 의식의 모든 줄을 손으로 따라 친다.
  2. boundary check 일부러 빠뜨려보기if (i < n) 를 지우고 빌드해서 무엇이 일어나는지 본다. cuda-memcheck 또는 compute-sanitizer 가 어떻게 알려주는지 확인.
  3. RGB → grayscale 직접 빌드rgb_to_grayscale.pyload_inline 호출을 추적, ./load_inline_cuda/ 디렉터리 안의 자동 생성된 main.cpp 를 직접 읽는다.
  4. 2D launch shape 바꿔보기 — 16×16 → 32×32, 8×8, 1×256 으로 바꾸면서 결과 정확성과 시간 변화를 확인. 같은 코드가 launch shape 으로 다르게 동작하는 첫 경험.
  5. mean filter radius sweep — radius ∈ {1, 3, 5, 9} 로 돌리면서 시간이 얼마나 늘어나는지 측정. radius 가 늘어나면 한 thread 가 읽는 pixel 수가 (2r+1)². data reuse 가 보이는가.
  6. __device__ helper 함수 추가해보기 — luma 계산 부분을 __device__ __forceinline__ unsigned char rgb2y(...) 로 빼고 같은 결과가 나오는지 확인. nvcc 가 inline 시킨다.
  7. 결과를 PyTorch 의 torchvision.transforms.Grayscale 과 비교 — 같은 입력에 두 결과가 byte 단위로 정확히 같은지. 다르면 어디가 다른지 (rounding, 채널 순서, 가중치).
  8. 한 페이지 plan — “이 강의의 패턴을 LLM 추론의 한 자리에 어떻게 적용할까” 를 한 페이지로 적어본다 — 예: KV cache 의 한 head 에 대해 “한 thread = 한 (batch, seq) 좌표” 로 분해하는 식.
§ 11다른 강의로 이어지는 길· connections

이 강의가 다음 강의들에서 어디에 다시 등장하는지

L002 의 framing (grid/block/thread, host↔device 의식, 한 thread = 한 출력) 이 시리즈 전체에 흩어져 있는 모든 강의의 출발 좌표가 된다.

§ 12열린 질문· open questions

다시 들었을 때 직접 검증해야 할 것들

PMPP Ch.1–3 의 한 시간 요약이 의도적으로 넘어간 자리들과 — 자기 GPU 위에서 직접 확인해야 손에 박히는 사실들.

검증 메모

이 노트의 모든 코드 인용은 gpu-mode/lectures/main 의 lecture_002 디렉터리에서 fetch 한 실제 소스를 요약한 것. 강의 시점과 현재 repo 사이에 약간의 형식 차이는 있을 수 있음 (C10_CUDA_KERNEL_LAUNCH_CHECK 의 추가 등). 큰 의미 변화는 없음.

← Lecture 001 Mark Saroufim — 같은 코드를 어떻게 보는가의 도구 사다리 Lecture 003 → Jeremy Howard — Python 안에서 CUDA 커널을 step-by-step 으로