llm.supples.org

Fitting a 35B model into 16 GB, then checking it gets things right

Four open models, tuned for a local Goose agent on one Radeon RX 9070 XT and then tested on real agent work. Qwen3.6 35B A3B won. It passed all 12 hard runs, and putting each part of it on the right side of the memory bus made long chats 6.5 times faster.

5.5tok/s
generating in a 79K-token chat
Attention
Experts
layer 1layer 40
On the GPU (16 GB VRAM)In system RAM, run by the CPU

The setup

A desktop PC running an AI agent entirely offline.

CPU and RAMAMD Ryzen 7 7700X, 30 GB
GPURadeon RX 9070 XT, 16 GB VRAM. The same card drives the monitors, so filling its VRAM takes the desktop down.
OSBluefin Linux (Fedora Atomic), GNOME on Wayland
Model serverLM Studio 0.4.26, llama.cpp runtime 2.64.0 (Vulkan)
AgentGoose 1.56, with Playwright MCP for browser control
Jobs it needs to doCode, web design, documents, web research, personal finance, GitHub and Cloudflare via MCP, computer control

Web search, GitHub, Cloudflare and browser control come from Goose's extensions and MCP servers, not from the model. The model's job is to call those tools reliably and get the details right. It also needs vision for screen-based computer control.

Starting point: three stacked failures

Goose answered every message with null result from llama cpp. Each problem was hidden behind the one before it.

SymptomCauseFix
missing tensor 'blk.64.ssm_conv1d.weight'Goose's built-in llama.cpp couldn't read a newer hybrid GGUFRun models in LM Studio
Config said LM Studio, chat still used the built-in backendA Goose session keeps the provider it was created withStart a new chat
request (9501 tokens) exceeds the available context size (8192 tokens)Goose sends its system prompt and every tool definition before you typeLoad with a larger context (now 128K)

Read the logs before changing settings. Each error message named its own cause.

Why mixture-of-experts models fit this card

An MoE model stores all its weights but only computes with a few of them per token.

Dense (Qwen3.8 27B)MoE (Qwen3.6 35B A3B)
Weights stored27B35B
Weights used per tokenall 27Babout 3B: a router picks a few experts
File at 4-bit16.8 GB22.1 GB
Past 16 GB of VRAMevery token waits on system RAM, so it slows sharplyrarely used experts sit in RAM, so it slows a little
Measured generation8.9 tok/s29.2 tok/s untuned, 39.3 tuned

Per gigabyte, an MoE model is somewhat less capable than a dense one. But on a card the dense model doesn't fit, MoE wins clearly. The dense model was dropped.

Speed tuning

Most of the gain came from where the model's pieces sit in memory, not from exotic settings.

Generation speed in a 79K-token chat, tok/s
LM Studio automatic placementTuned, in each model's colour
128K context, 1 parallel slot, mmap off. GLM-4.7-Flash's automatic placement overflowed VRAM, so it has no "before".

The big one: keep every layer on the GPU

Raising the context to 128K looked free on a 10K-token prompt. A 79K-token prompt showed the real cost: Qwen3.6's generation fell from 29 to 5.5 tok/s. The real launch arguments explained why. LM Studio's automatic placement put 22 whole layers on the GPU and 18 on the CPU, attention included. So the CPU was doing attention over the entire chat.

The fix is the standard one for MoE models: every layer on the GPU, with only some layers' experts kept in system RAM (llama.cpp's --n-cpu-moe, LM Studio's "MoE experts on CPU"). Attention stays on the GPU, and only the cheap, rarely used expert weights wait on RAM.

Every step of every sweep

Each sweep step changed one setting, reloaded the model and timed a ~10K-token prompt and a ~79K-token prompt. The highlighted row is the setting the picker chose: fastest long-chat generation with VRAM and pinned RAM both at or under 14 GB.

Other settings that mattered

Two memory limits, not one

VRAM is the obvious limit. The second is GTT: system RAM the GPU driver pins for the GPU, capped at about 15.6 GB here. Experts kept in RAM are pinned in GTT, so moving more of them off the GPU trades one limit for the other. With 80% of experts in RAM, GTT filled, the load failed and the kernel logged 2,832 Not enough memory errors in one burst.

