← Blog

What actually runs on a 24 GB GPU

September 24, 2026

aillmbenchmarkgpu

A 24 GB graphics card is an interesting size in local inference right now. It is one slot, one power cable or none at all, and it sits under a desk without a conversation about the electricity bill. It is also almost exactly the size where the useful open models stop fitting.

So I measured where the edge actually is, on one card, with one set of flags, over several weeks. What follows is what fit, what did not, how fast each one is when the context is empty and when it is nearly full, and which of the numbers turn out not to mean what they look like.

The card

Everything below ran on an NVIDIA RTX PRO 4000 Blackwell SFF Edition, the single slot variant.

Chip GB203GL, compute capability sm_120
Memory 24,467 MiB total, about 23.4 GiB usable
Board power limit 70 W, hard
Form factor Single slot, bus powered, no auxiliary connector
Driver / CUDA 595.91.07 / 13.2
Street price about 2,500 EUR at the time of writing

The 70 W is not a setting. It cannot be raised:

# nvidia-smi -pl 75
Provided power limit 75.00 W is not a valid power limit which should be
between 60.00 W and 70.00 W for GPU 00000000:01:00.0

The card has no auxiliary power connector, so it draws only what the PCIe slot supplies. The full height RTX PRO 4000 Blackwell is a 140 W part; the difference is physical. That matters because it changes which model wins. On a card that is power bound rather than compute bound, the thing that decides throughput is active parameters per token, not total parameter count. A mixture of experts model that routes 3B of its 30B parameters per token does its work inside the envelope and draws about 52 W. A dense 27B pins the meter at 70 W and decodes at roughly half the speed.

Two constraints then, and they pull in different directions: 23.4 GiB of memory sets what can be loaded at all, and 70 W sets how fast it runs once loaded.

What fits

Every model below was served with vLLM at --gpu-memory-utilization 0.90, fp8 KV cache, eager mode, except Bonsai which needs llama.cpp.

Model Origin Released Active Weights
google/gemma-4-12B-it-qat-w4a16-ct 🇺🇸 Google 2026-05-23 12B dense 8.32 GiB
cyankiwi/Ministral-3-14B-Instruct-2512-AWQ-4bit 🇫🇷 Mistral 2025-10-31 14B dense 9.84 GiB
QuantTrio/Qwen3-Coder-30B-A3B-Instruct-AWQ 🇨🇳 Alibaba 2025-07-31 3B of 30B 15.74 GiB
cyankiwi/gemma-4-26B-A4B-it-qat-AWQ-INT4 🇺🇸 Google 2026-03-11 4B of 26B 16.66 GiB
RedHatAI/Qwen3.8-27B-INT4 🇨🇳 Alibaba 2026-08-05 27B dense 16.84 GiB
dbirks/Qwen3.8-27B-W4A16-AutoRound 🇨🇳 Alibaba 2026-08-05 27B dense 16.84 GiB
prism-ml/Ternary-Bonsai-2-27B-gguf 🇨🇳 Alibaba base, ternary by Prism ML 2026-09-16 27B dense 6.70 GiB

Released is the first appearance of the base model on Hugging Face, not of the quantisation, which in several cases followed months later. Origin is who trained the base, not who published the file I ran: several of these quantisations come from independent contributors rather than the original lab.

What does not fit

Model Origin Weights Failure
nvidia/Qwen3.6-35B-A3B-NVFP4 🇨🇳 Alibaba base, NVFP4 by NVIDIA 21.82 GiB No available memory for the cache blocks
cyankiwi/gemma-4-31B-it-qat-AWQ-INT4 🇺🇸 Google 19.18 GiB 1.07 GiB available against 3.21 GiB needed

Both loaded their weights and then had nothing left for a cache. The 31B names its own ceiling in the error: an estimated maximum model length of 2,512 tokens. A model you can only talk to in paragraphs is not deployable.

Qwen3.6 35B A3B is the frustrating one. It routes 3B active parameters per token, exactly the profile that wins on a power bound card, and both the NVFP4 format and the architecture are supported by the runtime. It fails on size alone. Every published quantisation currently sits between 21.8 and 23.7 GiB. A build at or below 18 GiB would be the most promising thing to try on this card.

The working ceiling is about 19 GiB of weights, measured twice rather than estimated once. Above that the model loads and then cannot serve.

Context: how much, and for how many people

