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

Getting Started With CUDA for Python Programmers

Jeremy Howard 가 제안한 학습 순서 — Python 안에서 같은 알고리즘을 “kernel 모양” 으로 먼저 짜고, 그 코드를 거의 글자 그대로 CUDA C 로 옮긴다. 디버깅이 어려운 GPU 위에서 헤매기 전에, 익숙한 Python 의 print/breakpoint 위에서 알고리즘을 손에 익히는 학습 사다리. RGB→grayscale 과 (5×784)·(784×10) 짜리 작은 matmul 두 예제로 세 단계 (single-thread → block-of-threads → CUDA) 를 차례로 깐다.

Python kernel mockup load_inline 2D matmul RGB→grayscale torch.utils.cpp_extension CUDA_LAUNCH_BLOCKING wurlitzer PMPP Ch.3 응용
J
Speaker
Jeremy Howard
answer.ai · fast.ai 창립자 · Jupyter 위에서 GPU 를 가르치는 방식의 설계자
강의 번호
L003
스피커
Jeremy Howard
학습 우선순위
High · 정독
다시 볼 때
notebook 셀별로 따라 친다
§ 01강의가 풀려는 문제· Why this lecture exists

“CUDA 가 어렵다” 는 평판의 진짜 출처

CUDA 의 학습 곡선이 가파른 이유는 문법이 아니다. 알고리즘과 GPU 모델 두 개를 동시에 처음 보기 때문이다. Jeremy 의 진단이 정확하다 — “Python 으로 이미 짤 수 있는 알고리즘을, GPU 위에서 다시 짜면서 동시에 indexing/launch/메모리 모델까지 처음 만나니, 어디서 막힌 건지가 안 잡힌다.”

강의가 푸는 두 질문.

  1. 알고리즘 자체는 익숙한 Python 위에서 먼저 손에 잡고 — print, breakpoint, 작은 입력을 자유롭게 쓰면서 — 그 위에 “GPU 의 모양” 을 layered 로 얹을 수 있는가.
  2. 그렇게 익숙해진 Python kernel 을 거의 mechanical 하게 CUDA C 로 변환 할 수 있는가. (Jeremy 가 ChatGPT 에 그대로 붙여넣어 변환을 추천하는 자리.)
시리즈 안에서의 위치

L001 은 “이미 도는 코드를 어떻게 보는가” 의 강의, L002 는 “CUDA 프로그램이 무엇으로 구성되는가” 의 강의. L003 은 “그 사이를 학습자가 어떻게 건너가는가” 의 강의. 다음 단계인 L005 가 같은 사다리에 shared memory 를 한 단 더 올린다.

“GPU 위에서 디버깅하는 건 어렵다 — 그러니 가능한 한 마지막에 GPU 로 넘긴다. 알고리즘 자체는 Python 으로 다 끝내놓고.”Jeremy Howard · 학습 노트
§ 02Jeremy 의 학습 사다리· Python → block kernel → CUDA

같은 알고리즘을 세 번 짠다 — 매번 한 단씩 GPU 모델에 가까워지면서

Jeremy 가 pmpp.ipynb 에서 깐 학습 패턴은 RGB→grayscale 과 matmul 두 예제에 똑같이 반복된다. 같은 알고리즘 → 세 단계의 변환: ① 일반 Python loop, ② 한 thread 시점의 “kernel” 함수 + 외부 loop, ③ block-aware 한 “kernel” 함수 + 이중 loop. 이 셋을 끝낸 다음 비로소 CUDA C 로 mechanical 하게 옮긴다.

FIG · 같은 알고리즘의 3단 변환RGB→grayscale 의 한 줄 진화
L0 · 일반 Python
for i in range(n):
    res[i] = 0.30*x[i] + 0.59*x[i+n] + 0.11*x[i+2n]
— 외부 loop, kernel 개념 없음 —
~3 s
L1 · Python kernel
def k(i, x, out, n):
    out[i] = 0.30*x[i] + ...

