Field Manual No.11 · 27 August 2026

# Maximising Qwen3.8-27B on a 5090 Laptop

40.8 to 141.6 tokens per second on a laptop GPU. The build recipe, the sweeps, and the claims we had to correct along the way.

Every number here was measured on one machine running Windows. Follow the steps and you land on the same configuration. Runs that turned out to be invalid are kept and labelled rather than quietly dropped, because knowing which measurements were wrong is most of the value.

RTX 5090 Laptop 24GB sm_120 · driver 610.88 CUDA 12.8.93 llama.cpp 192067b Qwen3.8-27B UD-Q4_K_M 18-entry trap catalogue
§ 01 · Start here

## The configuration

If you read one thing, read this. Every flag is justified by a measurement below.

    llama-server -m Qwen3.8-27B-UD-Q4_K_M.gguf \
      -ngl 999 -fa on --jinja \
      -c 262144 -b 1024 -ub 1024 \
      --cache-type-k q4_0 --cache-type-v q4_0 \
      --spec-type draft-mtp --spec-draft-n-max 5 \
      --reasoning-effort low \
      --parallel 1 --no-mmap \
      --host 127.0.0.1 --port 8080

Why each flag is there — measured contribution, not folklore
Flag | Effect | Evidence
--- | --- | ---
--spec-draft-n-max 5 | +137% | Swept 2–12, sharp peak at 5. Community default of 2 costs 35 tok/s here.
--reasoning-effort low | ~4.2× | Time-to-answer 231 s+ → 55 s on an identical prompt.
--spec-type draft-mtp | enables | MTP heads ship inside the GGUF as `blk.*.nextn.*`; ignored without this.
-fa on | required | Saves ~2.3 GB. Without it 256K will not fit.
--cache-type-k/v q4_0 | required | Lighter and faster than f16. 256K lands at 23,504 of 24,462 MB.
--parallel 1 | protects | Speculative decode is single-stream; the gain vanishes above 1.
-c 262144 | −2% | 81.2 vs 82.9 tok/s at 32K. Nearly free to allocate.
--no-mmap | neutral | Weights live in VRAM; mmap only helps when paging.

Pick effort per task

`off` — 33 s, pure code, best draft acceptance. `low` — 55 s, still reasons, the general default. `medium` — 91 s, real deliberation. `xhigh` — the shipped default, and it did not finish inside 16,384 tokens.

n-max is workload dependent — do not use one value

Structured output wants a much wider draft window than prose or code. Set `--spec-draft-n-max 10` when generating JSON, HTML, XML or any rigid format; `5` for code and prose. Using 5 on JSON costs 21% (141.6 → 112.1 tok/s).

Workload | Set n-max | Decode | vs baseline
--- | --- | --- | ---
JSON / structured | 10 | 141.6 | +250%
HTML / markup | 10 | 118.1 | +192%
Code | 5 | 93.7 | +131%
Prose | 5 | 58.4 | +45%

§ 02 · Rationale

## Decisions & why

Each of these was a fork where the obvious choice was wrong.

Decision | Chosen | Reasoning
--- | --- | ---
CUDA version | 12.8.1 | The 13.x line crashes the Blackwell MMQ kernel. `winget` only offers 13.3, so this must come from NVIDIA's archive manually. 12.8+MMQ benchmarks ~6,500 t/s prefill against ~750 for 13.x+cuBLAS.
Driver | keep 610.88 | The 12.8.1 installer bundles 572.61. Installing it would be a downgrade, so `Display.Driver` is omitted from the subpackage list.
Quantizer | Unsloth | Their GGUFs preserve the `blk.*.nextn.*` MTP tensors. Not every quantizer keeps them, and without them the single largest speed lever is dead.
Quant level | UD-Q4_K_M | 15.32 GiB leaves ~8 GB for KV cache, which is what makes 256K context reachable. Q5/Q6 would trade context headroom for fidelity.
Generator | Ninja | Much faster than MSBuild, but does not auto-discover MSVC — `vcvars64.bat` must be imported first.
Shell | PowerShell | Git Bash cannot load the built binaries at all (UCRT resolution failure).
Bench tool | llama-cli | `llama-bench` has no speculative-decoding flags, so it cannot measure the thing that matters most.

