Skip to content

Instantly share code, notes, and snippets.

@bigsnarfdude
Last active August 4, 2026 19:36
Show Gist options
  • Select an option

  • Save bigsnarfdude/2b05be835f983fc46c643ea4d80ea634 to your computer and use it in GitHub Desktop.

Select an option

Save bigsnarfdude/2b05be835f983fc46c643ea4d80ea634 to your computer and use it in GitHub Desktop.
Getting Unsloth DeepSeek-V4-Flash-0731 GGUF running losslessly on a single rented A100 and 2xH100

162 billion parameters (8 bit quant) on one 40 GB card and tested on 2xH100 (2x80gb cards)

@bigsnarfdude

bigsnarfdude commented Jul 31, 2026

Copy link
Copy Markdown
Author

162 billion parameters (8 bit quant) on one 40 GB card

DeepSeek-V4-Flash-0731 running losslessly on a single rented A100 — what it actually costs, and what it would take to host at home.

31 July 2026 · Lambda A100-SXM4-40GB · llama.cpp b1-876a432

How to read the numbers. Everything is tagged. measured was run on this box today. estimated is projected from bandwidth arithmetic and could be wrong by a factor of two. unverified is pricing or spec I haven't checked against current listings.

TL;DR

  • It runs, losslessly. The full 162 GB Q8 quant serves on a 40 GB GPU at 15–17 tokens/sec, because only 13B of the 284B parameters fire per token.
  • The GPU is almost beside the point. System RAM is the gate — 216 GB here, of which ~150 GB holds experts the GPU never touches.
  • Throughput is nearly flat from 0 to 32k context (17.2 → 15.3 tok/s). Predictably slow beats fast-then-collapsing for agentic work.
  • Codex drove it end-to-end, and when it hit a missing dependency it reported the failure honestly instead of faking a pass.
  • For a home build, the purchase this implies is memory bandwidth, not a bigger GPU. 40 GB → 48 GB of VRAM buys roughly 3%.

The setup

DeepSeek-V4-Flash is a mixture-of-experts model: 284B parameters total, but each token routes through 6 of its 256 experts plus one shared expert — about 13B parameters of actual work.

Think of it as a reference library. The building holds a quarter-million books, but answering any single question means pulling six off the shelf. You don't need the whole library on the desk — you need the desk to be fast, and the stacks merely need to be reachable.

That maps onto hardware almost exactly. The "desk" — attention, the shared expert, the KV cache — fits in 40 GB of VRAM. The "stacks" — 96% of the weights — sit in system RAM, and the CPU fetches six per token. The question was whether that fetch is fast enough to be usable.

What we did

1 · Verify the repo arch, real file sizes
<rect x="235" y="20" width="190" height="56" rx="5" fill="#fff" stroke="#2c5aa0" stroke-width="1.5"/>
<text x="330" y="42" text-anchor="middle" fill="#1a1a1a" font-weight="600">2 · Check the box</text>
<text x="330" y="60" text-anchor="middle" fill="#666" font-size="11">216 GB RAM, 472 GB disk</text>

<rect x="460" y="20" width="190" height="56" rx="5" fill="#fff" stroke="#2c5aa0" stroke-width="1.5"/>
<text x="555" y="42" text-anchor="middle" fill="#1a1a1a" font-weight="600">3 · Pull 162 GB</text>
<text x="555" y="60" text-anchor="middle" fill="#666" font-size="11">~10 min @ 260 MB/s</text>

<line x1="202" y1="48" x2="231" y2="48" stroke="#2c5aa0" stroke-width="1.5" marker-end="url(#ar)"/>
<line x1="427" y1="48" x2="456" y2="48" stroke="#2c5aa0" stroke-width="1.5" marker-end="url(#ar)"/>
<line x1="555" y1="78" x2="555" y2="136" stroke="#2c5aa0" stroke-width="1.5" marker-end="url(#ar)"/>

<rect x="460" y="140" width="190" height="56" rx="5" fill="#fff" stroke="#2c5aa0" stroke-width="1.5"/>
<text x="555" y="162" text-anchor="middle" fill="#1a1a1a" font-weight="600">4 · Build llama.cpp</text>
<text x="555" y="180" text-anchor="middle" fill="#666" font-size="11">CUDA, sm_80</text>