run_kernel(k, n, x, res, n)
한 thread 시점만 짠다.
loop 는 runner 가.
~3 s
L2 · block kernel
def k(bi, ti, bdim, x, out, n):
    i = bi*bdim + ti
    if i < n: out[i] = ...

blk_kernel(k, blocks, 256, x, ...)
CUDA 의 indexing 한 줄을
Python 으로 흉내냄.
~3 s
L3 · CUDA C
— 위 함수를 ChatGPT 에 붙여
변환 →
__global__ void k(uchar* x, ...) {
  int i = blockIdx.x*blockDim.x + threadIdx.x;
  if (i<n) out[i] = 0.30*x[i] + ...
}
~0.3 ms
L1 에서 L2 로 갈 때 알고리즘은 한 글자도 바뀌지 않는다. 호출 패턴만 바뀐다 — “한 번에 N 개 thread 가 부른다고 가정” 하는 식. CUDA 로의 변환이 거의 무손실인 이유.

이 사다리의 진짜 가치는 L1 과 L2 에서 print, breakpoint 가 그대로 동작한다는 점이다. i 가 어떤 값에서 잘못됐는지, blockidx*blockdim + threadidx 가 어떻게 계산되는지를 Python 안에서 직접 확인할 수 있다. CUDA C 로 옮긴 뒤에는 같은 디버깅이 printf(§09) 또는 cuda-gdb 의 영역.

§ 03RGB → grayscale, Python 위에서· naive 첫 단계

알고리즘을 가장 단순한 형태로 손에 둔다

예제는 (C, H, W) shape 의 RGB tensor 를 (H, W) shape 의 grayscale 로 변환. 가중치는 0.2989·R + 0.5870·G + 0.1140·B 의 luma 공식. notebook 의 첫 셀이 PIL/torchvision 으로 강아지 사진을 가져와 (3, 1066, 1600) tensor 를 만든다.

# pmpp.ipynb cell 9 — naive Python
def rgb2grey_py(x):
    c, h, w = x.shape
    n = h * w
    x = x.flatten()
    res = torch.empty(n, dtype=x.dtype,
                      device=x.device)
    for i in range(n):
        res[i] = (0.2989*x[i]
                + 0.5870*x[i+n]
                + 0.1140*x[i+2*n])
    return res.view(h, w)

코드를 펼쳐 보면 한 가지 디테일이 의도적이다 — tensor 를 .flatten() 으로 1D 로 만들어 놓고, 채널은 메모리 위 offset (x[i+n], x[i+2n]) 으로 본다. 이렇게 1D 로 깔아놓는 게 다음 단계의 “한 thread = 한 출력 element” 패턴과 정확히 매핑된다. (c, h, w) 에 대한 3중 loop 가 1중 loop 가 되는 자리.

왜 0.30 / 0.59 / 0.11 인가

Rec.601 luma 가중치. 인간 시각이 G 에 가장 민감하고 B 에 가장 둔감하다는 측정에서 나옴. L0020.21·R + 0.71·G + 0.07·B (Rec. 709) 와 약간 다른 표준. 두 가중치 다 grayscale 로 보면 차이가 미미하다.

이 단계에서 %%time 으로 측정한 시간이 약 3초 — 1066×1600 = 약 1.7M element 의 Python loop. 느린 게 핵심이 아니다, 알고리즘이 정확히 무엇을 계산하는지가 손에 잡히는 게 핵심.

§ 04Python kernel mockup· run_kernel(f, n, *args)

한 thread 의 시점만 짠다 — 그리고 외부 runner 가 N번 호출

두 번째 단계가 학습 사다리의 진짜 도구다. kernel 함수를 “한 thread 의 시점” 만으로 짜고, loop 는 외부 run_kernel 이 책임진다. 이 함수의 시그니처가 정확히 CUDA kernel 의 메탈 모델이다.

# pmpp.ipynb cell 13–17
def run_kernel(f, times, *args):
    for i in range(times):
        f(i, *args)               # 한 thread 의 호출

