Melih Berat Şanlı

Getting Qwen3.8-27B onto a 16 GB RTX 5080

This is the command I actually run now, on an RTX 5080 with 16 GB:

llama-server -m Qwen3.8-27B-UD-Q3_K_XL.gguf -ngl 99 -fit off \
  -c 90112 -ctk q4_0 -ctv q4_0 -fa on \
  --spec-type draft-mtp --spec-draft-n-max 3 \
  -a "Qwen3.8-27B-UD-Q3_K_XL" --jinja --host 0.0.0.0

It gives me 90,112 tokens of context and about 130 tok/s, with the whole model on the card and nothing spilling into system RAM. The guides I started from said a 16 GB card gets you 8K to 16K of context, or 4-bit weights with part of the model in RAM and the speed that implies. Four of those flags are the difference, and each one cost me an evening to find.

Building it for Blackwell needs an architecture flag

The 5080 is sm_120, and a normal build won’t target it. The one that works:

cmake -S . -B build -G Ninja -DGGML_CUDA=ON -DCMAKE_BUILD_TYPE=Release \
  -DCMAKE_CUDA_COMPILER=/usr/local/cuda-13.3/bin/nvcc \
  -DCMAKE_CUDA_ARCHITECTURES=120a-real
cmake --build build -j $(nproc)

I installed cuda-compiler-13-3 and cuda-libraries-dev-13-3 rather than the whole CUDA meta-package, which pulls in a driver you may not want. The build prints what it found, and this line is the one to check:

CUDA0: NVIDIA GeForce RTX 5080, compute capability 12.0, VMM: yes, VRAM: 15841 MiB

Note the 15841. nvidia-smi says the card has 16303 MiB, and CUDA can only ever see 15841 of it. That missing 462 MiB is reserved by the driver and never shows up as a process. That gap confused me for a while when my arithmetic kept coming out 400-odd MiB optimistic.

The default settings quietly gave me less context than I asked for

This is the one that wasted the most time. llama.cpp has a --fit option, it defaults to on, and what it does is shrink -c until the model fits in memory. Without telling you.

So I’d ask for -c 32768, get no error, and assume I had 32K. I didn’t. Every context number I wrote down that first evening was fiction, and I only caught it because a later measurement made no sense against an earlier one.

Pass -fit off and it will fail loudly instead, which is what you want while you’re finding the limit. Once you know your number you can leave it off permanently.

Most of the context lives in the KV cache, and it can be 4-bit

The KV cache is the model’s memory of everything in the conversation so far, and it grows with every token. By default llama.cpp stores it at 16 bits. -ctk q4_0 -ctv q4_0 stores it at 4 instead.

For this model that’s 18 KiB per token instead of 64, so the same memory holds 3.6 times the conversation. Bisecting the real ceiling with -fit off gives 159,744 tokens at 4-bit against roughly 40,960 at 16-bit.

My worry was what that does to quality. I ran perplexity over 104,694 tokens of llama.cpp’s own C++ source and got +0.037% at 8K context and +0.094% at 32K, against an error bar of about ±0.6% on the measurement itself. So the difference is somewhere between 6 and 16 times smaller than the wobble in the instrument.

One catch: a quantised cache needs -fa on. With flash attention off, llama.cpp won’t even create the context.

Two cache settings look sensible and are 23 times slower

Having found that 4-bit is fine, I tried the ones in between. q5_1 should sit neatly between q8_0 and q4_0, and it does on memory. On speed it falls off a cliff:

cache typeprompt processinggeneration
f162187 tok/s58.5 tok/s
q8_02174 tok/s58.0 tok/s
q4_02171 tok/s57.9 tok/s
q4_1228 tok/s43.9 tok/s
q5_094 tok/s35.2 tok/s
q5_194 tok/s36.0 tok/s

Same context length, same everything, one variable. The reason is that CUDA’s fast attention kernels exist for f16, q8_0 and q4_0 and nothing else, so the others fall back to a slow path. No warning is printed. I thought the first q5_1 run had hung; it was reading a 32,000 token prompt at 94 tok/s, which takes about six minutes.

The free speedup that ships inside the model file

