</>CodeWithKarani

4-Bit Quantization Doesn't Mean 4x Less VRAM: What Eats the Rest

Karani GeoffreyKarani Geoffrey5 min read

The maths seemed airtight. A 7B model in 4-bit is about 4GB of weights. You have a 12GB GPU. Three times the headroom. You start the server and it dies loading, or it loads and then falls over the first time two requests arrive at once:

Running out of memory loading 7B AWQ quantized models with 12GB vram
torch.OutOfMemoryError: CUDA out of memory.

Quantization shrinks the weights. It does not shrink everything else. The weights are only one of four things competing for your VRAM, and for a serving workload they are frequently not even the largest. Sizing a GPU off the quantized file size is the single most common way people end up with a model that "should fit" and does not.

Total VRAM is not just weights. Budget for four things: quantized weights + KV cache + activations + framework/CUDA overhead. On a 12GB card a 4-bit 7B model leaves only a few GB after weights and roughly 1GB of CUDA context, and the KV cache grows with your max_model_len and how many requests run at once. To make it fit, cap the context length and concurrency, not just the weights.

  • Lower --max-model-len (KV cache scales linearly with it).
  • Set --gpu-memory-utilization deliberately and test-load at your real target length and concurrency.
  • Never size a GPU tier from the weight file alone.

Where the other 8GB goes

Think of GPU memory during inference as four buckets, filled in this order:

  1. Quantized weights. The one number people quote. A 4-bit 7B model is roughly 4 to 5GB once you include the small unquantized pieces (embeddings, some norms, the scales and zero-points the quantization format keeps).
  2. KV cache. Every token in every active sequence stores a key and value vector for every layer. This grows linearly with sequence length and with the number of concurrent sequences. At a long context and any real concurrency it can dwarf the weights. This is the bucket people forget entirely.
  3. Activations. The transient tensors computed during each forward pass. Scales with batch size and hidden dimension.
  4. Framework and CUDA overhead. The CUDA context itself, cuBLAS/cuDNN workspaces, memory fragmentation, and the serving runtime's own buffers. On a single GPU this is commonly around 1GB before you have loaded a single weight.

Quantization only attacks bucket one. Buckets two through four are computed in the compute dtype (typically fp16/bf16) regardless of how the weights are stored, so a 4-bit model and a 16-bit model with the same context and concurrency carry a similar KV cache and activation cost. That is why "4-bit" does not translate to "one quarter of the VRAM."

12GB 0 What you planned for weights ~4.5GB "plenty of room left" What actually happens CUDA overhead ~1GB weights ~4.5GB activations KV cache grows with context x concurrency
The bucket that overflows the card is usually the KV cache, not the weights you sized around.

A worked sizing example

Take a 7B model, 4-bit AWQ, on a 12GB card, served with vLLM. Rough budget:

BucketApprox VRAMScales with
CUDA context + framework~1GBfixed per GPU
Quantized weights (4-bit 7B)~4.5GBmodel size and quant format
Activations~0.5 to 1GBbatch size, hidden dim
KV cachewhatever is leftmax_model_len x concurrent seqs

That leaves on the order of 5 to 6GB for the KV cache. A single 7B-class model in fp16 KV can spend well over 100MB per thousand tokens of context per sequence. Ask for a long max_model_len and several concurrent requests and you blow through 5GB fast, which is exactly the OOM people hit. vLLM pre-allocates the KV cache region up front based on --gpu-memory-utilization, so if the remaining budget cannot hold even one sequence at your requested length, it refuses to start rather than failing later.

Step 1: Cap the context length to what you actually use

KV cache is linear in max_model_len. If your prompts are 2k tokens, do not reserve for 32k. The single most effective lever:

vllm serve TheModel-AWQ \
  --quantization awq \
  --max-model-len 4096 \
  --gpu-memory-utilization 0.90

Dropping max-model-len from 32768 to 4096 cuts the per-sequence KV cache reservation by 8x. According to the vLLM memory conservation docs, reducing max-model-len is the primary fix when the KV cache does not fit.

Step 2: Set gpu-memory-utilization deliberately