def rgb2grey_k(i, x, out, n):
    out[i] = (0.2989*x[i]
            + 0.5870*x[i+n]
            + 0.1140*x[i+2*n])

def rgb2grey_pyk(x):
    c, h, w = x.shape; n = h * w
    x = x.flatten()
    res = torch.empty(n, dtype=x.dtype, device=x.device)
    run_kernel(rgb2grey_k, h*w, x, res, n)   # N번 launch 흉내
    return res.view(h, w)

이 코드의 모든 사실이 의미 있다.

  • kernel 은 return 하지 않는다 — Jeremy 가 markdown 셀에서 명시적으로 못 박는다 (“NB: A kernel can not return anything. It can only change contents of things passed to it.”). CUDA 의 __global__ void 와 정확히 매핑.
  • 인자 첫 번째는 thread 의 좌표i. 나머지 인자는 모두 buffer 또는 scalar.
  • kernel 안에서는 자기 좌표 외엔 제어 흐름이 없다 — “나는 i 번째다, 내 일만 한다”. 다른 thread 가 무엇을 하는지 모른다.
왜 이게 중요한가

이 시그니처를 손에 익히면 — CUDA 로 옮길 때 indexing 한 줄(i = bIdx*bDim + tIdx) 만 추가하면 된다. 알고리즘은 그대로다. 코드 변환 비용이 거의 0 이 되는 자리.

L0(naive Python) 와 L1(kernel mockup) 의 시간이 비슷하다 — Python loop 가 dominant 하니까. 시간을 줄이는 게 이 단계의 목표가 아니다. thread-시점의 함수를 손에 두는 게 목표.

§ 05block 을 Python 으로 흉내내기· blk_kernel(f, blocks, threads)

CUDA 의 indexing 한 줄을 Python 위에서 미리 본다

L1(run_kernel) 은 “한 thread 시점” 만 모델링한다 — block 이 없다. 다음 단계가 block 을 추가한 blk_kernel. CUDA 의 launch shape <<<blocks, threads>>> 와 thread 의 indexing 식 blockIdx*blockDim + threadIdx 를 Python 으로 그대로 흉내낸다.

# pmpp.ipynb cell 21–23
def blk_kernel(f, blocks, threads, *args):
    for i in range(blocks):
        for j in range(threads):
            f(i, j, threads, *args)               # (blockIdx, threadIdx, blockDim)

def rgb2grey_bk(blockidx, threadidx, blockdim, x, out, n):
    i = blockidx * blockdim + threadidx
    if i < n:                                # boundary guard — CUDA 와 동일
        out[i] = 0.2989*x[i] + 0.5870*x[i+n] + 0.1140*x[i+2*n]

def rgb2grey_pybk(x):
    c, h, w = x.shape; n = h*w
    x = x.flatten(); res = torch.empty(n, dtype=x.dtype, device=x.device)
    threads = 256
    blocks = math.ceil(h*w / threads)
    blk_kernel(rgb2grey_bk, blocks, threads, x, res, n)
    return res.view(h, w)

이 코드의 의미는 직접적이다.

  • 블록 수 cdiv(n, 256), 블록 당 thread 수 256 — CUDA 에서 흔한 default. thread 수는 32 의 배수 (warp 크기) 가 권장됨.
  • kernel 함수 시그니처가 (blockidx, threadidx, blockdim, ...) — CUDA 의 blockIdx.x, threadIdx.x, blockDim.x 와 정확히 1:1.
  • boundary check if i < n 가 필요한 이유는 n = h*w 가 256 으로 나눠 떨어지지 않을 가능성. 마지막 block 에 일부 thread 는 i >= n.
CUDA 의 한계도 같이 흉내

Jeremy 의 markdown 셀이 명시적으로 깐다 — “2^31 max blocks for dim 0, 2^16 max for dims 1 & 2”, “1024 max threads per block (use a multiple of 32)”. 이 한계는 Python blk_kernel 안에서는 강제되지 않지만, CUDA 에서는 강제. 알고 가야 한다.

