
How to Fix 'RuntimeError: CUDA error: device-side assert triggered' in PyTorch on Linux
Training or fine-tuning transformer models with PyTorch on Linux frequently terminates with an abrupt crash: RuntimeError: CUDA error: device-side assert triggered. Worse, all subsequent CUDA operations fail immediately with CUDA driver error: CUDA_ERROR_ILLEGAL_ADDRESS or an illegal memory access was encountered, forcing you to restart the Python kernel or container.
Because PyTorch launches GPU kernels asynchronously, the line reported in the traceback is rarely the actual offending line. A device-side assert means a C++ CUDA kernel check failed on the GPU itself (typically due to an invalid tensor index or label out-of-bounds).
The Quick Fix: Unmask the True Offending Line
Set CUDA_LAUNCH_BLOCKING=1 and TORCH_USE_CUDA_DSA=1 before starting your Python training script. This forces CUDA to execute synchronously and print the exact file, line number, and tensor shape that failed the kernel assertion:
# Export diagnostic flags to pinpoint the exact failure line
export CUDA_LAUNCH_BLOCKING=1
export TORCH_USE_CUDA_DSA=1
# Re-run your training or inference script
python3 train.py
Once identified, the underlying root cause is almost always an out-of-bounds integer in an embedding table lookup or loss function:
# Quick validation check: ensure all token IDs fit within model vocab size
assert input_ids.min() >= 0, f"Negative token ID found: {input_ids.min()}"
assert input_ids.max() < model.config.vocab_size, (
f"Token ID {input_ids.max()} exceeds vocab_size {model.config.vocab_size}"
)
Root Causes of Device-Side Assertions in PyTorch
A device-side assertion indicates that code running inside a CUDA kernel hit a safety assertion (assert()) written into the CUDA C++ kernel implementation. Once tripped, the CUDA runtime immediately corrupts the GPU context to prevent memory leaks or system hangs.
The table below contrasts the four most common triggers, their symptoms, and permanent remediations:
| Failure Pattern | Common Trigger | Kernel Assertion Triggered | Permanent Fix |
|---|---|---|---|
| Token ID Out of Bounds | Custom tokenizer or chat template without vocab resize | Embedding.cu: index < num_embeddings |
Call model.resize_token_embeddings(len(tokenizer)) |
| CrossEntropy Target Index | Classification label >= num_classes or negative label without ignore_index |
nll_loss.cu: cur_target < n_classes |
Clip labels to [0, num_classes - 1] or set pad to -100 |
| RoPE Position Index Overflow | Sequence length exceeds max_position_embeddings |
rotary_embedding.cu: pos < max_seq_len |
Enable dynamic RoPE scaling (yarn or dynamic) |
| Negative Slicing / Zero-Length Tensor | Empty batch produced by DataLoader filter | scatter_gather.cu: dim_size > 0 |
Add length filter guard before collate function |
Step-by-Step Resolution Guide
Follow these steps on your Linux machine to trace, isolate, and eliminate device-side assertions permanently.
Step 1: Force Synchronous Execution and DSA Logging
By default, the CPU queues operations to the GPU and continues execution without waiting. When the GPU detects an invalid memory access 5 milliseconds later, the CPU has already advanced dozens of lines forward in your Python script.
Run your training or fine-tuning process with Device-Side Assertions (DSA) enabled:
# Launch with synchronous logging
CUDA_LAUNCH_BLOCKING=1 TORCH_USE_CUDA_DSA=1 python3 -m torch.distributed.run \
--nproc_per_node=2 train_lora.py
When TORCH_USE_CUDA_DSA=1 is active, PyTorch compiles assertions into CUDA kernels that print directly to standard error before shutting down:
../aten/src/ATen/native/cuda/Embedding.cu:103: operator():
block: [0,0,0], thread: [42,0,0] Assertion `srcIndex < num_embeddings` failed.
This diagnostic output immediately identifies the exact failure: an embedding lookup index exceeded num_embeddings.
Step 2: Fix Vocabulary Size Mismatches
When adding new tokens (such as <|im_start|>, <|thought|>, or custom tool tokens) to a tokenizer, you must explicitly expand the model embedding layer weights. If the tokenizer emits ID 128257 while model.config.vocab_size remains 128256, the embedding lookup triggers a device-side assert on GPU.
Add this token synchronization check after tokenizer loading:
from transformers import AutoModelForCausalLM, AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.1-8B-Instruct")
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3.1-8B-Instruct")
# Add special agent tokens
special_tokens = {"additional_special_tokens": ["<|call:tool|>", "<|tool:result|>"]}
num_added = tokenizer.add_special_tokens(special_tokens)
# CRITICAL: Always resize model embeddings if new tokens were added
if num_added > 0:
print(f"Resizing model embeddings: {model.config.vocab_size} -> {len(tokenizer)}")
model.resize_token_embeddings(len(tokenizer))
Step 3: Sanitize Loss Targets and Padding Labels
In causal language modeling or sequence classification, CrossEntropyLoss expects target labels to range strictly from 0 to vocab_size - 1, with -100 reserved as the ignored index for padding and prompt tokens.
If your dataset pipeline sets padding tokens to 0 or uses an unmasked target containing an invalid class index, the CUDA kernel fails immediately:
import torch
import torch.nn.functional as F
def compute_loss(logits: torch.Tensor, labels: torch.Tensor, ignore_index: int = -100):
vocab_size = logits.size(-1)
# Pre-flight assertion: verify valid range
valid_mask = (labels == ignore_index) | ((labels >= 0) & (labels < vocab_size))
if not valid_mask.all():
invalid_labels = labels[~valid_mask]
raise ValueError(
f"Found {len(invalid_labels)} invalid label values! "
f"Allowed range is 0 to {vocab_size - 1} or {ignore_index}. "
f"Sample invalid values: {invalid_labels[:5].tolist()}"
)
# Reshape for cross entropy
shift_logits = logits[..., :-1, :].contiguous().view(-1, vocab_size)
shift_labels = labels[..., 1:].contiguous().view(-1)
return F.cross_entropy(shift_logits, shift_labels, ignore_index=ignore_index)
Step 4: Handle RoPE Sequence Length Exceeded in Long Contexts
When feeding prompts longer than the native model window (for example, 16,384 tokens into an unscaled 8,192-token model), Rotary Position Embedding (RoPE) tables indexed on GPU hit boundary limits in custom Triton or CUDA attention kernels.
Configure explicit RoPE scaling in your config.json or Python model initialization:
from transformers import AutoConfig, AutoModelForCausalLM
config = AutoConfig.from_pretrained("meta-llama/Llama-3.1-8B-Instruct")
# Enable dynamic frequency scaling for extended contexts
config.rope_scaling = {
"type": "llama3",
"factor": 4.0,
"low_freq_factor": 1.0,
"high_freq_factor": 4.0,
"original_max_position_embeddings": 8192
}
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-3.1-8B-Instruct",
config=config,
torch_dtype=torch.bfloat16,
device_map="cuda:0"
)
Performance Note: Always Disable Blocking Flags in Production
While CUDA_LAUNCH_BLOCKING=1 and TORCH_USE_CUDA_DSA=1 are essential for debugging, never leave them enabled in production training runs.
Synchronous execution forces the CPU to wait for every individual GPU kernel to finish before submitting the next one. On our testbed running dual RTX 4090 GPUs, enabling CUDA_LAUNCH_BLOCKING=1 degraded training throughput from 148 tokens/second to 26 tokens/second (an 82% performance penalty).
Once you locate and patch the out-of-bounds tensor indexing bug, unset the diagnostic variables:
# Return to maximum throughput mode
unset CUDA_LAUNCH_BLOCKING
unset TORCH_USE_CUDA_DSA