Why your fine-tuning run is so much slower than it should be, and how to speed it up
training throughput far below expected tokens/sec or steps/sec for the GPU and model size
Also appears as
- GPU utilization stuck below 50% during training (checked via nvidia-smi)
- each training step takes several times longer than benchmark numbers suggest it should
Short answer
Slow fine-tuning is usually a GPU sitting idle waiting on the CPU dataloader, a batch size too small to keep the GPU's compute units busy, missing FlashAttention or another fused kernel so attention runs in a much slower fallback path, or CPU/NVMe offloading (for models too large to fit fully in VRAM) that is inherently bound by PCIe bandwidth rather than GPU compute. Check GPU utilization with nvidia-smi first to know which of these you're actually facing.
Affects: LoRA and full fine-tuning on single or multi-GPU setups, HuggingFace transformers, Accelerate, and DeepSpeed
Fastest path to faster training
- 1Run nvidia-smi in a loop during training and check GPU utilization; if it's frequently below 70-80%, the bottleneck is likely the dataloader or CPU-side preprocessing, not the GPU itself.
- 2Increase per-device batch size and/or gradient accumulation steps until GPU memory is well utilized (watch nvidia-smi memory.used), since small batches underutilize modern GPU compute.
- 3Enable FlashAttention 2 (attn_implementation='flash_attention_2') if your model and GPU support it, since the default eager attention implementation is meaningfully slower, especially at longer sequence lengths.
- 4Increase dataloader_num_workers and enable pin_memory=True so data loading and tokenization happen in parallel with GPU compute instead of blocking it.
- 5If using CPU or NVMe offloading (DeepSpeed ZeRO-Offload) because the model doesn't fit in VRAM, understand that offloading is inherently PCIe-bandwidth-bound; the real fix is more or bigger GPUs, not further offload tuning.
How to confirm this is your problem
- nvidia-smi shows GPU utilization frequently dropping to 0-30% between steps rather than staying consistently high
- Training throughput (tokens/sec or steps/sec) is far below published benchmark numbers for similar hardware and model size
- Increasing batch size doesn't meaningfully speed up wall-clock training time per epoch
- CPU usage is pegged at 100% on one or more cores while GPU sits comparatively idle
- Training with CPU or disk offloading is dramatically slower than training without it (expected, but often more severe than anticipated)
Root causes and fixes
Batch size too small to utilize the GPU's compute units
Modern GPUs have far more parallel compute capacity than a very small batch can occupy; with a tiny batch, most of the GPU's tensor cores sit idle during each forward and backward pass, and per-step overhead (kernel launches, synchronization) becomes a larger fraction of total time relative to actual compute, making each step slower than it needs to be for the work performed.
Fix: Increase per_device_train_batch_size as far as VRAM allows, and use gradient_accumulation_steps to reach your desired effective batch size without exceeding memory, which recovers most of the throughput lost to a too-small batch.
Sequence packing not used, wasting compute on padding
Without packing, a batch of variable-length examples is padded to the length of its longest member, meaning shorter examples waste compute processing padding tokens that produce no useful gradient. On datasets with high length variance, this can mean a large fraction of every batch's compute is spent on tokens that contribute nothing to learning.
Fix: Enable sequence packing (TRL's SFTTrainer supports packing=True) which concatenates multiple short examples into fixed-length sequences separated by EOS tokens, dramatically reducing wasted compute on padding for datasets with variable-length examples.
Missing FlashAttention or another fused attention kernel
The default eager attention implementation computes the full attention matrix with several separate, unfused GPU operations, each with its own memory read/write overhead. FlashAttention and similar fused kernels compute the same mathematical result with a single, memory-efficient fused kernel, which is significantly faster and also uses less memory, especially as sequence length grows.
Fix: Load the model with attn_implementation='flash_attention_2' (or 'sdpa' as a solid built-in alternative if FlashAttention installation isn't feasible) instead of leaving the default eager implementation active.
model = AutoModelForCausalLM.from_pretrained(model_id, attn_implementation='flash_attention_2')
Dataloader-bound: CPU preprocessing and tokenization can't keep up with the GPU
If tokenization, data augmentation, or disk I/O for reading training examples happens synchronously on the main process with too few worker processes, the GPU finishes its compute for a batch and then sits idle waiting for the next batch to be prepared, which shows up as low GPU utilization even though the model and hardware are otherwise capable of much higher throughput.
Fix: Increase dataloader_num_workers (start around 4-8 and tune based on CPU core count), enable pin_memory=True, and pre-tokenize the dataset once and cache it to disk rather than re-tokenizing on the fly every epoch.
PCIe-bound CPU or NVMe offloading for models that don't fit in VRAM
When using DeepSpeed ZeRO-Offload or similar techniques to move optimizer states or parameters to CPU RAM or NVMe storage because the model is too large for available VRAM, every offloaded tensor must move across the PCIe bus each time it's needed, and PCIe bandwidth is orders of magnitude slower than GPU-to-GPU memory bandwidth, making offloaded training inherently much slower regardless of how well it's tuned.
Fix: Recognize this as a hardware capacity limitation rather than a tuning problem: the durable fix is more VRAM (larger or additional GPUs) or a smaller/quantized model that fits without offloading, not further offload configuration adjustments.
Mixed precision or TF32 not enabled
Training in full fp32 precision without enabling TF32 (on Ampere and newer GPUs) or bf16/fp16 mixed precision leaves substantial GPU compute throughput on the table, since these formats allow the GPU's tensor cores to run at significantly higher throughput than full fp32 matrix operations.
Fix: Enable bf16 or fp16 mixed precision training explicitly, and ensure torch.backends.cuda.matmul.allow_tf32 = True is set if any fp32 operations remain in your pipeline.
Diagnostic commands
Monitor GPU utilization live during training
nvidia-smi --query-gpu=utilization.gpu,utilization.memory --format=csv -l 2
Utilization consistently below 70-80% points to a bottleneck outside the GPU itself (dataloader, small batch, or offloading), while consistently high utilization with still-slow wall-clock time suggests the workload itself (attention implementation, precision) needs optimizing.
Check which attention implementation is active
python -c "print(model.config._attn_implementation)"
Should read flash_attention_2 or sdpa for good performance. A value of eager means you're leaving meaningful speed and memory efficiency on the table, especially at longer sequence lengths.
Profile dataloader wait time versus compute time
python -c "from torch.profiler import profile; # profile a few training steps and inspect DataLoader time"
A large fraction of wall-clock time attributed to data loading rather than model compute confirms a dataloader bottleneck, pointing you toward more workers, caching, or pre-tokenization rather than GPU-side tuning.
Check whether offloading is active and how much is offloaded
python -c "print(ds_config.get('zero_optimization', {}).get('offload_optimizer'))"If offload_optimizer or offload_param is configured, expect meaningfully lower throughput than a fully in-VRAM run; this confirms PCIe-bound offloading as an expected cause rather than a bug to chase further.
Stopping it from happening again
- Benchmark a short training run (50-100 steps) with nvidia-smi monitoring before committing to a full multi-hour job, to catch utilization problems early.
- Pre-tokenize and cache datasets to disk once rather than re-tokenizing on the fly every epoch.
- Default new training scripts to flash_attention_2 or sdpa rather than eager attention.
- Size GPU capacity to fit the model and desired batch size without offloading whenever the budget allows, since offloading should be a deliberate fallback, not a default.
- Track tokens/sec as a standard metric across runs so throughput regressions are caught immediately rather than discovered after a slow run completes.
When this becomes an architecture problem
If GPU utilization is already high and you've enabled FlashAttention, packing, and mixed precision, but throughput still falls short of what the hardware should deliver, that points to a deeper configuration or parallelism strategy issue (tensor parallelism setup, interconnect bottleneck, or DeepSpeed ZeRO stage choice) worth a focused review. If you're regularly forced into CPU/NVMe offloading because your current GPU fleet can't fit the models you need to fine-tune, that's a capacity planning and hardware decision, not a software tuning one.
Frequently asked questions
What GPU utilization should I expect during fine-tuning?
A well-tuned training run typically keeps GPU utilization at 80-95% for most of each step, with brief dips at batch boundaries. Utilization frequently dropping to 0-30% indicates a dataloader, batch size, or offloading bottleneck rather than the GPU itself being the limiting factor.
Does FlashAttention actually make a big difference for fine-tuning speed?
Yes, particularly as sequence length grows. FlashAttention avoids materializing the full attention matrix and uses a single fused, memory-efficient kernel, commonly providing significant speedups and memory savings over the default eager attention implementation, especially for context lengths beyond a few thousand tokens.
Why is CPU offloading so much slower than training fully on GPU?
Offloaded parameters or optimizer states must move across the PCIe bus between CPU RAM and GPU VRAM every time they're needed, and PCIe bandwidth is dramatically lower than GPU memory bandwidth. This makes offloaded training inherently much slower regardless of tuning; it trades speed for the ability to fit a model that otherwise wouldn't fit in available VRAM at all.
Does increasing batch size always speed up training?
Only up to the point where the GPU's compute units are fully utilized; beyond that, further batch size increases mainly consume more memory without meaningfully improving throughput, and can eventually cause out-of-memory errors. Monitor GPU utilization to know when you've reached diminishing returns.
Size it properly next time
Free calculators that prevent this class of failure before you provision hardware.
Fine-Tuning GPU-Hours Estimator
Estimate GPU-hours for LoRA, QLoRA, and full fine-tuning on the same model size and dataset, so you can compare method tradeoffs before choosing.
Free ToolLoRA Fine-Tuning Cost Calculator
Turn model size, dataset tokens, epochs, and rank into a GPU-hour and dollar estimate for a LoRA fine-tuning run on rented or owned hardware.
Free ToolGPU Sizing Calculator for LLM Inference
Work out how many GPUs you need to serve a given open-weight model to your user base, based on memory footprint and token throughput.
Free ToolMulti-GPU Tensor Parallelism Calculator
Model how tensor-parallel throughput actually scales across multiple GPUs, accounting for interconnect overhead that keeps scaling sub-linear.
Related problems
Gradient checkpointing errors during fine-tuning
Gradient checkpointing errors during fine-tuning almost always come from three sources: the use_reentrant parameter left unset (it now must be explicit and False is usually correct for transformer models), an attention implementation that isn't fully compatible with checkpointing's re-computation approach, or leaving use_cache=True enabled while checkpointing is on, which conflicts because checkpointing recomputes the forward pass and a live KV cache assumes it won't be recomputed. Set use_reentrant=False and use_cache=False together.
CUDA out of memory even though nvidia-smi shows free VRAM
This almost always means memory fragmentation: the allocator has enough total free memory but no single contiguous block large enough for the requested allocation, because the address space is broken into many small free-and-used segments from prior allocations of different sizes. The fix is enabling PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True, reducing allocation size variability, or restarting the process to reset the address space.
How to reduce VRAM usage for LLM inference
VRAM usage during inference comes from three independent budgets: model weights (params x bytes/param), KV cache (scales with batch x context length), and activation/workspace memory. Reducing usage means attacking whichever budget dominates: quantize weights to cut the largest fixed cost, cap max context length and concurrency to cut the largest variable cost, and use an efficient attention/serving backend to minimize workspace overhead.
Training loss not decreasing during fine-tuning
Training loss that stays flat is most often caused by a LoRA adapter that targets the wrong modules (missing q_proj/k_proj/v_proj/o_proj), a learning rate that is too low for LoRA or too high and bouncing, or labels that were never masked so the model is trying to learn the prompt tokens as if they were random noise. Check target_modules first, then the label mask, then the learning rate.
GuideFine-Tuning Failure Modes: What Actually Goes Wrong
Fine-tuning failure modes that actually derail enterprise projects: catastrophic forgetting, eval overfitting, data leakage, and how to catch each one.
GuideMulti-Node LLM Training Infrastructure: Networking and Storage
Multi-node LLM training infrastructure explained: InfiniBand vs RoCE tradeoffs, storage throughput needs, and cluster topology for enterprise fine-tuning.
GuideOn-Prem GPU Cluster Design: Node Sizing, Networking, and Storage
Design an on-prem GPU cluster: node sizing for H100/H200/B200, InfiniBand vs RoCE networking, storage throughput, and rack power for enterprise AI workloads.
GuideLoRA vs QLoRA: Choosing the Right Fine-Tuning Method
LoRA vs QLoRA for enterprise fine-tuning: rank and alpha choices, real VRAM math by model size, and when each method actually wins.
Still stuck, or tired of fighting your own infrastructure?
Netray deploys and operates on-prem AI for regulated manufacturers and defense suppliers. We have debugged this stack in production, on air-gapped networks, at scale.