이 시점에서 학습자가 손에 가지고 있는 건 — CUDA 와 정확히 같은 모양의 알고리즘이다. 두 차이만 남았다: ① Python 의 indexing 산술이 GPU 의 32-bit register 산술로, ② Python 의 외부 loop 가 GPU 의 SM 위 병렬 실행으로. 두 변환 모두 mechanical 하다.

§ 06load_cuda — Python 위의 wrapper· load_inline 의 깔끔한 포장

L001 의 load_inline 을 한 줄로 줄인다 — notebook 위에서 자주 쓸 수 있게

CUDA 코드를 끼워넣는 도구는 L001 §05 에서 본 torch.utils.cpp_extension.load_inline 그대로다. Jeremy 는 자주 쓰는 옵션을 묶어 한 줄짜리 wrapper 로 만든다.

# pmpp.ipynb cell 28
def load_cuda(cuda_src, cpp_src, funcs,
              opt=False, verbose=False):
    return load_inline(
        cuda_sources=[cuda_src],
        cpp_sources=[cpp_src],
        functions=funcs,
        extra_cuda_cflags=["-O2"] if opt else [],
        verbose=verbose,
        name="inline_ext",
    )

# 모든 CUDA 셀 머리에 붙는 boilerplate
cuda_begin = r'''
#include <torch/extension.h>
#include <stdio.h>
#include <c10/cuda/CUDAException.h>

#define CHECK_CUDA(x) TORCH_CHECK(x.device().is_cuda(), #x " must be a CUDA tensor")
#define CHECK_CONTIGUOUS(x) TORCH_CHECK(x.is_contiguous(), #x " must be contiguous")
#define CHECK_INPUT(x) CHECK_CUDA(x); CHECK_CONTIGUOUS(x)

inline unsigned int cdiv(unsigned int a, unsigned int b) {
  return (a + b - 1) / b;
}
'''

여기서 한 가지 미묘한 trick — name="inline_ext" 가 항상 같다. 같은 이름이면 PyTorch 가 build cache 를 재사용해서 두 번째 호출부터 빠르다. 다른 셀에서 다른 커널을 짜더라도 같은 “module” 안에 함수만 추가되는 패턴.

(노트북 셀에서 cuda_src 를 수정해서 재빌드할 때, 같은 name 이면 PyTorch 가 디스크의 .so 를 재로드. 의도적으로 다른 이름을 줘서 cache 를 무효화하는 것도 가능.)

cuda_begin 의 보일러플레이트

CHECK_CUDA · CHECK_CONTIGUOUS 매크로는 잘못된 입력을 빨리 catch 하기 위함. CUDA tensor 가 아니거나 메모리 layout 이 비연속이면 raw data_ptr 가 우리 가정과 다를 수 있다. cdiv 는 ceiling division — block 수 계산의 표준.

§ 07CUDA 로 옮기는 mechanical step· rgb2grey 의 거의 1:1 변환

Python block kernel 의 변수명을 그대로 — indexing 한 줄만 추가

학습 사다리의 마지막 단. Python rgb2grey_bk 를 그대로 ChatGPT 또는 손으로 CUDA C 로 옮긴다. 변환의 “보일러플레이트” 는 cuda_begin 가 다 써놨고, 함수 본체는 거의 한 글자도 안 바뀐다.

// pmpp.ipynb cell 33 — load_cuda 가 빌드하는 본체
__global__ void rgb_to_grayscale_kernel(
    unsigned char* x, unsigned char* out, int n)
{
    int i = blockIdx.x * blockDim.x + threadIdx.x;          // ← 이 한 줄만 추가
    if (i < n)
        out[i] = 0.2989f*x[i] + 0.5870f*x[i+n]
               + 0.1140f*x[i+2*n];
}

