REBRANDING NOTICE: EzDeploy is now Roptal. Platform launching on roptal.com. Docs: docs.roptal.com.REBRANDING NOTICE: EzDeploy is now Roptal. Platform launching on roptal.com. Docs: docs.roptal.com.REBRANDING NOTICE: EzDeploy is now Roptal. Platform launching on roptal.com. Docs: docs.roptal.com.REBRANDING NOTICE: EzDeploy is now Roptal. Platform launching on roptal.com. Docs: docs.roptal.com.
Oryvo
← All articles

GPU Memory Optimization for PyTorch Inference: Reduce VRAM by Half

T4 has 16GB VRAM. If your model uses 15GB you are at the limit. Cut usage in half with FP16, quantization, Flash Attention.

GPU Memory Optimization for PyTorch Inference: Reduce VRAM Usage by Half

GPUs are expensive. A T4 has 16GB VRAM. If your model and overhead use 15GB, you can't increase batch size, add a KV cache, or run anything else on that GPU.

Here's how to cut PyTorch inference memory usage without changing your model architecture.

1. Use FP16 Instead of FP32

Half precision cuts memory usage by 50% with negligible accuracy loss for most models.

model = AutoModel.from_pretrained("model-name")
model = model.half()  # FP32 → FP16
model = model.to("cuda")

Memory: 14GB → 7GB for a 7B parameter model.

Verify accuracy:

with torch.no_grad():
    fp32_out = fp32_model(input_ids)
    fp16_out = fp16_model(input_ids)
    
    diff = (fp32_out - fp16_out.float()).abs().max().item()
    print(f"Max difference: {diff:.6f}")

For most transformer models, FP16 max difference is under 1e-3. Acceptable for production.

2. Use torch.no_grad()

PyTorch builds a computation graph during forward passes for backpropagation. During inference, you don't need gradients. The graph wastes memory.

# Bad: computes gradients (memory grows with each forward pass)
output = model(input_ids)

# Good: no gradient computation
with torch.no_grad():
    output = model(input_ids)

Memory savings: ~30-40% depending on model size.

3. Disable KV Cache for Short Sequences

For short inputs (under 128 tokens), the KV cache overhead is larger than the benefit. Disable it:

output = model.generate(
    input_ids,
    use_cache=False,  # disable KV cache
    max_new_tokens=50,
)

Memory savings: 100-500MB for small models, 1-2GB for large models.

4. Use torch.inference_mode() (PyTorch 1.9+)

inference_mode() is a stricter version of no_grad() that additionally disables autograd version tracking. Slightly faster and uses less memory:

with torch.inference_mode():
    output = model(input_ids)

Functionally identical to no_grad() for inference but with slightly lower overhead. Use it for production inference.

5. Offload to CPU When GPU Is Idle

If your GPU sits idle between requests, move the model to CPU and only load it to GPU when a request arrives:

import threading
import time

class LazyGPUModel:
    def __init__(self, model_name):
        self.model = AutoModel.from_pretrained(model_name)
        self.on_gpu = False
        self.last_used = time.time()
        self._idle_timeout = 300  # 5 minutes
    
    def predict(self, inputs):
        if not self.on_gpu:
            self.model = self.model.to("cuda")
            self.on_gpu = True
        self.last_used = time.time()
        with torch.inference_mode():
            return self.model(inputs)
    
    def check_idle(self):
        if self.on_gpu and time.time() - self.last_used > self._idle_timeout:
            self.model = self.model.to("cpu")
            torch.cuda.empty_cache()
            self.on_gpu = False

This frees the GPU for other processes when there's no traffic. Adds a ~500ms penalty on the first request after idle (loading model back to GPU).

6. Use Flash Attention

Flash Attention computes attention without materializing the full attention matrix. For long sequences (2K+ tokens), it cuts memory by up to 80%.

pip install flash-attn --no-build-isolation
model = AutoModel.from_pretrained(
    "model-name",
    attn_implementation="flash_attention_2",
    torch_dtype=torch.float16,
)

Only works on Ampere+ GPUs (A100, A10, RTX 3090+). T4 doesn't support it.

7. Quantize to INT8

INT8 quantization reduces memory by 4x (32 bits → 8 bits per weight):

from transformers import BitsAndBytesConfig

quant_config = BitsAndBytesConfig(
    load_in_8bit=True,
    llm_int8_threshold=6.0,
)

model = AutoModel.from_pretrained(
    "model-name",
    quantization_config=quant_config,
)

Memory: 14GB → 3.5GB for a 7B model.

Tradeoff: INT8 inference is slower than FP16 on most GPUs. The memory savings let you run larger models on smaller GPUs, but per-token latency increases. For a 7B model on T4: FP16 = 15 tokens/sec, INT8 = 8 tokens/sec. Worth it if you're VRAM-constrained, not if you're latency-sensitive.

8. Delete Unused Tensors

PyTorch doesn't garbage-collect GPU memory immediately. Tensors you think are freed are still allocated:

# After each request:
del outputs, hidden_states, logits
torch.cuda.empty_cache()

empty_cache() releases cached memory back to the GPU. Without it, after 100 requests you might hit OOM even though each request uses only 5GB.

Real-World Impact

For a 7B parameter model on a T4 (16GB):

OptimizationVRAM UsageTokens/sec
Baseline (FP32, with gradients)OOM (14GB + overhead)
+ no_grad()12.5 GB12
+ FP167.2 GB22
+ INT8 (4-bit)3.8 GB8
+ Flash Attention (2K seq)3.2 GB18
+ All combined3.2 GB18

From OOM to 3.2GB with 18 tokens/sec. On the same hardware.

When Not to Optimize

If your model fits comfortably in VRAM with 30%+ headroom, don't optimize. The complexity isn't worth it. Only optimize when you're within 80% of VRAM capacity or when you need to increase batch size.

Roptal detects your model's VRAM requirements during repo analysis and applies appropriate optimizations — FP16, no_grad, Flash Attention — based on the target GPU.