--gpu-memory-utilization tells vLLM what fraction of the card it may claim for weights plus KV cache. Raising it toward 0.95 gives the KV cache more room; setting it too high starves the CUDA context and activations and can itself OOM. On a shared or display-attached GPU, leave more headroom. Do not treat it as a magic dial; it only redistributes the same finite card.

Step 3: Bound concurrency

Total KV cache is per-sequence cost times the number of sequences in flight. If you need a longer context, you must accept fewer simultaneous requests, and vice versa. Cap it explicitly rather than discovering the ceiling in production:

vllm serve TheModel-AWQ --quantization awq \
  --max-model-len 8192 --max-num-seqs 8

Verification: test-load at your real target, not the file size

The only trustworthy check is to start the server at the exact max-model-len and concurrency you intend to run, then watch actual usage:

nvidia-smi --query-gpu=memory.used,memory.total --format=csv -l 2

Then fire concurrent requests at your intended --max-num-seqs and confirm memory settles below total with headroom. If it survives a clean load but OOMs under load, your KV cache budget is too tight for the concurrency you asked for. Weight file size told you none of this; the live load test tells you all of it.

What people get wrong

Sizing the GPU off the weight file. "4-bit 7B is 4GB, 12GB is plenty" ignores three of the four buckets. The weight file is a lower bound on VRAM, never an estimate of it. This is the same trap, one layer up, as assuming quantization gives proportional savings; it reduces the weight footprint and leaves the rest untouched.

Cranking --gpu-memory-utilization to 1.0 to "use the whole card." There is no such thing; the CUDA context and activations live outside vLLM's pool and you will OOM the moment real activations spike. Leave headroom.

Testing with one request and shipping. A single request barely touches the KV cache. The OOM appears at concurrency. Load-test at your real --max-num-seqs before you commit to a GPU tier.

When it is still broken

  1. The server refuses to start even at a short context. Weights plus 1GB overhead may already be near your card. Drop to a smaller model, a more aggressive quant, or a bigger GPU; there is no context length that will save you if the weights alone leave no room for a single sequence.
  2. It loads then OOMs minutes in. That is concurrency, or growing sequences reaching their length limit. Lower --max-num-seqs or --max-model-len.
  3. You need long context and high concurrency on a small card. Those two multiply, and a 12GB card cannot serve both. Either shard across GPUs with tensor parallelism or accept the tradeoff. The economics of picking a tier are the whole subject of my real cost math for self-hosting open-weight models, which is worth reading before you buy or rent.
  4. Fragmentation, not total, is the killer. If free memory looks sufficient but allocation still fails, set PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True to reduce fragmentation before assuming you are truly out.

Frequently asked questions

Why does a 4-bit 7B model run out of VRAM on a 12GB GPU?
Quantization only shrinks the weights (about 4.5GB for a 4-bit 7B). The rest of VRAM goes to the KV cache, activations, and roughly 1GB of CUDA and framework overhead. The KV cache grows with your max_model_len and how many requests run at once, and it is computed in fp16 regardless of weight quantization, so it can easily exceed the remaining space and cause an OOM.
Does quantization reduce KV cache memory?
No, not by default. Weight quantization shrinks the stored weights only. The KV cache, activations and framework buffers are computed in the compute dtype, typically fp16 or bf16, so a 4-bit model and a 16-bit model carry a similar KV cache and activation cost at the same context length and concurrency. There are separate KV-cache quantization options, but standard weight quantization does not touch it.
How do I stop vLLM running out of memory on a small GPU?
Lower --max-model-len, since KV cache scales linearly with context length, and bound concurrency with --max-num-seqs. Set --gpu-memory-utilization deliberately (around 0.90) to leave room for the CUDA context and activations. Then test-load at your real target length and concurrency rather than trusting the weight file size.
How much VRAM do I actually need beyond the weights?
Budget for four buckets: quantized weights, KV cache, activations, and about 1GB of CUDA and framework overhead per GPU. On a 12GB card a 4-bit 7B model leaves roughly 5 to 6GB for the KV cache after weights and overhead, and that must cover your context length multiplied by concurrent sequences. Size from a live load test, never from the weight file alone.
#vLLM#quantization#AWQ#VRAM#KV cache#GPU
Keep reading

Related articles