Vulkan beat ROCm on every model

LM Studio's ROCm runtime was slower and used about 5 GB more VRAM than estimated, so far less of each model fit:

ModelBest ROCm offloadROCm read / genVulkan read / gen
Gemma 4 26B A4B50%933 / 25.02,776 / 36.3
Qwen3.6 35B A3B20%476 / 21.21,118 / 29.2
Qwen3.8 27Bnone fit–629 / 8.9
tok/s on a ~10K prompt, 32K context, before the 128K tuning.

Every ROCm run also logged thousands of init_user_pages: Failed to get user pages: -1 errors. ROCm was asking for write access to the read-only memory-mapped model file. Turning mmap off removed the errors but didn't change the speed. The server log also showed missing hipBLASLt kernels for this GPU (gfx1201) in LM Studio's ROCm bundle. That's LM Studio's to fix.

A second look found that most of this gap was LM Studio's packaging, not ROCm. See ROCm, revisited.

GLM-4.7-Flash: a runtime limit

GLM's attention cache was far larger than LM Studio estimated. Its long-prompt reading stuck near 110 tok/s at every placement and batch size, 16× slower than Gemma. That pattern points to the runtime, not the settings: LM Studio's Vulkan build likely lacks a fast path for GLM's attention type. That's inferred from the measurements, not confirmed.

Quality: does it get the job right?

Speed says nothing about accuracy. Each tuned model ran real Goose tasks headless, scored automatically and then checked by hand.

Six easy tasks barely separated the models (Qwen3.6 12/12, Gemma 11/12). So a harder set followed, with traps, conflicting sources and hidden checks. Each model ran each task twice, with at most 40 tool steps and 25 minutes per run.

TaskWhat makes it hard
H1 CodeImplement a spec across three modules; 10 hidden tests the model never sees
H2 ResearchTen facts about three npm packages across several pages, with sources
H3 FinanceBalance transfer vs personal loan: fees, a 0% promo, a grossed-up loan amount, APR vs APY
H4 DocumentA brief from notes, emails and a policy, where a later email changes the date and an owner
H5 WebA pricing page whose monthly/yearly toggle must actually work, plus accessibility and phone layout
H6 AgentFlag the right 4 orders among 40 across four pages, past decoys and confirmation dialogs
Hard-task score, % of available points
2 points for a pass, 1 for a partial, 0 for a fail. Gemma's second H6 run was stopped to clear the queue, so it has 22 points available instead of 24.

What the failures were

Temperature matters

Qwen3.6 runs at its publisher's precise-coding temperature of 0.6, while Gemma ran at Google's general default of 1.0. Rerun at 0.6, Temperature fixed Gemma's coding reliability but not its step efficiency.

The scorers needed checking too

Before any model ran, every scorer was tested against a correct answer and a deliberately wrong one. That caught two wrong expected values in the hidden tests, which would have failed a perfect model. After the runs, three correct answers turned out to be marked down: two briefs that noted a handover in the owner column, and a loan answer that said "cheapest" instead of "cheaper". They were re-scored.

Reading every brief and loan explanation by hand also found slips the scorers passed. Qwen3.6 had two: a brief said someone was "out through mid-November" when the email gave no return date, and one loan breakdown didn't add up despite correct totals. Gemma and Flint had none.

Read the actual answer before trusting either a pass or a fail.

ROCm, revisited

Outside LM Studio, ROCm works. It still loses to Vulkan where it counts, and now we know why.

ROCm is AMD's answer to NVIDIA's CUDA, so it ought to win on AMD hardware. The first round said otherwise, so round two took LM Studio out of the picture: the same model and the same tuned settings, run in llama.cpp's own containers with llama-bench.

What was wrong inside LM Studio

Upstream containers, same model and settings