Capacity comes free from the vLLM startup log, which reports the KV pool it managed to build. That single number gives the whole context and concurrency curve, because the pool is shared: max context per stream is the pool divided by the number of concurrent requests.

KV cache pool at identical settings. Attention design decides this, not model size.
Model KV pool 1 stream 2 4 8
Gemma 4 12B 332,491 332k 166k 83k 41k
Ternary Bonsai 2 27B ~225,000 225k 112k 56k 28k
Ministral 3 14B 131,936 131k 65k 32k 16k
Gemma 4 26B A4B 102,077 102k 51k 25k 12.7k
Qwen3 Coder 30B A3B 94,064 94k 47k 23k 11.7k
Qwen3.8 27B 76,458 76k 38k 19k 9.5k

Gemma 4 12B holds four times the context of the Qwen3.8 27B while being half its size on disk. That is sliding window attention: most of its layers keep only a short window of keys and values, so the cache does not grow the way full attention forces it to.

Speed when the context is already full

Every throughput number published for a local model is measured on an empty context, and that is not how anyone uses one. An agent re-sends a growing transcript on every turn. So I measured decode speed with the context pre-filled to 15k, 30k, 45k and 60k tokens.

The models do not degrade the same way. They do not even preserve their ranking.

Decode speed against context already in the cache. Two models are flat; one loses more than a third.
Model 0 15k 30k 45k 60k retained
Qwen3 Coder 30B A3B 42.1 42.0 42.1 42.0 41.9 100%
Gemma 4 26B A4B 39.3 39.6 39.4 39.5 39.6 101%
Qwen3.8 27B 22.4 21.8 21.2 20.6 20.0 89%
Gemma 4 12B 41.3 38.1 36.1 35.4 34.8 84%
Ministral 3 14B 43.9 38.8 34.6 31.0 27.7 63%

Ministral 3 14B is the fastest model in the set on an empty context and the second slowest by 60k. If you benchmark at zero depth and deploy into an agent loop, that is the mistake you make.

The two flat models are the two mixture of experts models, and I think the reason is that they are already bound by streaming expert weights rather than by reading the cache, so the growing cache costs them almost nothing on top. The steepest decline belongs to the one dense model in the set with conventional full attention over its whole context. Gemma 4 12B sits between the two, losing some speed but far less than Ministral, which is consistent with its sliding window keeping most of the cache small. That is an inference from five models on one card rather than a proof, but the grouping is clean.

Prefill behaves differently again, and here the spread is much wider:

Time to first token against context depth. This is what a cold agent turn costs.

A cold 60k turn costs six seconds on Gemma 4 26B A4B and twenty two on Qwen3.8 27B before a single token appears. Both engines cache prefixes, so in a steady conversation only the new tokens are prefilled and these numbers apply to the first turn or to a cache miss. I measured that gap by accident: one context that had already been prefilled returned first token in 0.11 s where the identical cold context took 15.77 s, about 145 times faster.

Does it matter what is in the context?

The obvious worry with a depth benchmark is that the filler is fake. If lorem ipsum behaves differently from real traffic, the curve above is decoration.

It does not. At exactly 30,000 tokens:

Filler Gemma 4 12B Gemma 4 26B A4B
Lorem ipsum 35.9 t/s 39.4 t/s
English prose 35.8 39.5
Source code 35.9 39.6
Tool call transcript 35.7 38.8

Under one percent apart on the dense model and about two on the mixture of experts one, which is inside the run to run noise. Decode at depth is bounded by streaming the cache back, and the cache does not care whether it holds Latin, English, Python or escaped JSON.

The control that makes this a real answer rather than a sloppy one is that all four were padded to the same token count, not the same character count. Those four texts tokenise at very different ratios: lorem ipsum is Latin and fragments badly, code is punctuation dense, tool call JSON is full of escaped quotes. Matched by length they would have differed by thousands of tokens, and the benchmark would have measured the tokeniser rather than the engine. Each filler was trimmed against the server's own tokeniser until it landed on the target.

So lorem ipsum is a fair stand in for production traffic, provided you count tokens.

Temperature zero, and why "deterministic" is not quite true

Temperature zero is greedy decoding: no sampling, take the highest scoring token every step. Same weights, same prompt, same winner.

What breaks it is arithmetic rather than sampling. vLLM batches requests continuously, so a request shares kernels with whatever else is in flight. Change the batch and matrix shapes change, a different kernel or split is chosen, and floating point addition is not associative, so the same numbers summed in a different order differ in the last bits. Usually invisible. But when the top two tokens are nearly tied, a last bit decides which wins, and the answers diverge from there.

Five sequential repeats and four concurrent ones per model, counting distinct answers:

Model Type 5 sequential 4 concurrent
Gemma 4 12B dense 1 distinct 1 distinct
Qwen3.8 27B INT4 dense 1 1
Qwen3.8 27B W4A16 dense 1 1
Gemma 4 26B A4B MoE 1 2 distinct
Qwen3 Coder 30B A3B MoE 4 distinct 4 distinct

Every dense model was bit stable across all nine runs. Both mixture of experts models were not, and Qwen3 Coder could not reproduce itself even one request at a time. Five models is a small sample, so treat that as a pointer rather than a law, but routing tokens to experts plainly adds a second way for batch composition to change what each kernel sees.

The two Qwen3.8 quantisations produced the identical output hash, which is worth stating plainly: INT4 and W4A16 of the same model are the same model. There is nothing to gain from sweeping that axis.

Making it actually deterministic

Three knobs, measured on Gemma 4 12B.

Temperature is free. Decode held at 40.7, 40.7, 40.6 and 40.6 tokens per second at temperature 0, 0.2, 0.7 and 1.0. Sampling is not where the time goes.

Determinism survives further up the range than expected. Temperatures 0.2 and 0.7 both returned a single distinct answer across nine runs. Only at 1.0 did a second variant appear, twice in five. On a short prompt the distribution is peaked enough that sampling rarely changes the winner, which is a property of the prompt as much as the setting.

A fixed seed restores determinism even at temperature 1.0. Passing seed: 42 collapsed both 0.7 and 1.0 back to one answer across all nine runs, at no cost.

VLLM_BATCH_INVARIANT=1 is the real fix. It swaps in kernels whose reduction order does not depend on batch shape, so identical input gives bit identical logits whether it runs alone or beside seven others. Gemma 4 26B A4B, which produced two answers under concurrency by default, became reproducible with it on. The cost on Gemma 4 12B was 37.4 tokens per second against 40.7, about eight percent.

It is not universally available. Enabling it on Qwen3 Coder 30B A3B, the least reproducible model in the set and therefore the one that needed it most, crashed the engine before it served a token:

RuntimeError: expected scalar type Half but found BFloat16

Disabling prefix caching changed nothing and cost nothing, because Gemma was already stable.

So: deterministic by default if the model is dense, fixable with a flag if it is a mixture of experts whose dtype the kernels support, and otherwise not fixable in this build at all.

What this does not measure

None of the above says whether a model is any good. Throughput, capacity and reproducibility are the properties you can measure cheaply and compare honestly across models; quality is not, and a number that looks objective usually is not.

The depth curves are single stream. They do not describe behaviour under concurrency, where the pool is divided and the scheduler starts making decisions. Tokens per second should also not be compared too confidently across rows, because these models tokenise differently and a token is not a constant unit of work.

And the whole exercise is five models on one card with one set of flags. The groupings are clean enough to act on and too small to generalise from.

The jokes

Somewhere in the middle of this I asked all of them for their best joke at temperature zero, mostly as a smoke test, and three returned the same scarecrow gag byte for byte across two quantisations and two inference engines. A fourth, trained in France with no shared ancestry, offered it as well. That says less about the models than about what is written on the internet under the heading of "best joke".

It got out of hand, so it lives on its own page: 24 professions and industries, five models, every answer precomputed.

Method

One model served at a time, since 24 GiB holds exactly one of these. Capacity read from the vLLM startup log. Depth measured by pre-filling the context to a target token count verified against the server's own tokeniser, then timing a short generation. Determinism measured by hashing completions and counting distinct values. Harness and raw output are in the repo alongside the earlier sweep this builds on.

Sources

  1. Gemma 4 12B (QAT w4a16) huggingface.co
  2. Ministral 3 14B (AWQ 4bit) huggingface.co
  3. Qwen3 Coder 30B A3B (AWQ) huggingface.co
  4. Gemma 4 26B A4B (QAT AWQ INT4) huggingface.co
  5. Qwen3.8 27B INT4 huggingface.co
  6. Ternary Bonsai 2 27B (GGUF) huggingface.co
  7. Qwen3.6 35B A3B (NVFP4), does not fit huggingface.co