§ 03 · Reproduce it

## Install & build, in order

0

### Verify prerequisites

MSVC is the usual blocker. Check before downloading 3 GB of toolkit.

    winget --version
    git --version
    & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" `
       -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 `
       -property installationPath

If VS Build Tools is absent, install it with the C++ workload — the bare package is not enough:

    winget install --id Microsoft.VisualStudio.2022.BuildTools --exact `
      --override "--quiet --wait --norestart `
                  --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"
    winget install --id Kitware.CMake --exact
    winget install --id Ninja-build.Ninja --exact

Note

If `winget` reports exit 43 (“no applicable upgrade”), the package is already installed — but your `--override` was not applied. Verify `cl.exe` exists rather than trusting the exit code.

1

### Install CUDA 12.8 — not 13.x

    curl.exe -L -o cuda_12.8.1.exe `
      https://developer.download.nvidia.com/compute/cuda/12.8.1/local_installers/cuda_12.8.1_572.61_windows.exe

    # 3.14 GB. Verify the signature before running it.
    (Get-AuthenticodeSignature .\cuda_12.8.1.exe).Status   # -> Valid

    # Explicit subpackages. Display.Driver deliberately ABSENT.
    .\cuda_12.8.1.exe -s -n `
      nvcc_12.8 cudart_12.8 thrust_12.8 `
      cublas_12.8 cublas_dev_12.8 `
      nvrtc_12.8 nvrtc_dev_12.8 `
      cuda_profiler_api_12.8 nvtx_12.8 nvml_dev_12.8 `
      cuobjdump_12.8 nvdisasm_12.8 nvprune_12.8 cuxxfilt_12.8 `
      nvfatbin_12.8 nvjitlink_12.8 `
      visual_studio_integration_12.8

    verify:
    nvcc --version          -> release 12.8, V12.8.93
    nvidia-smi --query-gpu=driver_version --format=csv,noheader   -> 610.88 (unchanged)

Trap 01 · silent total failure

One invalid subpackage name aborts the entire install. Exit `-522190823` = `0xE0E00019`. The NVIDIA log ends normally at 2.101 s with no error text and nothing is installed. There is no `cuda_cccl`; it is `nvrtc_12.8` and `nvml_dev_12.8`, not the `cuda_`-prefixed forms. Get names from NVIDIA's Windows install guide — never guess.

2

### Add CUDA to PATH

The installer does not do this, and nothing tells you.

    $cuda = 'C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.8\bin'
    [Environment]::SetEnvironmentVariable('Path',
      [Environment]::GetEnvironmentVariable('Path','Machine') + ";$cuda", 'Machine')

Trap 02 · no error message

Without this, every llama.cpp binary exits `-1073741515` (`0xC0000135`, `STATUS_DLL_NOT_FOUND`) producing no stdout and no stderr at all. `--help` returns nothing.

3