BackendRead, emptyWrite, emptyRead at 78KWrite at 78KPinned RAM
Vulkan (RADV, Mesa 26.2.5)2,18941.01,15036.512.3 GB
ROCm 7.2.12,23141.11,36726.90.1 GB
ROCm 7.2.1, hipBLASLt off2,76341.71,24525.60.1 GB
ROCm 7.2.1, one hardware queue2,35541.61,34426.30.1 GB
ROCm 10.3 nightly (TheRock, via Lemonade)2,76434.01,62227.60.3 GB
ROCm 7.2.1, hipBLASLt off, f16 cache2,56742.91,51332.80.4 GB
Qwen3.6 35B A3B Q4_K_M, tok/s. "Read" is a 2,048-token prompt, "write" is 128 generated tokens, measured with an empty chat and with 78K tokens already in context. llama.cpp build b11870. All runs clean: no watchdog stops, no kernel GPU errors.

Why ROCm still trails

ROCm matches CUDA's programming interface, not its performance. llama.cpp's ROCm backend is its NVIDIA code, automatically translated and tuned mainly for NVIDIA and AMD's data-centre chips. Consumer RDNA 4 only got official ROCm support this year. Vulkan on Linux, by contrast, rides on Mesa's RADV driver and ACO compiler, which have had years of gaming-driven tuning on exactly these cards.

The two kinds of work favour different strengths. Writing a reply is limited by memory bandwidth, where Vulkan's kernels get closer to the card's 640 GB/s. Reading a prompt is limited by maths, where ROCm's matrix libraries pull ahead. That is exactly the pattern above: ROCm reads 20–40% faster, and Vulkan writes faster in long chats, which is where an agent spends its time.

The one setting that mattered was the cache type. ROCm reads a Q8 cache slowly over long contexts, and switching to f16 took long-chat writing from 25.6 to 32.8 tok/s. The remaining 10% gap is llama.cpp's ROCm attention code, which no setting fixes. The BIOS was checked too: EXPO already runs the RAM at DDR5-6000, Resizable BAR is on, and the B650 board's graphics slot is PCIe 4.0 by design.

Vulkan for running models; ROCm for training them, which is something Vulkan can't do.

Leaving LM Studio

Same engine, fewer layers in between: llama.cpp's own server as a Podman Quadlet.

LM Studio's Vulkan runtime works fine, but it trails upstream llama.cpp by weeks and hides launch settings behind per-model JSON with traps (one missing key silently undid the expert placement). Upstream Vulkan matched it on speed (36.5 vs 35.8 tok/s at 78K), so moving costs nothing.

The replacement is llama-server in router mode, running as a systemd user service through Podman's Quadlet. One server lists several models, each with its own tuned flags in an INI file, loads whichever one Goose asks for by name, keeps only one in memory, and puts it to sleep after 10 idle minutes. Asleep, it hands back its VRAM and pinned RAM (measured: VRAM back to 1.0 GB, pinned RAM to 0.2 GB), so games get the card without anyone remembering to stop anything. A cold load takes about 12 seconds.

Bluefin's own docs point to llmman for this. It runs the same upstream containers, but its build command reads a whole model file into memory, then a second copy, before writing it. Packaging the 21 GB model needed about 42 GB of RAM, and the kernel stopped it at 24.6 GB of the 30 available. The streaming code to fix it already exists in its source, unused.

Same model, two servers, and an Unsloth quant

With the new server in place, the 12 hard-task runs were repeated for the same Qwen3.6 file and for Unsloth's "Dynamic" quant of the same model (UD-Q4_K_XL, a similar size), which promises more quality per gigabyte.

RunPointsMedian time per taskMedian tokens written
LM Studio, Q4_K_M (Oct 24)24/24102 s3,066
llama-server, Q4_K_M24/24154 s4,890
llama-server, Unsloth UD-Q4_K_XL22/24108 s3,566
6 tasks × 2 runs each, same sampling (temperature 0.6, top-k 20, top-p 0.95). Generation speed was the same on both servers (about 40 tok/s); time differences come from how much the model chose to write.

Final setup

Qwen3.6 35B A3B, served by upstream llama.cpp (Vulkan) in a Podman Quadlet: 128K context, every layer on the GPU, 25 layers' experts in system RAM.

