GPU 커널을 직접 작성하는 일은 인내심과 노력, 그리고 정신 건강까지 요구합니다. 저는 최신 LLM(Claude Opus 5, GPT 5.6 Sol)을 활용해 커널을 문자 그대로 출력해 내는 실험을 해 왔습니다. 그 과정에서 배운 내용을 이야기해 보려고 합니다.
솔직히 말하면, GPU 커널 작성은 검증하기가 민망할 정도로 쉬운 작업입니다.
먼저, 커널을 작성하려는 연산을 결정합니다. 둘째, 첫 번째 버전을 작성하고 명백한 오류 없이 컴파일되는지 확인합니다. 셋째, 느린 참조 구현(reference implementation)으로 정확성을 확인합니다. 일치하지 않으면 정확성 문제를 해결하려고 시도합니다. 그다음 커널의 실행 시간을 벤치마킹합니다. 이후 버전은 이를 기반으로 구축되며, roofline 지표에 도달하거나 만족할 때까지 계속 최적화합니다.

검증 가능한 루프: GPU 커널 개발
위 이미지는 이를 검증 가능한 루프로 보여 줍니다:
- 컴파일 확인은 정확성을 고민하기 전에 수행하는 가벼운 로컬 루프입니다(B <-> C).
- 정확성 확인(D <-> E <-> F)은 검증 가능한 보상의 핵심 루프입니다. 비교할 ground-truth 참조 구현이 있기 때문에 커널 작성이 자동화된 검증에 적합한 문제가 되는 이유가 바로 이 부분입니다.
- 최적화(G -> H -> D로 복귀)는 새 버전마다 동일한 정확성 루프를 재사용합니다. 빠르지만 틀린 커널은 가치가 없으므로 각 버전에 대해 정확성을 검증해야 합니다.
- roofline/만족도 확인(I)은 최적화를 계속할지 중단할지를 결정하는 외부 루프입니다.
첫 번째 버전
위 이미지는 여전히 큰 그림을 보여 줄 뿐이며, 진짜 어려움은 세부 사항에 있습니다. 에이전트가 좋은 첫 버전 작성에 착수할 수 있도록 필요한 모든 컨텍스트를 확보해 두어야 합니다.
여기서 CUDA DSL이 등장합니다. Triton, CuTeDSL, Tilelang은 Python에서 시작하기 매우 쉬운 도구들입니다. CUDA C++에 비해 학습 곡선이 완만하지만, 이런 DSL의 추상화가 오히려 에이전트를 더 혼란스럽게 만들 수도 있습니다. 에이전트에게 이러한 추상화에 대한 컨텍스트를 전달할 방법이 필요합니다.
최신 LLM은 이미 "좋은" Triton 코드를 작성하는 방법을 알고 있습니다. 컨텍스트가 전혀 없어도 Triton 추상화를 잘 다룰 수 있습니다. 하지만 Triton보다 훨씬 더 많은 제어를 제공하는 CuTeDSL 같은 다른 DSL의 경우, 에이전트가 DSL 추상화를 이해하기 위해 살펴볼 수 있는 컨텍스트 디렉토리를 두는 것이 큰 도움이 된다는 것을 발견했습니다.
예를 들어, 에이전트가 CuTeDSL로 커널을 작성할 때
NVIDIA cutlass
리포지토리를 컨텍스트 디렉토리에 클론해 두면, 다음과 같은 관련 추상화
Layout Algebra, Copy/GEMM atoms, memory hierarchy, example kernels
등을 참고할 수 있어 매우 좋은 방법입니다.
제 경험상, 좋은 첫 버전의 커널은 명백한 오류 없이 컴파일되고 아래에서 설명할 정확성 테스트를 통과합니다.
테스트, 벤치마크, 프로파일링
에이전트에게 충분한 컨텍스트가 주어지면, 실제 병목 지점은 검증으로 이동합니다. 참조 구현 자체와 이를 기준으로 한 검증이 점점 더 중요해집니다. 저는 이 단계를 정확성 테스트 또는 간단히 테스트라고 부릅니다. 참조 구현의 속도는 그 의도만큼 중요하지 않습니다. 측정하고 검증하려는 것이 바로 에이전트가 최적화할 대상이기 때문입니다.
일반적으로 연산이 낮은 정밀도(lower precision)로 수행되지 않는 경우, 최대 절대/상대 오차(MAE), 평균 제곱 오차(MSE/RMSE), PSNR(Peak Signal to Noise Ratio)을 측정합니다. 낮은 정밀도가 관여하는 경우에는 PSNR과 코사인 유사도(cossim)를 측정하는 편입니다.
커널 버전을 실제로 GPU에서 실행하는 방식은 GPU를 로컬에서 사용할 수 있는지, 아니면 클라우드를 통해 사용하는지에 따라 달라집니다. 어느 쪽이든 에이전트는 어떤 형태로든 자신의 출력 결과에 접근할 수 있어야 합니다.
저는 아래의 rung 방법론이 N개의 테스트 함수를 구성하는 좋은 방법이라고 생각합니다:
1def rung(name):2 def deco(fn):3 try:4 out = fn()5 results[name] = {"ok": True, **(out or {})}6 print(f"[{name}] ok " + " ".join(f"{k}={v}" for k, v in (out or {}).items()))7 except Exception as e:8 results[name] = {"ok": False, "err": f"{type(e).__name__}: {e}"}9 print(f"[{name}] FAILED {type(e).__name__}: {e}")10 traceback.print_exc()11 return fn12 return deco
다음과 같이 호출할 수 있습니다:
1out = {}23@rung("pre-checks")4def _():5 run_pure_checks()6 run_dsl_checks()78@rung("run")9def _():10 out["o"] = custom_kernel(*inputs)11 torch.cuda.synchronize()12 return {"shape": tuple(out["o"].shape),13 "finite": bool(torch.isfinite(out["o"]).all())}
benchmark rung의 경우 여러 가지를 할 수 있습니다:
- 전체 소요 시간에 대한 End-to-end 커널 실행 시간을 벤치마킹합니다.
- 커널 내부 추적(intra-kernel tracing)을 사용해 커널 내 섹션을 벤치마킹하고 출력에 덤프합니다(커스텀 트레이서 또는 CUPTI 사용).
- 생성된 IR, PTX, SASS, CUBIN을 dumps 디렉토리에 덤프하고 에이전트가 이를 살펴보게 합니다.
마지막 항목은 조금 더 자세히 설명할 수 있습니다. 때로는 DSL이 차선(suboptimal)의 PTX(결국에는 SASS)를 생성하도록 lowering하는 경우가 있습니다. 그런데 그 대신 사용할 수 있는 더 나은 명령어나 형태를 발견하게 됩니다. 에이전트는 PTX/SASS 텍스트 파일을 읽고, 차선의 부분을 DSL에 맡기는 대신 더 낮은 수준의 코드를 인라인할 수 있습니다. 여기서도 "검색 가능한" PTX 문서를 컨텍스트로 전달하는 것이 매우 유용합니다.
모든 것을 하나로 묶어 주는 마지막 요소는 프로파일링(Profiling)입니다. 에이전트가 NCU(Nsight Compute Systems) CLI에 접근할 수 있다면, 위의 검증 가능한 피드백 루프의 일부로 커널을 프로파일링하고 리포트를 생성하도록 요청할 수 있습니다.
마치며
"그럼 GPU 커널 개발은 이제 끝난 건가요?"
"음, 맞기도 하고, 사실 아니기도 합니다."
맞다는 것은, 레이아웃, 인덱싱, 추상화, 그리고 전반적인 구조 같은 어려운 부분들을 충분한 컨텍스트를 가진 에이전트가 대부분 해결할 수 있기 때문입니다. 2~3주 분량의 작업을 1~2일로 쉽게 줄일 수 있습니다. 아니라는 것은, 실제 병목 지점이 커널이 아닌 검증으로 이동했기 때문입니다. 이제 방법은 단 하나가 아닙니다. 컨텍스트와 하네스(harness)가 좋을수록 프로세스가 빨라집니다. 특수한 경우에는 그 효과가 더욱 크며, 좋은 하네스만 있으면 됩니다.
마지막으로, 에이전트를 자율적인 존재로 취급하기보다는, 여러분이 안내할 수 있는 매우 똑똑한 조수로 취급하는 것이 여전히 필요합니다. 바로 이 지점에서 GPU와 커널에 대한 근본적인 이해가 유용하게 쓰입니다. 여기서도 인간의 역할, 즉 여러분이 여전히 필요합니다.
달콤쌉싸름한 기분이죠, 저도 알아요 :)