### Build llama.cpp for sm_120

    git clone --depth 1 https://github.com/ggml-org/llama.cpp
    cd llama.cpp

    # Ninja does not auto-discover MSVC; import vcvars into the session first.
    $vc = 'C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\vcvars64.bat'
    cmd /c "`"$vc`" && set" | ForEach-Object {
      if ($_ -match '^([^=]+)=(.*)$') { Set-Item "env:$($matches[1])" $matches[2] }
    }

    # Delete build/ EVERY time. See Trap 03.
    Remove-Item build -Recurse -Force -ErrorAction SilentlyContinue

    cmake -B build -S . -G Ninja `
      -DCMAKE_BUILD_TYPE=Release `
      -DGGML_CUDA=ON `
      -DCMAKE_CUDA_ARCHITECTURES=120 `
      -DGGML_CUDA_FORCE_CUBLAS=OFF `
      -DGGML_CUDA_F16=ON `
      -DLLAMA_CURL=OFF
    cmake --build build -j 24

    # verify you are on the fast path, not silently on cuBLAS:
    Select-String build\CMakeCache.txt -Pattern 'FORCE_CUBLAS|CUDA_ARCHITECTURES'
      GGML_CUDA_FORCE_CUBLAS:BOOL=OFF          <- must be OFF
      CMAKE_CUDA_ARCHITECTURES:UNINITIALIZED=120

    # confirm the GPU is seen with the right compute capability:
    .\build\bin\llama-bench.exe --list-devices
      Device 0: NVIDIA GeForce RTX 5090 Laptop GPU, compute capability 12.0, VRAM 24462 MiB

Trap 03 · the expensive one

`GGML_CUDA_FORCE_CUBLAS` persists in `CMakeCache.txt` across reconfigures. If it was ever `ON`, it stays `ON`, the MMQ path is disabled, and you lose 5–6× on prefill with no warning whatsoever. Delete `build/` and grep the cache afterwards.

Non-issue

MSVC 14.44 compiles cleanly against CUDA 12.8 despite postdating it. Do not pre-emptively add `-allowUnsupportedCompiler`.

4

### Fetch the model

    curl.exe -L --fail --retry 3 -o Qwen3.8-27B-UD-Q4_K_M.gguf `
      https://huggingface.co/unsloth/Qwen3.8-27B-GGUF/resolve/main/Qwen3.8-27B-UD-Q4_K_M.gguf

    # verify byte-exact against the remote Content-Length:
    (Get-Item Qwen3.8-27B-UD-Q4_K_M.gguf).Length     -> 16464440224

Reported by llama.cpp as `qwen35 27B Q4_K - Medium, 15.32 GiB, 27.32 B params`.

5

### Disable Sysmem Fallback

NVIDIA Control Panel → Manage 3D Settings → CUDA - Sysmem Fallback Policy → Prefer No Sysmem Fallback, scoped to `llama-server.exe`. Restart the process — the policy binds at CUDA context creation.

Trap 04 · 5–10× silent tax

Since driver 536.40, Windows does not OOM when you overfill VRAM. It spills into shared system memory over PCIe. No error, no crash, and llama.cpp still reports “using GPU”. At 256K context you hold 958 MB of headroom, so you want a loud failure rather than a quiet one.

§ 04 · Reference

## Trap catalogue

Every failure encountered, with its exact symptom. Ordered by cost.

# | Trap | Symptom | Fix
--- | --- | --- | ---
Build & toolchain
01 | Bad CUDA subpackage name | exit -522190823 | Aborts whole install, no error text. Use documented names.
02 | CUDA bin not on PATH | exit -1073741515 | `STATUS_DLL_NOT_FOUND`, zero output. Add to machine PATH.
03 | Stale `FORCE_CUBLAS` | silent, 5–6× slower | Delete `build/`; grep `CMakeCache.txt`.
04 | CUDA 13.x on Blackwell | segfault / cuBLAS | Use 12.8. `winget` only has 13.3.
05 | Ninja can't find MSVC | compiler test fails | Import `vcvars64.bat` before configure.
06 | Bundled driver downgrade | 610.88 → 572.61 | Omit `Display.Driver` from subpackages.
07 | `winget` exit 43 | override ignored | Package present; workload may not be. Verify `cl.exe`.
Shell & flags
08 | Git Bash runs the exe | api-ms-win-crt-heap | Use PowerShell. Failure line starts with the exe path, not `error:` — a naive grep reports success.
09 | `-no-cnv` removed | invalid argument | Use `-st`/`--single-turn`. Present in `arg.cpp` but unregistered — trust `--help`.
10 | `llama-bench` for MTP | flags absent | Use `llama-cli`/`llama-server`.
11 | PowerShell arg splatting | mangled args | Verify the binary received what you sent before blaming the flag.
Model & measurement
12 | `-effort minimal` | Jinja Exception | Only `low`/`medium`/`xhigh` exist.
13 | `-effort high` | silently faked | Falls back to `xhigh`. Byte-identical output proves it.
14 | Token cap too low | truncated mid-word | Invalidates time-to-answer. Verify output completes.
15 | Sysmem fallback | silent 5–10× | Set “Prefer No Sysmem Fallback”.
16 | Benchmarking at `-np > 1` | MTP gain vanishes | Speculative decode is single-stream.
17 | `-f` plus empty `-p` | hangs forever | llama-cli waits on stdin; under a non-interactive shell it never returns. Model loads (VRAM occupied) then sits at idle power. Pass `-f` alone.
18 | `$args` in PowerShell | --model is required | `$args` is an automatic variable; assigning to it and splatting silently drops everything. Rename it.