<rect x="235" y="140" width="190" height="56" rx="5" fill="#fff" stroke="#2c5aa0" stroke-width="1.5"/>
<text x="330" y="162" text-anchor="middle" fill="#1a1a1a" font-weight="600">5 · Split the model</text>
<text x="330" y="180" text-anchor="middle" fill="#666" font-size="11">experts → RAM, rest → GPU</text>

<rect x="10"  y="140" width="190" height="56" rx="5" fill="#f2f9f3" stroke="#2a9d3f" stroke-width="1.5"/>
<text x="105" y="162" text-anchor="middle" fill="#1a1a1a" font-weight="600">6 · Agentic test</text>
<text x="105" y="180" text-anchor="middle" fill="#666" font-size="11">Codex writes + runs code</text>

<line x1="456" y1="168" x2="429" y2="168" stroke="#2c5aa0" stroke-width="1.5" marker-end="url(#ar)"/>
<line x1="231" y1="168" x2="204" y2="168" stroke="#2c5aa0" stroke-width="1.5" marker-end="url(#ar)"/>

Under an hour of wall clock, most of it download and CUDA build running in parallel. The key move is step 5 — splitting by tensor type rather than by layer:

llama-server -m DeepSeek-V4-Flash-0731-UD-Q8_K_XL-00001-of-00005.gguf \ -ngl 99 -ncmoe 37 # all to GPU, then claw 37 layers of experts back -c 32768 -t 28 --jinja \ --temp 1.0 --top-p 1.0 --min-p 0.0 \ --host 127.0.0.1 --port 8080

-ngl 99 sends every layer to the GPU; -ncmoe 37 pulls the expert tensors of the first 37 layers back to CPU RAM. The blunt layer-level dial would drag all 256 experts along with every layer it moved — the tensor-level split is what makes this work at all.

What we found

confirmed needs validation hit a wall open caveat

Lossless Q8 fits, with room to spare measured 161.9 GB across 5 shards, loading in 20 seconds via mmap. Peak: 35.4 GB of 40 GB VRAM, ~158 GB of 216 GB RAM. llama.cpp reports 284.33B params, MXFP4 MoE.

Expert placement: +12% prompt, +8% generation measured

-ncmoeExpert layers on GPUpp512tg128
43078.9716.30
40382.2416.99
37688.4017.67
349failed to load — out of VRAM

Each expert layer is ~3.6 GB, so the cliff at 34 is exactly where you'd predict. -ncmoe 37 survives at 32k context with 4.5 GB of VRAM headroom.

Throughput barely moves with context measured

Context depthtg128 (tok/s)vs. empty
016.68 ± 0.21
4,09616.87 ± 0.72+1%
16,38417.15 ± 0.06+3%
32,76815.28 ± 0.03−8%

This is the most useful result here. Generation is bottlenecked on streaming expert weights out of CPU RAM — a constant cost per token regardless of context length. Attention runs on the GPU and is a small enough share that growing the KV cache barely registers, helped by this model's MLA plus sparse attention (index_topk 512, sliding_window 128), which doesn't scale linearly with context.

A flat 15–17 is far more usable for agentic work than a 25 that decays to 6.

Codex ran a full agentic loop measured Wrote fizzbuzz.py and a 4-function pytest file, executed shell commands in a sandbox, finished in 18,742 tokens. Code verified correct against a fresh interpreter.
It failed honestly measured pytest wasn't installed and the sandbox blocked network, so it couldn't install it. Rather than claiming a pass, it said so, replicated the assertions in plain Python to verify correctness anyway, and explained how to run the real suite later.
Prompt processing is the remaining constraint measured 88 tok/s means a genuinely new 10,000-token context costs about two minutes before the first output token. llama.cpp's prompt cache absorbs this across turns in a session, but an agent that rewrites its context each step won't benefit much.
Codex 0.146 dropped wire_api = "chat" It now speaks only the Responses API. This nearly forced a downgrade — but llama.cpp's HEAD, committed the same day, exposes /v1/responses. One config line fixed it. It also exposes /v1/messages, the Anthropic wire format.
No cheap quant fallback exists on this model Q4_K_XL is 155 GB against Q8's 162 GB. The routed experts are quantization-aware-trained and ship natively in MXFP4, so 4-bit conversion touches only the other 4% of weights. The 3-bit quant (103 GB) is the first real step down — and it wasn't uploaded yet for 0731.