WhereSettingValueWhy
ServerEngineghcr.io/ggml-org/llama.cpp:server-vulkan, pinned buildSame speed as LM Studio, newer builds, exact flags
ServerModerouter, one model at a time, sleep after 600 s idleLoads on demand; frees the GPU for games
ServerGPU passed incard1 and renderD128 onlyKeeps it off the Ryzen's built-in graphics
Modeln-gpu-layers / n-cpu-moe99 / 25Attention on the GPU, rarely used experts in RAM; 6.5× faster long chats
Modelload-modenone (no mmap)61–99% faster prompt reading
Modelctx-size / parallel131072 / 1Long chats, one user
ModelK and V cacheq8_01.2 GB less VRAM, no speed cost on Vulkan
Modelubatch-size2048About 2× faster prompt reading
ModelSamplingtemp 0.6, top-k 20, top-p 0.95, min-p 0Qwen's precise-coding preset
GooseProvidercustom llama_server provider → 127.0.0.1:8080OpenAI-compatible; 131072-token limit per model
GooseExtensionsDeveloper, Todo, Memory, Analyze, Computer Controller, Extension Manager, PlaywrightFewer tools to choose from
DesktopControlllm status | unload | off | on | logsllm off before gaming guarantees nothing loads mid-game
GNOMEAutomatic suspendoffSuspending with a model loaded crashed the desktop

The model's entry in ~/.config/llama/models.ini (shared settings like the cache type sit in a [*] section):

[qwen3.6-35b-a3b]
model       = /models/lmstudio/lmstudio-community/Qwen3.6-35B-A3B-GGUF/Qwen3.6-35B-A3B-Q4_K_M.gguf
mmproj      = /models/lmstudio/lmstudio-community/Qwen3.6-35B-A3B-GGUF/mmproj-Qwen3.6-35B-A3B-BF16.gguf
n-cpu-moe   = 25
ubatch-size = 2048
temp        = 0.6
top-k       = 20
top-p       = 0.95
min-p       = 0

The other models

Lessons

Things that cost time, so you don't have to repeat them.

  1. Read the server's real launch arguments. /proc/<pid>/cmdline showed settings the UI implied were applied but weren't.
  2. Benchmark long prompts. 128K context looked free at 10K tokens and cost 80% of generation speed at 79K.
  3. Never run two model servers on a GPU that drives your screen. A leftover ROCm server kept 13.4 GB and the next load crashed GNOME.
  4. Watch GTT as well as VRAM. Moving experts to RAM trades one limit for the other.
  5. Block suspend during long GPU jobs. "Performance" power mode doesn't stop the idle timer. Use systemd-inhibit --what=sleep:idle.
  6. Journal every step. A START with no END marks exactly where a crash happened.
  7. Use the publisher's sampling settings. Gemma at temperature 1.0 made a logic bug it didn't make at 0.6.
  8. Test the tests. Scorers had wrong expected values and false negatives; both were caught only by checking them.
  9. On a desktop Ryzen, hide the built-in graphics from ROCm with HIP_VISIBLE_DEVICES=0, pass it /dev/kfd plus all of /dev/dri, and use an f16 cache.
  10. Separate the app from the engine before blaming the engine. "ROCm is slow" was mostly "LM Studio's ROCm package is broken".
  11. Cap memory on anything that touches a 20 GB file. A packaging tool that read the model into RAM took the terminal down with it. systemd-run --user -p MemoryMax=… confines the damage.
  12. Small things: lms chat prints stats to stderr; a 300 KB prompt is over Linux's 128 KB argument limit, so pipe it on stdin; Playwright MCP blocks file://, so serve test pages over local HTTP.

How it was run

A small Python harness, standard library only, running unattended as systemd user services.

The charts and sweep tables are generated from the run journals, and all the raw numbers are in data/results.json. The harness lives in a private repository.

Log

Results so far, newest first.

Next up: more runs per task so small differences become meaningful, an 80-step H6 rerun, and a fresh ROCm check when llama.cpp's RDNA 4 attention code improves.

Sources