#### Trap 13 in detail — how a fake flag was caught

`--reasoning-effort high` returns plausible numbers, so it looks like data. Stripping timing lines and hashing the output bodies proves all three are the same generation:

    high.log     14190 bytes   64.0 t/s   md5 4df134687af5408a19b989dc4b5c8530
    xhigh.log    14190 bytes   63.7 t/s   md5 4df134687af5408a19b989dc4b5c8530
    default.log  14190 bytes   63.9 t/s   md5 4df134687af5408a19b989dc4b5c8530

The 0.3 tok/s spread is run noise on identical work. Generalisable lesson: when a flag has no documented value list, verify it changed the output before you trust the number.

§ 05 · Evidence

## Complete raw data

Individual runs, not just means. `--temp 0` and a fixed seed make speculative decoding deterministic run-to-run, but NOT byte-identical across draft widths — see the corrected methodology note below.

Experiment A — draft width, reasoning OFF. `-n 1024`, 2 runs each. Prompt t/s is short-prompt, not comparable to `llama-bench` pp512.
Config | Run 1 | Run 2 | Mean | Spread | vs base | Prompt t/s
--- | --- | --- | --- | --- | --- | ---
baseline | 40.9 | 40.6 | 40.8 | 0.3 | — | 219 / 225
n-max 2 | 61.1 | 61.1 | 61.1 | 0.0 | +49.9% | 199 / 197
n-max 3 | 65.2 | 65.2 | 65.2 | 0.0 | +60.0% | 194 / 192
n-max 4 | 63.8 | 63.8 | 63.8 | 0.0 | +56.6% | 203 / 197
n-max 5 | 96.7 | 96.9 | 96.8 | 0.2 | +137.5% | 193 / 199
n-max 6 | 94.7 | 95.2 | 95.0 | 0.5 | +133.0% | 188 / 189
n-max 7 | 93.9 | 93.9 | 93.9 | 0.0 | +130.4% | 194 / 188
n-max 8 | 94.1 | 93.7 | 93.9 | 0.4 | +130.4% | 190 / 193
n-max 10 | 89.2 | 89.6 | 89.4 | 0.4 | +119.4% | 193 / 184
n-max 12 | 83.1 | 83.2 | 83.2 | 0.1 | +104.0% | 184 / 191

Experiment B — draft width, thinking ON. `-n 512`, 3 runs each. Same optimum, far smaller gain.
Config | Run 1 | Run 2 | Run 3 | Mean | vs base
--- | --- | --- | --- | --- | ---
baseline | 40.9 | 40.5 | 40.6 | 40.7 | —
n-max 2 | 46.3 | 46.3 | 46.3 | 46.3 | +13.9%
n-max 3 | 43.3 | 43.3 | 43.4 | 43.3 | +6.6%
n-max 5 | 51.8 | 51.7 | 51.7 | 51.7 | +27.2%
n-max 6 | 47.1 | 47.0 | 47.3 | 47.1 | +15.9%
n-max 8 | 51.0 | 51.4 | 51.2 | 51.2 | +25.9%
draft-dspark | 40.3 | — | — | 40.3 | −1.0%