Two traps. Shard 00001-of-00005 is only 5.25 MB — a split header, not weights. Downloading just that file, which the obvious single-file URL invites, gets you nothing loadable.

And the first prompt-processing measurement was meaningless: llama.cpp's prompt cache had already absorbed the test prompt, so it reported 4 tokens instead of 1,800. All throughput figures here come from llama-bench, which sidesteps that.

Hosting this at home

The result generalizes into one uncomfortable conclusion: the thing worth buying is memory bandwidth and capacity, not a bigger GPU. Our card held 35 GB of a ~190 GB working set. Extrapolating the -ncmoe sweep, going 40 GB → 48 GB of VRAM buys about two more expert layers, or ~3%. That is a poor use of $4,000.

Where the 17 tok/s actually comes from

  1. Experts are 96% of the file (~155 GB); everything else is ~7 GB.
  2. Per token, 6 of 256 experts fire across ~41 MoE layers ≈ 6.2B params ≈ 3.3 GB at MXFP4.
  3. With -ncmoe 37, the CPU supplies 37/43 of that ≈ 2.8 GB per token.
  4. 17 tok/s × 2.8 GB ≈ 48 GB/s effective — against ~204 GB/s theoretical for 8-channel DDR4-3200.

We're at roughly a quarter of peak bandwidth. That gap is the interesting part: it suggests the bottleneck isn't raw DRAM speed but the hybrid split itself — 43 layers of CPU↔GPU round trips, every single token. A unified-memory machine has no split and pays none of that overhead, which is a structural advantage on top of whatever bandwidth it has.

Platform options

PlatformMax RAMBandwidthFits Q8?Est. tok/s
Consumer AM5 / LGA1851
2-channel DDR5
192 GB~80–90 GB/smarginal5–8 est
Used EPYC Rome/Milan
8-channel DDR4 — same class as this box
512 GB+~200 GB/syes~15–17 meas
Threadripper Pro / Xeon-W
8-channel DDR5
512 GB+~330 GB/syes25–40 est
Mac Studio M2 Ultra
unified memory, no split
192 GB~800 GB/stight40–60 est
Rent (A100 40 GB + 216 GB host)216 GByes15–17 meas

"Max out the RAM" on a consumer board is the trap. You get capacity without bandwidth — a machine that loads the model and then generates at a third of what we measured. Worse, most consumer platforms cap at 192 GB (4×48 GB), which sits under unsloth's 169 GB recommendation once the OS takes its cut, and populating all four DDR5 slots usually forces the memory clock down — costing bandwidth exactly when you added capacity.

The 192 GB question

A used M2 Ultra Studio at ~$4k is the most interesting option on that table, and its constraint is knowable now without benchmarking anything — it's arithmetic.

192 GB total, minus ~12–16 GB for macOS, minus KV cache, against a 162 GB model. It fits. It does not fit comfortably, and you won't run large contexts. You'd also need to raise Metal's wired-memory limit — the default caps GPU allocation near 75% (~144 GB), which won't even load it:

sudo sysctl iogpu.wired_limit_mb=184320 # 180 GB, leaving macOS ~12 GB

So the real question isn't "192 GB or not" — it's which quant you're buying for:

TargetSizeOn 192 GB
Q8_K_XL — lossless162 GBWrong size. Want 256 GB+.
Q4_K_XL155 GBSaves 7 GB. Pointless.
IQ3_XXS — 3-bit103 GBRoomy, fast, real context headroom. Not lossless; not yet uploaded for 0731.

Rent or buy

At roughly $1–2/hr for this class of instance, $4k is something like 3,000 hours — about 2.5 years at 5 hrs/day. Renting wins on intermittent use and skips the platform question entirely. Buying wins on always-on availability, silence, no metered clock, and data never leaving the house.

The argument against the M2 Ultra specifically is age: it's a 2023 part, and whether $4k used is fair in mid-2026 depends on the current lineup. If a newer Ultra with more unified memory is close in price, the 192 GB ceiling is the thing you'd regret — this model already needs 162 GB, and the next one will not be smaller.

