
PyTorch로 모델을 학습시키다가 RuntimeError: CUDA error: device-side assert triggered를 만나면 대부분 당황합니다. 에러 메시지가 가리키는 줄을 아무리 들여다봐도 코드에 문제가 없어 보이기 때문입니다. 이는 우연이 아니라 CUDA의 비동기 실행 구조 때문에 생기는 필연적인 현상입니다. 이 글에서는 왜 스택트레이스가 엉뚱한 위치를 가리키는지, 그리고 실제 오류가 발생한 줄을 정확히 찾아내는 구체적인 절차를 다룹니다.
왜 스택트레이스가 엉뚱한 곳을 가리키는가
CUDA 커널 실행은 기본적으로 비동기(asynchronous)로 동작합니다. model(input)이나 tensor.cuda() 같은 호출은 GPU에 작업을 큐에 올려놓기만 하고, CPU 쪽 파이썬 코드는 즉시 다음 줄로 넘어갑니다. 실제로 GPU가 해당 커널을 실행하고 어설션(assert)에 걸리는 시점은 파이썬 코드가 이미 몇 줄, 심하면 몇 함수를 더 지나간 이후입니다.
문제는 CUDA 에러가 곧바로 파이썬으로 전달되지 않는다는 점입니다. GPU에서 발생한 오류는 다음번에 CPU-GPU 동기화가 일어나는 지점, 예를 들어 .item(), .cpu(), print(tensor), loss.backward() 같은 호출에서야 비로소 예외로 드러납니다. 그래서 트레이스백에는 실제 오류를 일으킨 임베딩 조회나 인덱싱 연산이 아니라, 우연히 그 시점에 동기화를 요구한 코드 줄이 찍힙니다. 게다가 한 번 device-side assert가 발생하면 CUDA 컨텍스트 자체가 손상되어, 이후의 모든 CUDA 호출이 같은 에러를 반복해서 뱉습니다. 이 때문에 프로세스를 재시작하지 않고 계속 디버깅하면 점점 더 헷갈리는 상황에 빠지게 됩니다.
CUDA_LAUNCH_BLOCKING=1로 진짜 실패 지점 찾기
가장 먼저 시도해야 할 방법은 CUDA_LAUNCH_BLOCKING 환경변수를 1로 설정하는 것입니다. 이 값을 켜면 모든 커널 호출이 동기적으로 실행되어, CPU는 각 커널이 끝날 때까지 기다린 다음에야 다음 줄로 넘어갑니다. 그 결과 에러가 실제로 발생한 커널 호출 줄에서 바로 예외가 터지므로, 스택트레이스가 정확한 위치를 가리키게 됩니다.
# 리눅스/맥 환경
CUDA_LAUNCH_BLOCKING=1 python train.py
# 파이썬 스크립트 안에서 설정하는 경우 (import torch 이전에!)
import os
os.environ["CUDA_LAUNCH_BLOCKING"] = "1"
import torch
주의할 점은 이 환경변수를 import torch보다 먼저 설정해야 한다는 것입니다. CUDA 컨텍스트가 초기화된 이후에 값을 바꿔도 반영되지 않는 경우가 있습니다. 또한 동기 실행은 GPU 파이프라이닝 이점을 없애 학습 속도를 눈에 띄게 떨어뜨리므로, 문제 위치를 특정한 뒤에는 반드시 꺼두어야 합니다. 디버깅 전용 스위치로 생각하는 것이 맞습니다.
가장 흔한 원인: 인덱스 범위 초과

실무에서 device-side assert를 유발하는 원인의 상당수는 인덱스 범위 초과입니다. 대표적으로 세 가지 패턴이 반복됩니다.
첫째, nn.Embedding에 어휘 크기(num_embeddings)를 벗어난 인덱스를 넣는 경우입니다.
import torch
import torch.nn as nn
embedding = nn.Embedding(1000, 128).cuda()
idx = torch.tensor([500, 999, 1000]).cuda() # 유효 범위는 0~999, 1000은 범위 밖
out = embedding(idx)
print(out)
# RuntimeError: CUDA error: device-side assert triggered
# (내부적으로는 srcIndex < srcSelectDimSize 어설션 실패)
둘째, nn.CrossEntropyLoss나 F.nll_loss에 클래스 개수를 벗어난 타깃 라벨을 전달하는 경우입니다. 예를 들어 모델 출력이 10개 클래스인데 라벨 텐서에 10이나 그 이상의 값, 혹은 음수가 섞여 있으면 손실 계산 커널에서 t >= 0 && t < n_classes 어설션이 실패합니다.
셋째, torch.gather, index_select, scatter_, one_hot 등에서 인덱스 텐서의 값이 대상 차원의 크기를 벗어난 경우입니다. 데이터 전처리 과정에서 토크나이저의 vocab_size와 모델 임베딩 레이어의 크기가 어긋났을 때 특히 자주 나타납니다.
CPU로 재현해서 명확한 에러 메시지 얻기
CUDA_LAUNCH_BLOCKING=1로도 원인이 명확하지 않다면, 같은 코드를 CPU에서 돌려보는 것이 가장 효과적입니다. GPU 커널은 성능을 위해 검증 로직을 최소화하기 때문에 “device-side assert triggered”라는 뭉뚱그려진 메시지만 던지지만, CPU 연산은 대부분 파이썬 레벨에서 구체적인 예외를 발생시킵니다.
import torch
import torch.nn as nn
device = "cpu" # 혹은 os.environ["CUDA_VISIBLE_DEVICES"] = ""
embedding = nn.Embedding(1000, 128).to(device)
idx = torch.tensor([500, 999, 1000]).to(device)
out = embedding(idx)
# IndexError: index out of range in self
위 예시처럼 CPU에서는 “index out of range in self”라는 명확한 메시지와 함께 정확한 파이썬 줄 번호가 나옵니다. GPU 메모리가 부족해 전체 배치를 CPU로 옮기기 어렵다면, 배치 크기를 줄이거나 문제가 의심되는 레이어만 잘라내어 최소 재현 코드를 만드는 방식이 효율적입니다. 재현 코드를 최소화하는 과정 자체가 원인을 좁혀가는 디버깅이 됩니다.
compute-sanitizer로 커널 단위까지 정밀 진단하기

CUDA_LAUNCH_BLOCKING=1과 CPU 재현으로도 해결되지 않는, 커스텀 CUDA 커널이나 서드파티 확장 모듈에서 발생하는 문제라면 NVIDIA의 compute-sanitizer가 유용합니다. 과거 cuda-memcheck의 후속 도구로, CUDA Toolkit에 기본 포함되어 있습니다.
compute-sanitizer --tool memcheck python train.py
memcheck 툴은 실제로 어떤 커널에서 어떤 스레드가 잘못된 메모리 접근이나 어설션 실패를 일으켰는지, 소스 파일과 라인 정보(디버그 심볼이 있는 경우)까지 함께 출력합니다. 실행 속도가 크게 느려지므로 전체 학습이 아니라 문제가 재현되는 최소 스크립트에 적용하는 것을 권장합니다. 이 도구는 GPU 메모리 관련 문제나 out-of-bounds 접근을 커널 단위에서 정밀하게 짚어주기 때문에, 파이썬 레벨 디버깅으로 원인이 좁혀지지 않을 때 마지막 수단으로 효과적입니다.
CUDA의 비동기 실행 모델 자체는 PyTorch 공식 문서의 CUDA semantics 페이지에 상세히 설명되어 있으며, compute-sanitizer의 사용법은 NVIDIA 공식 Compute Sanitizer 문서에서 확인할 수 있습니다.
정리 및 다음 단계
device-side assert triggered가 가리키는 줄은 실제 원인이 아니라 다음 동기화 지점일 뿐입니다. 이는 CUDA 커널이 비동기로 실행되기 때문에 생기는 구조적인 특성입니다. 순서대로 접근하면 원인을 훨씬 빠르게 좁힐 수 있습니다. 먼저 CUDA_LAUNCH_BLOCKING=1을 켜서 정확한 커널 호출 줄을 확인하고, 임베딩·손실 함수·인덱싱 연산의 범위를 의심합니다. 그래도 불명확하면 같은 코드를 CPU에서 돌려 구체적인 파이썬 예외 메시지를 얻고, 커스텀 CUDA 코드가 얽혀 있다면 compute-sanitizer로 커널 단위까지 파고듭니다.
지금 이 에러를 마주하고 있다면, 가장 먼저 할 일은 학습 스크립트 맨 위에 os.environ["CUDA_LAUNCH_BLOCKING"] = "1" 한 줄을 추가하고 다시 실행해보는 것입니다.
자주 묻는 질문(FAQ)
Q1. CUDA_LAUNCH_BLOCKING=1을 켜도 여전히 같은 줄에서 에러가 나면 어떻게 하나요?
A. 해당 줄에서 호출하는 함수 내부(임베딩, 손실 함수, 커스텀 연산 등)에 여러 개의 텐서 인덱싱 연산이 포함되어 있을 가능성이 높습니다. 그 함수를 더 잘게 쪼개서 각 연산을 별도 줄로 분리한 뒤 다시 실행하면 정확히 어떤 연산에서 실패하는지 좁힐 수 있습니다.
Q2. 한 번 device-side assert가 발생한 뒤 계속 같은 에러만 나옵니다.
A. CUDA 컨텍스트가 손상되었기 때문입니다. 같은 프로세스에서는 이후 어떤 CUDA 연산을 시도해도 정상적으로 복구되지 않으므로, 파이썬 프로세스(또는 주피터 커널)를 재시작한 뒤 원인을 고치고 다시 실행해야 합니다.
Q3. 배치 단위로는 재현되는데 단일 샘플로는 재현되지 않습니다.
A. 배치 안에 인덱스 범위를 벗어나는 값이 섞여 있을 때만 발생하는 경우입니다. 배치를 CPU로 옮긴 뒤 라벨이나 인덱스 텐서의 최댓값·최솟값을 .max(), .min()으로 직접 확인해 모델이 기대하는 범위(예: 임베딩의 num_embeddings, 클래스 개수)와 비교해보는 것이 가장 빠른 확인 방법입니다.