Experiment C — reasoning effort. Time to a complete answer, identical prompt, n-max 5.
Effort | Answer time | Tokens | Decode | Status | Note
--- | --- | --- | --- | --- | ---
off | 33.2 s | ~3,008 | 90.6 | complete | No thinking block emitted.
low | 55.1 s | ~4,783 | 86.8 | complete | Thinks, then answers.
medium | 91.4 s | ~8,436 | 92.3 | complete | Extended deliberation.
xhigh (default) | 231 s+ | >16,384 | 70.3 | truncated | Hit the cap still mid-thinking. 231 s is a floor.
high | — | — | 64.0 | fake | Silently resolves to `xhigh`.
minimal | — | — | — | error | Jinja exception; not a valid level.

Experiment D — context window cost. Reasoning off, n-max 5, window allocated but empty.
Context | Decode | Prompt | Peak VRAM | % of 24,462 MB | Headroom
--- | --- | --- | --- | --- | ---
32,768 | 82.9 | 111.3 | 17,262 MB | 70.6% | 7,200 MB
131,072 | 81.3 | 100.2 | 19,920 MB | 81.4% | 4,542 MB
262,144 | 81.2 | 102.1 | 23,504 MB | 96.1% | 958 MB

Experiment F — context actually FILLED. Real source text fed to depth, then 256 tokens generated. This is the number that matters.
Depth | Prefill | Decode | Retention | Peak VRAM | Wall
--- | --- | --- | --- | --- | ---
empty (ref) | — | 83.4 | 100% | 17,262 MB | —
24K filled | 1357.5 | 63.8 | 76% | 17,272 MB | 37.9 s
100K filled | 807.3 | 57.5 | 69% | 19,930 MB | 155.9 s
200K filled | 522.2 | 36.1 | 43% | 23,514 MB | 466.1 s

The most important number in this guide

At 200K filled, decode is 36.1 tok/s — below the 40.8 tok/s unoptimised baseline we started from. Deep context costs more than MTP and reasoning-effort tuning gained combined. Every other speed figure in this guide is a shallow-depth figure.

Prefill degrades too: 1357 → 807 → 522 t/s with depth. At 200K that is roughly 6.4 minutes of ingest before the first token. Time-to-first-token, not decode rate, dominates a genuinely long session.

Peak VRAM at 200K filled (23,514 MB) matches the allocated-only figure (23,504 MB) almost exactly, confirming the KV cache is preallocated rather than grown. VRAM planning can use the empty-window numbers; throughput planning cannot. Note also that Experiment D's “Prompt t/s” column (~100–111) is overhead on a tiny prompt, not a real prefill rate — the true rates are in Experiment F.

Experiment G — output shape × draft width. Reasoning off, `-n 1024`. Baseline holds at 40.4–40.5 across all four, so content affects acceptance, never raw decode.
Shape | base | n2 | n3 | n4 | n5 | n6 | n8 | n10 | n12 | n16 | n20 | Best
--- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | ---
code | 40.5 | 60.3 | 62.6 | 61.2 | 93.7 | 89.8 | 93.1 | — | — | — | — | n5 +131%
prose | 40.4 | 48.5 | 45.8 | 38.6 | 58.4 | 53.8 | 49.0 | — | — | — | — | n5 +45%
json | 40.4 | 63.9 | 71.6 | 71.3 | 112.1 | 120.0 | 130.7 | 141.6 | 137.1 | 130.5 | 122.2 | n10 +250%
markup | 40.4 | 64.3 | 70.7 | 69.3 | 109.0 | 111.8 | 115.6 | 118.1 | 111.2 | 101.1 | 96.0 | n10 +192%