What's next

Test --no-mmap The loader explicitly warns that CPU tensor overrides plus mmap leaves performance on the table. With 216 GB there's room to load all 162 GB as anonymous memory. Given we're at ~24% of theoretical bandwidth, this is the cheapest thing left to try.
Try the speculative decoding module This checkpoint ships an MTP head (num_nextn_predict_layers: 1), and the build exposes --spec-draft-n-cpu-moe. Speculative decoding helps most exactly where we're bottlenecked — memory-bandwidth-bound generation. Potentially a much bigger win than the 8% from expert placement.
Write the models.json catalog Codex warned that model metadata was missing and fell back to defaults, and logged reasoning effort: none — it isn't passing effort through, so the template default governs. Worth wiring up before judging agentic quality.
Find the real context ceiling We ran 32k of a claimed 1M. Think Max reportedly wants 384k+, which almost certainly doesn't fit alongside 162 GB of weights here. Measuring actual KV bytes-per-token would settle it — the attention scheme is unusual enough that guessing is pointless.

Sources

  • Weights: unsloth/DeepSeek-V4-Flash-0731-GGUF, UD-Q8_K_XL, 5 shards / 161.9 GB
  • Config: deepseek-ai/DeepSeek-V4-Flash-0731 — 43 layers, 256 routed + 1 shared expert, 6 active/token
  • Runtime: llama.cpp 876a432 (31 Jul 2026), CUDA 12.8, sm_80
  • Host: Lambda A100-SXM4-40GB · 216 GB RAM · 30 vCPU EPYC 7J13 · Ubuntu 22.04
  • Agent: Codex CLI 0.146.0, wire_api = "responses" against 127.0.0.1:8080

Measured 31 July 2026. Throughput from llama-bench -r 2; depth series with -d 0,4096,16384,32768. Bandwidth figures for platforms other than this host are vendor peak specs, not measurements — treat the derived tok/s as order-of-magnitude only.

@bigsnarfdude

Copy link
Copy Markdown
Author

The offload cliff

DeepSeek-V4-Flash-0731 measured across three tiers of hardware — and what a 284B model actually knows about upgrading our Rails app.

4 August 2026 · 2× H100 80GB SXM · single A10 23GB · llama.cpp 071327508 · unsloth UD-Q8_K_XL, 161,869,615,520 bytes

Three findings, in order of how much they'd change your buying decision:

1. Offloading 7% of expert layers to CPU costs 75% of prefill speed. Generation barely notices. They are two different bottlenecks on one knob.

2. A second GPU in llama.cpp buys capacity, not speed. At matched offload, two H100s perform identically to one — very slightly worse.

3. On a real upgrade task with a known answer key, it scores 4.75 / 7 — and the two it reliably misses are exactly the two that break the boot.

1 · Prefill falls off a cliff. Generation slides.

Everything below is the same model file on the same box. Only -ncmoe changes — how many of the 43 expert layers live on CPU instead of GPU.

PREFILL (pp512, tok/s)GENERATION (tg128, tok/s)ncmoe 014453357623291711213641 (A10)4705735164794412414113Same knob. Two completely different curves.Generation waits on memory bandwidth — it degrades roughly linearly.Prefill waits on compute — the moment the CPU does expert matmuls, it collapses.Three of 43 layers offloaded (7%) → prefill drops 75%. Generation drops 11%.measured llama-bench, -r 2, 2× H100 except the grey A10 rows. Bars normalized to each metric's own maximum.

This is why the common advice — buy memory channels, buy RAM bandwidth — only ever fixed half the problem. It's correct for generation and irrelevant for prefill. With 41 of 43 layers on CPU, the CPU is doing the prefill matmuls, and no amount of DDR5 changes that.

The practical version

Coding agents open with a large system-and-tools preamble. Reasonix's is 13,215 tokens. Codex's is comparable.

A10   (ncmoe 41)   13,215 tok ÷  46.6 tok/s  =  284 seconds before the first byte
2×H100 (ncmoe 0)   13,215 tok ÷ 1445  tok/s  =    9 seconds

At 284 seconds Codex gives up mid-request — the server log reads srv stop: cancel task. That's not a misconfiguration. It's the cliff.