torch::Tensor rgb_to_grayscale(torch::Tensor input) {
    CHECK_INPUT(input);                                       // 매크로의 가치
    int h = input.size(1);
    int w = input.size(2);
    auto output = torch::empty({h, w}, input.options());
    int threads = 256;
    rgb_to_grayscale_kernel<<<cdiv(w*h, threads), threads>>>(
        input.data_ptr<unsigned char>(),
        output.data_ptr<unsigned char>(),
        w*h);
    C10_CUDA_KERNEL_LAUNCH_CHECK();
    return output;
}

변환 매핑을 펼쳐 보면.

  • def__global__ void
  • Python i = blockidx * blockdim + threadidx → CUDA int i = blockIdx.x * blockDim.x + threadIdx.x
  • Python if i < n: → CUDA if (i < n)
  • blk_kernel(fn, blocks, threads, …)fn<<<blocks, threads>>>(…)
  • x.flatten() 의 의미 → data_ptr<unsigned char>() + 1D offset 산술

이 매핑이 한 줄도 빠지지 않고 mechanical이라는 점이 학습 사다리의 가치다. 알고리즘에 대한 이해는 Python 단계에서 다 끝났고, CUDA 단계는 변환 작업.

시간 점프

Python 단계 ~3초, CUDA 단계 ~0.3ms. 약 1만 배의 점프. 같은 알고리즘, 같은 결과 (byte 단위로 비교 가능). 알고리즘에 대한 자신감과 launch 패턴에 대한 자신감이 분리된다는 사실이 이 강의의 핵심 메시지.

ChatGPT 의 실용적 사용

Jeremy 가 명시적으로 추천 — “Python kernel 을 그대로 ChatGPT 에 붙이고 ‘이걸 CUDA C 로 변환해 달라, 이름 같게’.” 매핑이 mechanical 하니 LLM 이 안정적으로 잘 한다. 사람이 직접 쓰는 것보다 빠르고 오류가 적다는 게 강의의 입장.

“알고리즘을 Python 으로 짜놓으면 — CUDA 변환은 더 이상 알고리즘 디자인이 아니라 그냥 매핑이다.”Jeremy Howard · L003
§ 082D matmul· block 좌표 두 개로

같은 사다리를 2D 에 적용 — (5, 784) × (784, 10) 의 작은 matmul

강의 후반부는 같은 학습 패턴을 matmul 에 적용. 입력은 MNIST 의 작은 sample (m1.shape = (5, 784)) 와 random 가중치 (m2.shape = (784, 10)). 출력은 (5, 10). 작은 사이즈를 쓰는 이유 — Python 단계에서 %time 으로 측정 가능한 범위에 두기 위해.

# pmpp.ipynb cell 56 — naive Python matmul
def matmul(a, b):
    (ar, ac), (br, bc) = a.shape, b.shape
    c = torch.zeros(ar, bc)
    for i in range(ar):
        for j in range(bc):
            for k in range(ac):
                c[i,j] += a[i,k] * b[k,j]
    return c
# 2D Python kernel — 한 출력 element 의 시점
def matmul_k(i, j, a, b, c, ac):
    s = 0.0
    for k in range(ac):
        s += a[i,k] * b[k,j]
    c[i,j] = s

여기서 새로 들어오는 패턴.

  • “한 thread = 한 출력 element” 의 같은 분해. (i, j) 좌표 한 쌍이 한 thread 에 매핑.
  • thread 안에 inner loop (k 차원) 가 남는다 — accumulation. 이게 data reuse 의 첫 자리다. 같은 row 의 thread 들이 같은 a row 를 다시 읽고, 같은 column 의 thread 들이 같은 b column 을 다시 읽는다.
  • 이 reuse 를 시각화한 게 다음의 작은 figure.
FIG · matmul 의 data reuserow 와 column 이 N번 다시 읽힌다
A (5×784)
a00
a01
a02
a03
a04
a05
a06
a07
a10
a11
a12
a13
a14
a15
a16
a17
×
B (784×10)
b00
b01
b10
b11
b20
b21
b30
b31
노란 row a0_ 은 b 의 모든 column 과 곱해진다 → b column 수만큼 다시 읽힘. 노란 column b_0 은 a 의 모든 row 와 곱해진다 → a row 수만큼 다시 읽힘. L005 의 shared memory tiling 이 이 reuse 를 잡는다.