Three things worth extracting. The optimum moves with output shape — n5 for code and prose, n10 for JSON and HTML. The gain varies by 5×, from +45% on prose to +250% on JSON; rigid formats are trivially predictable, so drafts land far more often. And n4 on prose is actively harmful (38.6 against a 40.4 baseline) — the only measured config anywhere in this guide that is slower than no speculation at all.

141.6 tok/s on JSON is the fastest figure measured on this box, well above the 96.8 headline, which was code-shaped. If your workload is structured extraction or API-shaped output, the ceiling here is considerably higher than the front page suggests.

Experiment E — power limit. `llama-bench`, pp512/tg128, 3 reps.
Limit | Decode (tg128) | Prefill (pp512) | Verdict
--- | --- | --- | ---
95 W default | 40.86 ± 0.68 | 1664.14 ± 1.73 | —
175 W enforced | 40.94 ± 0.58 | 1661.82 ± 9.09 | No effect. Difference is inside error bars in both directions.

§ 06 · Instrumentation

## Power & thermal telemetry

Sampled at 2 Hz during Experiment B, with the limit raised to 175 W.

Run | Samples | Peak W | Avg W | Peak SM | Avg SM | Mem clk | Peak °C
--- | --- | --- | --- | --- | --- | --- | ---
baseline | 41 | 171.2 | 109.3 | 1987 | 1687 | 14101 | 51
mtp-n2 | 40 | 170.6 | 100.0 | 2100 | 1574 | 14101 | 56
mtp-n3 | 41 | 170.3 | 103.7 | 2017 | 1612 | 14101 | 57
mtp-n4 | 41 | 170.9 | 101.8 | 2047 | 1602 | 9001 | 58
mtp-n5 | 38 | 169.9 | 95.0 | 2272 | 1545 | 14101 | 56
dspark | 42 | 173.3 | 108.5 | 2460 | 1572 | 14101 | 57

Three things fall out of this. Average draw is 95–109 W against a 170 W ceiling — the GPU is not power-starved even at the 95 W default, which is why raising the limit changed nothing. Peak temperature is 58 °C, so thermal throttling is simply not a factor on this chassis; the common advice to power-cap for sustained throughput does not apply here. The fastest config draws the least power — n-max 5 averages 95.0 W, the lowest of the set, while being the fastest. Speculative decoding replaces memory traffic with cheap parallel verification.

Anomaly

The `mtp-n4` trace caught a memory clock of 9001 MHz against 14101 MHz elsewhere — a momentary downclock. n4 is also the one config that underperforms its neighbours (63.8 against 65.2 at n3). Not enough samples to call it causal; flagged for anyone re-running.

§ 07 · Interpretation

## What the numbers mean

### The n-max curve is discontinuous, not a curve

n4→n5 jumps 52% between adjacent integers (63.8→96.8), and n5 beats n6. With run-to-run spread of ±0.2 this is not noise. The same cliff appears with thinking on (43.9→51.7), just smaller. This is kernel tiling: certain draft widths produce batch shapes that map well onto the hardware.

Practical consequence: you cannot interpolate. A coarse sweep of 2/4/8 would have found 63.8, 63.8, 93.9 and concluded “bigger is better, use 8” — missing the peak entirely. Sweep every integer through the plausible range.

### Derived draft acceptance

Not directly reported, but inferable. Speedup ≈ 1 + (mean accepted drafts per verify step):

Mode | Speedup | Implied extra tokens/step | Reading
--- | --- | --- | ---
reasoning off | 2.37× | ~1.37 of 5 | Code output drafts well.
thinking on | 1.27× | ~0.27 of 5 | Reasoning prose drafts badly.

Roughly 5× better acceptance on code than on reasoning prose, consistent with the commonly cited ~0.80 vs ~0.60 acceptance figures. Marked derived, not measured — llama.cpp did not print acceptance counters in these runs.

### MTP costs prefill to buy decode