2 · The second GPU is running a relay race

Two H100s at ~50% utilization each, drawing 290 W of a 700 W cap, looked wrong. llama.cpp's default --split-mode layer is a pipeline split: GPU0 runs its layers, hands off, GPU1 runs the rest. On single-stream decode they take turns.

The clean test is to hold offload constant and vary only card count:

config | prefill | generation -- | -- | -- 1× H100, -ncmoe 25 | 83.67 | 33.44 2× H100, -ncmoe 25 | 83.70 | 32.47 2× H100, -ncmoe 0 | 1445.94 | 56.97

Power never exceeded 362 W of a 700 W cap per card, idling at 117 W. Generation waits on HBM, not on math — the 700 W envelope is there for prefill.

Agentic work leaves the GPU 40–50% idle regardless; it's latency-bound and bursty. Batch is the shape this hardware wants: eight concurrent slots took aggregate throughput from 57 to 139.6 tok/s. Which also describes the honest use case — not an assistant you sit in front of, but an oracle-scored batch job you read in the morning.

Corrections

Predictions I put on record before measuring, all wrong in the model's favour:

  • Predicted 4/7 on the Gemfile eval, and specifically that deep_cloneable would be missed — it hits 95% with correct reasoning.
  • Called 2× H100 impractical because 162 GB "won't fit in 160 GB" — that was a GB/GiB error. The model is 150.75 GiB and fits with 8.5 GiB spare.
  • Estimated prefill at 300–800 tok/s. Measured 1,445.
  • Blamed Reasonix for the A10's zero cache hits. The H100 run shows the same retry event and still cached 40,200 tokens. unexplained

Open

  • untested --no-mmap — the loader explicitly warns that CPU tensor overrides plus mmap leaves performance on the table.
  • untested Speculative decoding via the MTP head. The 2× DGX Spark recipe measures 60.2% draft acceptance and 4.01 tokens/step — worth ~1.7× decode. Needs vLLM.
  • unexplained Why the A10 cached nothing. Two variables differed; not isolated.
  • the good one Does tool access rescue the knowledge gap? The 20-run eval was single-shot with no tools — it couldn't check whether liquid 3 runs on Ruby 3.2. Given bundle and the repo it might discover the breakage empirically, which would flip "doesn't know" into "doesn't know but can find out." Completely different conclusion about usability.

Raw data, logs and all 20 eval outputs: ~/Downloads/deepseek/results-h100-2026-08-04/ and results-a10-2026-08-04/. Scoring was regex against the documented answer key and grep against a known-correct fix — no model judged another model's output.

@bigsnarfdude

Copy link
Copy Markdown
Author

  ┌───────────────────────────────────────────────────────────┬──────────────────────────────────────┐
  │                         Decision                          │              Difficulty              │
  ├───────────────────────────────────────────────────────────┼──────────────────────────────────────┤
  │ rails 6.1.7.10 → 7.0.10 → 7.1.6, stepwise                 │ easy                     │
  ├───────────────────────────────────────────────────────────┼──────────────────────────────────────┤
  │ ruby 2.7.8 → 3.2.8                                        │ easy                     │
  ├───────────────────────────────────────────────────────────┼──────────────────────────────────────┤
  │ puma ~> 6.0 — Puma 5 refuses Rack 3                       │ moderate, well-known                 │
  ├───────────────────────────────────────────────────────────┼──────────────────────────────────────┤
  │ concurrent-ruby unpinned — pin only needed below 7.1      │ moderate (the Gemfile comment hints) │
  ├───────────────────────────────────────────────────────────┼──────────────────────────────────────┤
  │ deep_cloneable ~> 3.2 — 3.1 caps ActiveRecord < 7         │ hard, gemspec trivia                 │
  ├───────────────────────────────────────────────────────────┼──────────────────────────────────────┤
  │ liquid 3 → 4 — liquid 3 taint-checks, dead on Ruby 3.2    │ hard, obscure                     │
  ├───────────────────────────────────────────────────────────┼──────────────────────────────────────┤
  │ tinymce-rails 5.10.2 → 5.10.9 — 5.10.2 calls File.exists? │ very hard, deeptrivia               │
  └───────────────────────────────────────────────────────────┴──────────────────────────────────────┘

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment