Performance

GPU Performance Degradation Troubleshooting

A measurement-first workflow for slower inference, lower utilization, memory pressure, and changing throughput.

By Hostnot GPU ·

Start a performance investigation with one measurable regression: the same training step takes longer, image throughput falls, or request tail latency rises. GPU utilization alone is not an objective, and “100% busy” does not mean the workload is efficient.

Create a comparable baseline

Record model/code revision, input shape, precision, context and output length, batch or concurrency, GPU count and memory, driver, framework, image digest, and storage location. Separate model download, load, compilation, and warm-up from steady execution. For inference, distinguish queue wait, time to first token, decode rate, and end-to-end latency. Compare equal work: a shorter answer can make an apparent throughput improvement misleading.

Observe GPU and host together

nvidia-smi --query-gpu=timestamp,index,utilization.gpu,memory.used,power.draw,temperature.gpu,clocks.sm --format=csv -l 2
# In another terminal, where available:
vmstat 1 10

Stop the GPU loop with Ctrl+C. Unsupported fields should be omitted using the installed utility's help. vmstat can reveal CPU, runnable-process, paging, and I/O pressure; its first row is an average since boot, while subsequent rows cover the requested interval. Sampled GPU utilization can miss short bursts and does not distinguish compute-bound from memory-bound kernels.

Follow the bottleneck

Time CUDA operations correctly

CUDA launches are asynchronous. For an isolated PyTorch test, synchronize before starting and before stopping a host timer. This example times only a small matrix multiplication sequence; it is a timing demonstration, not a GPU benchmark or expected score.

import time
import torch

assert torch.cuda.is_available(), "CUDA is unavailable"
x = torch.randn(1024, 1024, device="cuda")
with torch.inference_mode():
    for _ in range(5):
        y = x @ x
    torch.cuda.synchronize()
    start = time.perf_counter()
    for _ in range(20):
        y = x @ x
    torch.cuda.synchronize()
print("seconds for 20 operations:", time.perf_counter() - start)

Do not insert synchronization into every operation of the production workload: it changes overlap and performance. For deeper analysis, use a short PyTorch Profiler or Nsight Systems trace with the real workload. Tracing adds overhead and may record tensor metadata, paths, or user content; restrict access and retention.

Prove the improvement and its cost

Change one variable, repeat enough warm measurements to report median and tail behavior, and check correctness and errors. Retain the baseline configuration for rollback. Report completed work per billed interval, including retries and idle setup time where relevant; hourly price alone cannot establish cost efficiency. Avoid upgrading drivers or raising power limits on a managed host in response to an unprofiled slowdown.

If escalation is needed, provide the fixed input shape, safe reproduction, before/after versions, and timeline. Next: verify distributed execution or design a reproducible benchmark.

Technical references