Rarely mentioned and worth knowing: enabling MTP reduces prompt throughput. Thinking-on runs show baseline 301.4 t/s prompt against 254–259 with MTP — roughly −15%. Reasoning-off runs show ~222 baseline against ~193 — about −12%.

Net strongly positive for generation-heavy work. But for a workload that ingests huge prompts and emits little — bulk classification, reranking — MTP may be a net loss. Measure your own shape.

### Rate is the wrong metric for reasoning effort

Decode rate across effort levels spans only 70–92 tok/s. Token spend spans 3,008 to over 16,384 — more than 5×. Optimising tok/s would have picked `medium` (92.3, the highest of the completed runs) when `off` answers the same question in a third of the time. Measure time to a complete answer.

### Why the laptop part beat its bandwidth ratio

The laptop 5090 has 896 GB/s against the desktop's 1792 — exactly half. A pure bandwidth model predicts ~30 tok/s baseline against the desktop's 61.4. It measured 40.8, or ~67% of desktop, not 50%. Decode is bandwidth-bound but not purely so. Predictions from bandwidth ratios alone will be pessimistic.

§ 08 · Don't repeat these

## Negative results

Tried | Result | Why
--- | --- | ---
95 W → 175 W | 0% | 40.86→40.94 tok/s. GPU genuinely drew 171 W peak at 1987–2472 MHz, but averaged only 95–109 W — it was never power-starved. Decode is bandwidth-bound and GDDR7 clocks don't scale with power budget.
draft-dspark | −1% | 40.3 vs 40.8 baseline. No benefit over no speculation at all.
n-max > 8 | falls off | n10 89.4, n12 83.2. Wider drafts cost more than they recover.
-effort high | fake | Byte-identical to `xhigh`. Plausible numbers that are just the default — worse than an error.
-effort minimal | error | `Jinja Exception: Unexpected reasoning effort minimal. Supported types are xhigh (default), medium, and low.`
Thermal power-capping | n/a | Peak 58 °C. Standard advice to cap at 75–80% for sustained throughput solves a problem this chassis does not have.
Git Bash as host shell | cannot run | UCRT resolution failure on every binary.

§ 09 · Method

## Benchmark methodology

Two methodology errors were made and corrected during this work. Both are easy to repeat and neither announces itself.

Rule | Reason
--- | ---
Verify output completes | At `-n 3072` the high-effort runs were cut off mid-token, so “time to answer” was really “time to hit the cap”. Even `-n 16384` did not finish `xhigh`. Read the tail of the output, don't trust the timer.
Confirm the flag did something | `-effort high` was benchmarked before anyone noticed it silently resolves to the default. Hash the output body against a known config.
`--temp 0` + fixed seed | Corrected. An earlier version of this guide claimed speculative decoding is lossless and therefore byte-identical across draft widths. That is false. Each config is deterministic run-to-run (rep1 == rep2, always), but configs diverge from each other by 98–117 lines. Cause: batched draft verification uses different matmul shapes than single-token decode; floating-point addition is not associative, an argmax flips, and divergence cascades. Lossless in theory, not bit-exact in practice. The fixed seed still gives reproducibility within a config, which is what makes the timings comparable.
Check quality across draft widths | Because output genuinely differs by n-max, wider drafts could in principle trade quality for speed. Probed on structured output: JSON stayed valid with all 10 records and every required field at base, n5, n10 and n20. Reassuring, but one probe on one task type — not a general guarantee.
Verify the flag changed the output | Hash the output body against a known-good config. Two separate settings in this guide (`--reasoning-effort high`, and `thinking:{type:'disabled'}` on the API side) produced plausible numbers that were measuring something other than what was intended.
`--parallel 1` | Speculative decode is single-stream. Mixed parallelism produces incomparable numbers.
Generate ≥ 400 tokens | Short generations pay more in speculation overhead than they recover.
Subtract model load | ~12.7 s constant on this box. Measure it once with `-n 1` and subtract, or it swamps short runs.
Sample telemetry alongside | Power and clock traces are what proved the limit change was inert, rather than assuming it helped.
Retain invalid runs | The truncated and faked runs are the most instructive rows in the dataset.