--spec-type draft-mtp turns on multi-token prediction, and for this model there’s nothing to download: the extra head is layer 64 of the GGUF you already have.

Generating is limited by how fast the card can read the weights, not by arithmetic. One token means reading all 12 GiB. If the model can guess the next few tokens cheaply and check them all in that same read, the guesses that turn out right are free. Output is identical either way, and I checked rather than trusting it: MTP off, draft 3 and draft 4 at temperature 0 all produce the same file, same MD5.

It costs about 528 MiB plus 150 MiB per draft token, which is context you don’t get to use. Draft 3 roughly doubles generation speed on code. Draft 4 is faster still on code and reasoning and gets worse on prose, where the guesses miss more often.

The reply arrives in a field a lot of clients ignore

This model reasons before it answers, and the reasoning arrives in reasoning_content, not content. My first benchmark script read only content, saw nothing, and recorded the time-to-first-token as infinite.

It also costs real tokens. Asked to say hi in exactly three words, it spent 175 tokens thinking about it. Sending reasoning_effort: "none" brings that to 4.

default                       175 completion tokens
reasoning_effort: "none"        4
/no_think suffix              115   (does nothing, that's the old Qwen3 syntax)

Moving the desktop off the card bought 292 MiB and stopped the ceiling moving

My monitor was plugged into the 5080, so GNOME and a browser were sitting in the same memory I was trying to fill. That was 292 MiB, and worse, it moved: open a few tabs and the ceiling dropped underneath whatever I had measured a minute earlier.

The 9800X3D has integrated graphics, so I moved the cable to the motherboard’s HDMI port and logged out and back in. The cable alone does nothing, since the desktop stays on whichever GPU it started on.

before:  292 MiB used by the desktop
after:    15 MiB

Small in absolute terms. What matters is that it stopped being a variable.

What it’s like to use

A first message into an empty context comes back in 0.13 seconds and writes at about 135 tok/s. That’s fast enough that it feels like a hosted model.

Long conversations are where it gets interesting. Filling all 90K takes about 90 seconds of prefill the first time, and generation drops to the mid-80s per second at that depth. But llama.cpp caches the prompt, so continuing an existing conversation skips almost all of it: a 36,000 token conversation re-prefills in 0.151 seconds instead of 20.7. Multi-turn is nearly free as long as you’re appending rather than rewriting the front of the context.

Here’s the sort of thing it produces in a single message, no follow-ups. I asked for a playable Flappy Bird clone and got one:

A Flappy Bird clone playing: a yellow bird flapping between green pipes on a blue sky, then hitting the first pipe Recorded by loading the file the model wrote and having a script tap at a fixed rhythm. The game code is untouched. I tried five different tap rates and every one of them died on the first pipe with a score of zero, which turned out to be the model’s fault rather than the script’s: the pipes are 220 px apart on a canvas 440 px wide.

The solar system was better. It got the eight orbital periods right to three decimal places, which I only found out by checking them afterwards:

A solar system animating on a dark canvas: the Sun at the centre with labelled planets moving along their orbits, the inner ones circling several times while the outer ones barely shift Sped up with the page’s own slider, which it also built. Mercury goes round about two and a half times here while Mars moves a third of the way, and that ratio is the part it had to work out rather than look up.

All ten files from this and a second run are at mberatsanli.github.io/qwen38-27b-local, running rather than as source. Four of them are broken.

What I still don’t know

  • Whether 3-bit weights are the right trade. A 4-bit version of this model exists at 13.27 GiB, and it would fit, but the extra gigabyte comes out of the context budget: about 93,000 tokens instead of 159,744. I haven’t measured whether the quality difference is worth that.
  • Whether the model actually uses the far end of a 90K context or only stores it. Being able to allocate the memory and being able to find something in it are different things, and I only tested the first.
  • How any of this behaves on a card that isn’t this one. Every number here is from one 5080.

The number I’d check first

If you’re doing this on different hardware, the useful thing isn’t my flags, it’s how many of your model’s layers actually keep a KV cache. This one has 65 blocks and only 16 of them grow with context, which is why the cache is small enough for any of the above to work. A dense model with the same parameter count would need four times the memory for the same conversation.