그리고 같은 사다리로 CUDA 단계까지 — 2D launch (dim3(16, 16) threads, dim3(cdiv(bc,16), cdiv(ar,16)) blocks) 로 띄우고, kernel 안에서 row = blockIdx.y*blockDim.y + threadIdx.ycol = blockIdx.x*blockDim.x + threadIdx.x 두 줄로 좌표를 계산. 나머지는 Python 의 inner loop 가 그대로.

이 단순 CUDA matmul 은 shared memory 를 쓰지 않는다 — 매 element 마다 a 의 한 row, b 의 한 column 을 global memory 에서 다시 읽는다. data reuse 가 보이지만 잡지 않은 상태가 의도적이다. L005 가 이 코드 위에 shared memory tiling 을 한 단 더 얹어 “Going Further” 의 의미가 된다.

§ 09notebook 위에서 디버깅하는 trick· CUDA_LAUNCH_BLOCKING · wurlitzer

“실행은 됐는데 결과가 이상하다” 를 어떻게 추적하는가

notebook 환경에서 CUDA 디버깅을 하면 흔히 만나는 두 문제 — error 가 어디서 났는지 위치가 비동기 launch 때문에 어긋난다, kernel 안의 printf 출력이 stdout 에 안 잡힌다. Jeremy 가 두 가지 환경 설정을 셀 머리에 박는다.

1
os.environ['CUDA_LAUNCH_BLOCKING'] = '1' 모든 CUDA launch 를 동기로 만든다 — error 위치가 정확히 launch 줄에서 잡힌다. cudaDeviceSynchronize() 가 자동으로 launch 직후에 박히는 것과 같음. 디버깅용; production 에선 끈다 (성능 떨어진다).
2
%load_ext wurlitzer kernel 안의 printf 출력을 Jupyter cell 의 output 으로 redirect. 안 켜면 stdout 이 어디로 가는지 모른 채 사라진다. 작은 입력에 대해 if (i == 0) printf("...") 패턴이 가능해진다.
3
verbose=True option in load_inline nvcc 의 컴파일 명령과 그 출력을 다 보여준다. 빌드 실패 시 어떤 flag 가 들어갔는지, 어떤 라인이 syntax error 인지 직접 확인.
4
매크로 CHECK_INPUT(x) kernel launch 전에 “이 tensor 가 정말 CUDA 위에 있고, contiguous 한가” 를 검증. 잘못된 가정으로 raw pointer 를 dereferencing 하기 전에 catch.
5
작은 입력으로 먼저 Python kernel mockup 단계에서는 img2 = tvf.resize(img, 150, antialias=True) 처럼 입력을 작게. 같은 kernel 이 작은 입력에서 정확하면 큰 입력에서도 거의 항상 정확.
통합 패턴

Jeremy 의 notebook 모든 셀의 머리에 위 1·2가 박혀 있고, load_cuda(..., verbose=True) 가 켜져 있고, 작은 입력으로 먼저 검증한 뒤 큰 입력. “CUDA 가 디버깅이 어렵다” 라는 신화를 깨는 가장 단순한 한 묶음의 환경 설정이 이 자리.

“CUDA 코드를 짜는 데 1시간이 걸렸다면 — 그 중 50분은 Python 위에서, 5분은 변환에, 5분은 CUDA 디버깅에 써라.”학습 노트 · L003 §09
§ 10기억할 메모와 코드· key takeaways · repo

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

이 강의에서 6개월 뒤 다시 돌아왔을 때 가장 먼저 복원해야 하는 사실들과 — 직접 손에 박아야 하는 코드 자료들.

