러스트로 GPU 커널을 작성한다는 건, 호스트 코드와 디바이스 코드를 한 언어로 쓰고 카고(cargo) 빌드 체계 안에서 관리한다는 뜻이다. 선택지는 크게 세 갈래다. 러스트 커널을 PTX로 직접 컴파일하는 길, 기존 CUDA 프로그래밍 모델을 러스트 바인딩으로 쓰는 길, 그리고 wgpu·Vulkan 컴퓨트처럼 벤더 중립 경로를 쓰는 길.
중요한 사실부터 말해 두자. 언어를 바꾼다고 커널이 빨라지지 않는다. GPU 성능은 메모리 접근 패턴과 점유율에서 결정되며 그건 언어와 무관하다. 러스트가 주는 건 경계 관리와 빌드 재현성이고, 그 결과로 얻는 건 커널을 자주 갈아엎을 수 있는 용기다.
나는 몇 년 전 러스트 프로젝트에 GPU 연산을 붙이려다 두 손 든 적이 있다. 바인딩 크레이트는 유지보수가 멈춰 있었고, build.rs가 CUDA 툴킷 버전마다 다르게 깨졌다. 결국 커널만 C++로 따로 빌드하고 FFI로 부르는, 아무도 행복하지 않은 구조로 마무리했다. 2026년의 상황은 그때보다 많이 낫다. 엔비디아가 러스트 지원을 공식 트랙으로 들고 나왔고, 벤더 중립 경로도 성숙했다.
세 갈래 경로 비교
| 경로 | 커널 언어 | 이식성 | 성숙도 | 적합한 경우 |
|---|---|---|---|---|
| 네이티브 러스트 → PTX | 러스트 | 엔비디아 전용 | 초기 단계 | 러스트 단일 언어로 통일하려는 신규 프로젝트 |
| CUDA 바인딩 | C++ 커널 + 러스트 호스트 | 엔비디아 전용 | 안정적 | 기존 CUDA 커널 자산이 있는 팀 |
| wgpu / Vulkan 컴퓨트 | WGSL·GLSL·SPIR-V | 넓음(AMD·인텔·애플 포함) | 실용 수준 | 벤더 중립이 필요하거나 데스크톱 앱에 내장 |
선택 기준은 단순하다. 텐서 코어 같은 벤더 고유 기능이 성능의 핵심이면 엔비디아 경로를, 여러 GPU에서 돌아야 하면 wgpu를 고른다. 중간은 없다. "나중에 이식하겠다"는 계획은 거의 실현되지 않는다.
GPU 프로그래밍의 사고 모델부터
언어 이전에 실행 모델을 이해해야 한다. GPU는 수천 개의 스레드를 동시에 돌리되, 스레드들은 워프(warp, 보통 32개) 단위로 같은 명령을 함께 실행한다. 여기서 두 가지 결론이 나온다.
- 분기(if)가 워프 안에서 갈리면 양쪽을 순차 실행한다 — 이것이 워프 다이버전스이고, 최악의 경우 성능이 절반 이하로 떨어진다.
- 인접한 스레드가 인접한 메모리를 읽으면 한 번의 트랜잭션으로 묶인다(coalescing). 흩어져 읽으면 대역폭이 그만큼 낭비된다.
커널의 기본 형태
벡터 덧셈 같은 가장 단순한 커널의 구조를 보면 공통 패턴이 보인다. 전역 인덱스를 계산하고, 경계를 검사하고, 작업을 수행한다. 어떤 경로를 택하든 이 뼈대는 같다.
// 개념적 형태 (경로에 따라 문법은 달라진다)
// 1) 전역 인덱스 = 블록 인덱스 * 블록 크기 + 스레드 인덱스
// 2) 배열 길이를 넘지 않는지 반드시 검사
// 3) 각 스레드는 자기 몫만 처리
fn vector_add(a: &[f32], b: &[f32], out: &mut [f32], idx: usize) {
if idx < out.len() { // 경계 검사를 빼먹으면 조용히 이웃 데이터를 덮는다
out[idx] = a[idx] + b[idx];
}
}
// 호스트 측: 블록/그리드 크기 결정
// 블록 크기는 보통 128 또는 256에서 시작해 실측으로 조정한다.
// grid = (n + block - 1) / block (올림 나눗셈)
경계 검사를 생략하면 어떻게 되는지 굳이 경험해 볼 필요는 없다. GPU에서는 배열 밖 접근이 세그멘테이션 폴트로 친절하게 알려 주지 않는다. 이웃 스레드의 데이터를 덮고, 결과는 그럴듯한 숫자로 나오며, 며칠 뒤에 학습 손실이 이상하다는 걸 깨닫게 된다.
러스트가 실제로 막아 주는 것
- 호스트–디바이스 버퍼 수명 관리 오류
- 해제된 메모리 참조
- 에러 처리 누락(Result 강제)
- 빌드 의존성 불일치(카고가 관리)
- 커널 내부 인덱스 계산 오류
- 동기화 누락으로 인한 경쟁 상태
- 워프 다이버전스·뱅크 충돌
- 메모리 접근 패턴에서 오는 대역폭 병목
이 표를 오해하면 안 된다. 러스트가 잡아 주는 항목들은 GPU 코드에서 디버깅이 가장 고통스러운 부류다. 호스트 쪽 버퍼 수명 문제는 재현이 어렵고, 에러 코드를 무시한 CUDA 호출은 한참 뒤에 엉뚱한 지점에서 터진다. 이 둘만 사라져도 개발 경험이 크게 달라진다.
공유 메모리와 타일링 — 성능이 갈리는 지점
행렬 곱을 예로 들면, 순진한 구현은 같은 데이터를 전역 메모리에서 반복해 읽는다. 타일링은 블록 단위로 데이터를 공유 메모리에 올려 두고 재사용한다. 전역 메모리 접근은 수백 사이클, 공유 메모리는 수십 사이클 수준이라 차이가 크다.
32로 잡았을 때 자주 발생하며, 폭을 33으로 늘려 패딩하는 고전적 우회법이 있다. 프로파일러에서 공유 메모리 효율이 낮게 나오면 이것부터 의심한다.벤더 중립 경로 — wgpu를 쓸 때
데스크톱 앱이나 크로스 플랫폼 도구에 GPU 연산을 넣는다면 wgpu가 현실적이다. WGSL로 컴퓨트 셰이더를 작성하고, 같은 코드가 Vulkan·Metal·DirectX 백엔드에서 돌아간다. 웹까지 대상이라면 WebGPU로 그대로 이어진다.
| 항목 | CUDA 계열 | wgpu |
|---|---|---|
| 하드웨어 | 엔비디아 | AMD·인텔·애플·엔비디아 |
| 정밀도 제어 | FP8·FP4 등 세밀한 지원 | 기본 타입 위주 |
| 텐서 코어 활용 | 가능 | 제한적 |
| 생태계 라이브러리 | 풍부(cuBLAS 등) | 직접 구현 비중 높음 |
| 배포 | 드라이버·툴킷 의존 | 상대적으로 단순 |
AI 학습·추론처럼 텐서 코어가 성능의 핵심인 워크로드에서는 wgpu가 불리하다. 반대로 이미지 처리, 시뮬레이션, 데이터 변환처럼 범용 병렬 연산이면 충분히 실용적이다.
측정 없이는 아무것도 아니다
첫 항목을 특히 강조하고 싶다. 작은 배열을 GPU로 보내 계산하고 다시 가져오면, 거의 항상 CPU보다 느리다. GPU가 이기려면 데이터가 충분히 크거나, 한 번 올린 데이터를 여러 번 재사용해야 한다.
언제 GPU를 쓰지 말아야 하나
분기가 데이터마다 크게 갈리는 작업, 순차 의존성이 강한 알고리즘, 데이터가 작은 작업은 GPU에 맞지 않는다. 트리 탐색이나 상태 기계처럼 스레드마다 다른 경로를 가는 코드는 워프 다이버전스로 이득이 사라진다. 이런 경우 SIMD를 쓴 CPU 구현이 더 빠른 경우가 많다.
자주 묻는 질문
러스트로 GPU 커널을 쓰면 C++보다 빨라지나요?
아닙니다. GPU 성능은 메모리 접근 패턴, 점유율, 공유 메모리 사용, 워프 다이버전스에서 결정되며 이는 언어와 무관합니다. 러스트의 이점은 호스트 측 메모리 안전성과 빌드 재현성이고, 그 결과 커널을 더 자주 개선할 수 있다는 점입니다.
어떤 경로를 선택해야 하나요?
텐서 코어 같은 엔비디아 고유 기능이 성능의 핵심이면 CUDA 계열을, 여러 벤더 GPU에서 동작해야 하면 wgpu·Vulkan 컴퓨트를 선택하세요. 기존 CUDA 커널 자산이 있다면 커널은 두고 호스트 코드만 러스트로 옮기는 점진적 방식이 안전합니다.
GPU 커널 최적화는 무엇부터 봐야 하나요?
메모리입니다. 인접 스레드가 인접 주소를 읽는지(합병 접근), 반복 읽는 데이터를 공유 메모리로 올렸는지, 뱅크 충돌이 없는지 순서로 확인하세요. 연산량 줄이기는 그다음입니다.
작은 데이터에도 GPU를 쓰면 좋은가요?
대개 손해입니다. 호스트와 디바이스 간 전송 비용이 연산 이득을 넘기기 때문입니다. 데이터가 충분히 크거나, 한 번 올린 데이터를 여러 커널에서 재사용하는 구조여야 이득이 납니다.
워프 다이버전스는 어떻게 줄이나요?
같은 워프 안의 스레드들이 같은 분기를 타도록 데이터를 정렬하거나, 분기 대신 산술 연산으로 대체하는 방법이 있습니다. 조건이 데이터에 따라 무작위로 갈리는 알고리즘은 애초에 GPU에 적합하지 않을 수 있습니다.
프로덕션에 러스트 GPU 스택을 써도 되나요?
네이티브 러스트 커널 경로는 아직 성숙 단계에 있어, 검증된 프로덕션 커널을 서둘러 옮기는 것은 권하지 않습니다. 신규 실험 워크로드나 비핵심 경로부터 시작해 툴체인 안정성을 직접 측정한 뒤 확대하는 방식이 안전합니다.

댓글 0