#### Benchmark harness

    $common = @('-m',$model,'-ngl','999','-fa','on',
                '-ctk','q4_0','-ctv','q4_0',
                '-c','32768','-n','1024','--seed','42','--temp','0',
                '-st','--no-warmup','--reasoning','off','-p',$prompt)

    foreach ($n in 2,3,4,5,6,7,8,10,12) {
      for ($i=1; $i -le 2; $i++) {
        & $cli @common --spec-type draft-mtp --spec-draft-n-max $n *> "$out\n$n-$i.log"
      }
    }
    # llama-cli reports: [ Prompt: NNN t/s | Generation: NN.N t/s ]
    # NOT the llama_perf_context_print / "eval time" format older guides parse.

§ 10 · Honest gaps

## Untested & open questions

Stated so nobody mistakes absence of data for absence of a problem.

- Filled-context throughput is unmeasured. Now measured — Experiment F. Decode retains 43% at 200K filled (36.1 tok/s). Better than the ~12 tok/s some published far-context figures suggest, but still below our unoptimised baseline.
- Depth beyond 200K is still unmeasured. The 262,144 window was allocated but filled only to ~200K. The last 60K may degrade further.
- Single-shot only. Each measurement is one prompt, one generation. A real multi-turn session with a growing cache may behave differently again.
- Quality was never measured. No perplexity, no benchmark scoring. The claim that `-effort low` is a reasonable default rests on it completing sensibly, not on evidence it answers as well as `xhigh`.
- Only one quant tested. Q5_K_M and Q6_K were never pulled. The speed cost of higher fidelity on this box is unknown.
- One prompt shape. Now measured — Experiment G. The optimum does move: n5 for code and prose, n10 for JSON and markup. Translation and tool-calling remain untested.
- Acceptance rates are derived, not measured. Inferred from speedup ratios.
- The n4 memory downclock was seen once. Unexplained, possibly relevant to n4's underperformance.
- Sysmem fallback was never actually disabled during these runs — it is a Control Panel setting. Results held anyway because 256K fits, but a filled window may behave differently.

§ 11 · Provenance

## Exact versions

GPU
  RTX 5090 Laptop, 24,463 MB
Compute cap
  12.0 → sm_120
Driver
  610.88
Bandwidth
  896 GB/s, 256-bit
Mem clock
  14,001 MHz (GDDR7)
Power
  95 W default / 175 W max
PCIe
  Gen 5 ×16
CPU
  Core Ultra 9 290HX Plus, 24C/24T
RAM
  63.4 GB
OS
  Windows 11 Home SL
CUDA
  12.8.93 (12.8.1)
MSVC
  14.44.35207
CMake
  4.4.2
Ninja
  1.13.2
llama.cpp
  192067b, 2026-08-26
Model repo
  unsloth/Qwen3.8-27B-GGUF
Quant
  UD-Q4_K_M, 15.32 GiB
Bytes
  16,464,440,224
Arch / params
  qwen35 / 27.32 B
Native context
  262,144

The laptop 5090 is a different part from the desktop 5090 — 24 GB on a 256-bit bus at 896 GB/s, against 32 GB / 512-bit / 1792 GB/s. Roughly half the bandwidth and three-quarters the VRAM. Published desktop figures do not transfer, and neither do these in the other direction.

#### Build flags of record

    -DCMAKE_BUILD_TYPE=Release
    -DGGML_CUDA=ON
    -DCMAKE_CUDA_ARCHITECTURES=120
    -DGGML_CUDA_FORCE_CUBLAS=OFF
    -DGGML_CUDA_F16=ON
    -DLLAMA_CURL=OFF