3단 사다리
Python loop → Python kernel(run_kernel) → Python block kernel(blk_kernel) → CUDA. 같은 알고리즘을 같은 변수명으로 4번.
kernel 의 metal 모델
def k(i, *args) 또는 def k(bIdx, tIdx, bDim, *args). kernel can not return. buffer 를 mutate 한다.
indexing 한 줄
i = blockIdx.x * blockDim.x + threadIdx.x + if i < n guard. 1D 커널의 표준 첫 두 줄.
cuda_begin 의 매크로
CHECK_CUDA · CHECK_CONTIGUOUS · CHECK_INPUT · cdiv. 모든 셀 머리에 붙는 보일러플레이트.
CUDA_LAUNCH_BLOCKING
launch 를 동기로 만들어 error 위치를 정확하게. 디버깅용. production 에선 끈다.
wurlitzer
Jupyter cell 안에서 kernel 의 printf 를 보려면 필수. %load_ext wurlitzer.
load_cuda wrapper
load_inline 의 자주 쓰는 옵션 묶기. name="inline_ext" 고정으로 build cache 재사용.
2D launch
dim3(16, 16) threads, dim3(cdiv(W,16), cdiv(H,16)) blocks. matmul 과 image 처리의 표준.
알고리즘은 Python 에서 끝낸다
CUDA 코드의 “창의력” 은 Python kernel 에 다 있다. CUDA 변환은 매핑 작업이고 LLM 이 잘한다.
Slides 없음 — notebook 자체가 자료. pmpp.ipynb 가 슬라이드 역할.
Code pmpp.ipynb · Colab

손에 새기기 — 실습 시퀀스

  1. 강아지 사진을 자기 노트북 환경에서 grayscalepmpp.ipynb 를 그대로 돌려 4단계 (naive → kernel → block kernel → CUDA) 의 결과가 byte 단위로 동일 한지 검증.
  2. L0 → L1 변환 직접 해보기 — 다른 알고리즘 (예: per-row max, per-row sum) 을 같은 4단계로 변환. 같은 변수명을 유지하면서.
  3. boundary check 일부러 빠뜨려서 본다 — Python 단계에서는 IndexError 가 나고, CUDA 단계에서는 illegal memory access. 같은 버그가 두 환경에서 어떻게 다르게 보이는지.
  4. thread 수 sweep — 256 → 64, 128, 512, 1024. %timeit 으로 시간 변화. n_warps 와의 관계 추측.
  5. matmul 을 작은 사이즈에서 검증, 큰 사이즈에서 측정 — (5, 784) × (784, 10) 의 결과를 PyTorch torch.matmultorch.allclose. 그 다음 (1024, 1024) × (1024, 1024) 로 키워 시간 비교 — naive 가 cuBLAS 와 얼마나 차이나는가.
  6. CUDA_LAUNCH_BLOCKING 의 효과 보기 — 일부러 잘못된 indexing (예: i+5) 을 넣어서 launch 했을 때, BLOCKING=1 과 0 에서 traceback 위치가 어떻게 다른지.
  7. printf 디버깅 — kernel 안에 if (i < 5) printf("i=%d, val=%f\n", i, x[i]); 를 박고 wurlitzer 로 볼 수 있는지 확인. 작은 입력에 한정.
  8. 한 페이지 plan — 자기가 짜고 싶은 다음 커널을 정하고, “Python 4단계 → CUDA” 의 사다리에 맞춰 계획서 작성.
§ 11다른 강의로 이어지는 길· connections

이 학습 사다리가 다른 강의에서 어떻게 확장되는지

L003 의 “Python 위에서 알고리즘을 손에 잡는다” 패턴이 시리즈의 여러 강의에서 한 단씩 확장된다.

§ 12열린 질문· open questions

다음에 다시 들었을 때 검증해야 할 것들

학습 사다리의 framing 안에서 의도적으로 비워둔 자리들과 — 직접 자기 환경에서 확인해야 손에 박히는 사실들.

← Lecture 002 Andreas Köpf — PMPP Ch.1–3 의 grid/block/thread 정식 모델 Lecture 004 → Thomas Viehmann — PMPP Ch.4–5 의 compute/memory roofline