Files
LLM-Server/docs/research/dual-r9700-speed-and-offload.md
T

241 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Research: fastest inference on 2x R9700 (27B Q8_K_XL vs Flash-Next, multi-GPU modes, RAM/SSD offload, Strata)
**Date:** 2026-10-29
**Question:** A second Radeon AI PRO R9700 arrives in about a week. What gives the fastest decode (and prefill) for agentic coding on this box: `Qwen3.8-27B-UD-Q8_K_XL`, `Qwen3.8-Flash-Next-UD-Q4_K_XL`, or something else? Which llama.cpp multi-GPU mode helps, does RAM/SSD offload work with 32 GB of RAM, and is Strata worth switching to?
**Scope note:** research only. Nothing in `docker-compose.yml` or `.env.example` was changed, so nothing needs deploying.
**Hardware assumed:** Ryzen 9 5950X, 32 GB DDR4 dual channel (reported ~94 % used, swap in use), ASRock B550M Pro4, 1 TB NVMe, Ubuntu 24.04, kernel 6.8, 1x R9700 now and 2x soon.
## TL;DR
| # | Finding | Confidence |
|---|---|---|
| 1 | **The second card does not make the dense 27B decode faster with `-sm layer`.** Layer split runs the cards one after the other, so one token still reads every weight at one card's bandwidth. It only adds VRAM. | High (llama.cpp docs) |
| 2 | **`UD-Q8_K_XL` is a quality move, not a speed move.** It reads ~30.1 GB per token against ~16.8 GB for today's `UD-Q4_K_XL`. Estimated decode is ~15 t/s without speculation and ~25 t/s with DFlash2, against ~46 t/s measured today. | Medium (estimate, math below) |
| 3 | **`-sm tensor` is the only mode that can speed up one stream, and on ROCm it is immature.** The one measured 2-card ROCm result for this exact model is 1.31x one card (2x 7900 XTX). DFlash2 crashes with `-sm tensor` (open bug), and MTP plus `-sm tensor` has lockup reports. Your second slot is PCIe 3.0 x4 behind the chipset, which hurts the per-layer all-reduces. | Medium |
| 4 | **Best use of the second card for 2 parallel agents: two independent single-GPU `llama-server` replicas**, one per card, each `--parallel 1`. Each agent keeps today's ~46 t/s, the decode drop when the other slot prefills goes away, and each slot gets the full 262144 tokens instead of 131072. Needs a second backend connection in OmniRoute with per-conversation stickiness. | Medium-high |
| 5 | **Flash-Next UD-Q4_K_XL (111.3 GB) does not fit.** Even with the 28.8 GB n-gram table left on SSD, 82.5 GB of weights remain for 64 GB of VRAM. The ~20-24 GB of experts left over would come from NVMe, because RAM is full. Estimated decode is then ~6-12 t/s. Only the all-in-VRAM quants (`UD-Q2_K_XL`, `UD-IQ3_XXS`) are realistic, at a large quality cost (mean KLD 0.22 / 0.17 vs 0.047). | Medium |
| 6 | **RAM is the binding constraint, and compose itself is a likely cause of the swap.** `--cache-ram` is set to 24576 MiB (24 GiB) on a 32 GB host that also runs OmniRoute with `mem_limit: 10g`. | High for the config, inferred for the swap |
| 7 | **Strata:** a real, fast-moving MIT engine built around Flash-Next expert tiering. It has R9700 HIP builds and a layer split for 2 cards, but its design wants all experts in RAM (~88 GB for UD-Q4_K_XL), it does not run the dense 27B at all, and its numbers are self-reported on hosts with 47-503 GB of RAM. Not worth it on 32 GB. Revisit after a RAM upgrade (board max 128 GB). | Medium |
## 1. Platform facts that drive everything
- **R9700:** 32 GB GDDR6, 256-bit, 20 Gbps, **640 GB/s**, PCIe 5.0 x16, 300 W ([Puget Systems listing](https://www.pugetsystems.com/parts/Video-Card/AMD-Radeon-AI-PRO-R9700-32GB-16908/), [Tom's Hardware launch](https://www.tomshardware.com/pc-components/gpus/amd-launches-radeon-ai-pro-r9700-to-challenge-nvidias-ai-market-dominance)). AMD's own product page timed out on fetch, so this is from third-party listings.
- **Effective bandwidth, calibrated from this repo's data:** no-spec decode for the 27B Q4 on one R9700 is 25-30 t/s across three outside sources ([`qwen3.8-27b-throughput-tuning.md`](qwen3.8-27b-throughput-tuning.md)). 16.8 GB/token x ~26.4 t/s ≈ **~440 GB/s effective (~69 % of peak)**. All estimates below use 420-450 GB/s.
- **ASRock B550M Pro4 slots**, from the [ASRock manual (PDF)](https://download.asrock.com/Manual/B550M%20Pro4.pdf) spec page with a Matisse/Vermeer CPU: "2 x PCI Express x16 Slots (PCIE1: Gen4x16 mode; PCIE3: Gen3 x4 mode)", plus one PCIe 3.0 x1 slot (PCIE2). M2_1 is "up to Gen4x4", M2_2 "up to Gen3 x2", and M2_2 shares lanes with SATA3_5_6. Memory is dual channel, max 128 GB. So **the second R9700 runs at PCIe 3.0 x4 (~3.9 GB/s per direction)**.
- **PCIE3 is behind the chipset.** The manual does not say so explicitly. It follows from the AM4 lane budget: the CPU gives x16 to the GPU, x4 to the CPU-attached M.2, and x4 as the chipset uplink. B550's own lanes and uplink are PCIe 3.0 ([ComputerBase B550 test](https://www.computerbase.de/2020-06/amd-b550-chipsatz-mainboard-test/), [Guru3D](https://www.guru3d.com/review/amd-ryzen-3-3100-and-3300x-review/page-3/)). Card 2 therefore shares the chipset's PCIe 3.0 x4 uplink with M2_2, SATA, USB and LAN. GPU-to-GPU traffic crosses CPU root complex → chipset → slot. Whether ROCm P2P works across that path was not verified. Assume it falls back to staging through host memory.
- **DDR4:** dual-channel DDR4-3200 peaks at 2 x 8 B x 3200 MT/s = **51.2 GB/s**. Plan for ~35-45 GB/s effective (estimate).
- **RAM budget today:** `docker-compose.yml` sets `--cache-ram ${LLAMA_CACHE_RAM:-24576}` (24 GiB) and OmniRoute `mem_limit: 10g` (`OMNIROUTE_MEMORY_MB=8192`). That is 34 GiB of permitted use on a 32 GB host before the OS, qdrant and page cache. `--cache-ram` is a ceiling that fills up over time, which fits the reported 94 % plus swap. `docs/architecture.md` already says to "keep it well below the host's free RAM".
- **Physical and power, unverified:** two 300 W cards plus a 142 W-PPT 5950X need a PSU of roughly 850 W or more. [`500k-context-…roadmap.md`](500k-context-2-parallel-agents-gpu-roadmap.md) already flagged PSU headroom as the limit. A dual-slot card in the bottom slot of a micro-ATX board can also need a fifth rear bracket in the case. Check both before the card arrives.
## 2. Qwen3.8-27B `UD-Q8_K_XL`
**File facts** (HF tree API plus the GGUF header, parsed from the first 12 MB of each file):
| | UD-Q4_K_XL (today) | UD-Q8_K_XL |
|---|---|---|
| File size | 17.56 GB | **31.46 GB** |
| Tensor types | mixed IQ4/Q4_K-Q6_K, Q8_0 for small tensors | Q8_0 FFN/attention, **BF16** `attn_q/k/v` and `output` |
| `token_embd` (row lookup, not streamed) | 0.72 GB | 1.35 GB |
| **Bytes streamed per token** | **~16.8 GB** | **~30.1 GB (1.79x)** |
Sources: [HF tree API](https://huggingface.co/api/models/unsloth/Qwen3.8-27B-GGUF/tree/main), [file page](https://huggingface.co/unsloth/Qwen3.8-27B-GGUF?show_file_info=Qwen3.8-27B-UD-Q8_K_XL.gguf). Both files carry the built-in MTP head (`qwen35.block_count = 65`, `nextn_predict_layers = 1`).
**Architecture** ([unsloth config.json](https://huggingface.co/unsloth/Qwen3.8-27B-GGUF/raw/main/config.json)): dense, `Qwen3_5ForConditionalGeneration`, 64 layers, hybrid. 48 Gated DeltaNet (`linear_attention`) layers and 16 `full_attention` layers (`full_attention_interval: 4`), 24 Q heads, 4 KV heads, head_dim 256, 262144 native context. Every weight is read for every token.
**Fit on 2x32 GB.** KV is 16 layers x 2 x 4 heads x 256 x 1.0625 B (q8_0) ≈ 34.8 KB/token, so ~9.1 GB at 262144 tokens. 31.5 GB weights + 1.1 GB DFlash2 Q4_K_M ([z-lab GGUF tree](https://huggingface.co/api/models/z-lab/Qwen3.8-27B-DFlash2-GGUF/tree/main)) + 9.1 GB KV + ~3-5 GB compute buffers per card comes to **~46-50 GB of 64 GB** (estimate). It fits with room. It does not fit on one card.
**Decode estimate** (bandwidth-bound, ~440 GB/s per card):
| Config | Math | Est. decode, no spec | With speculation (x1.7, the ratio this repo measures for DFlash2: 45.9 / ~27) |
|---|---|---|---|
| Q4_K_XL, 1 card (today) | 16.8 / 440 = 38 ms | ~26 t/s (measured 25-30) | **45.9 t/s measured** |
| Q8_K_XL, `-sm layer` | 30.1 / 440 = 68 ms, cards in series | **~15 t/s** | **~25 t/s** |
| Q8_K_XL, `-sm tensor` at 1.31x | 15 x 1.31 | ~19 t/s | DFlash2 crashes here (see §3). MTP x1.5 gives ~29 t/s if stable |
| Q4_K_XL, `-sm tensor` at 1.31x | 26 x 1.31 | ~34 t/s | MTP x1.5 gives ~50 t/s if stable |
All of these except the measured row are estimates. Q8_K_XL roughly halves single-stream speed against today. Pick it only if Q4 quality is shown to be the bottleneck on your own tasks.
## 3. llama.cpp multi-GPU on ROCm
Source: [`docs/multi-gpu.md`](https://github.com/ggml-org/llama.cpp/blob/master/docs/multi-gpu.md) and `common/arg.cpp` on master (fetched 2026-10-29). `--split-mode` takes `none, layer, row, tensor`.
| Mode | What it does | Decode on bandwidth-bound cards | Status |
|---|---|---|---|
| `layer` (default) | Pipeline: each GPU owns a contiguous block of layers and their KV | **No single-stream gain.** One token walks GPU0 then GPU1, each reading only its half, so the total time is the same. Prefill can overlap ubatches across GPUs. The docs say: "priority is a fast prefill. Can tolerate slow interconnect speeds" | Mature |
| `row` | Old row-split of dense matmuls | Docs: "**Deprecated** … Superseded by `tensor`". [PR #19378](https://github.com/ggml-org/llama.cpp/pull/19378): needs "a synchronization after every single operation", only worth it on old slow GPUs | Avoid |
| `tensor` | Meta-backend tensor parallelism: weights **and** KV split, all-reduce at sync points | Both cards read their half of each layer at the same time, so it is the only mode with a decode win. Docs: "Performance should be good for multiple NVIDIA GPUs using the CUDA backend, no guarantees otherwise" | **EXPERIMENTAL**. Merged in [#19378](https://github.com/ggml-org/llama.cpp/pull/19378) (2026-04-09) |
**Measured `-sm tensor` numbers that are relevant here:**
- **Qwen3.8-27B Q4_K_XL, 2x RX 7900 XTX, ROCm: 47.06 t/s = 1.31x one card.** Per-card DRAM utilization drops from 65.9 % to 43.0 % when split ([#28863](https://github.com/ggml-org/llama.cpp/issues/28863), closed without a fix). This is the best proxy for a 2x R9700 result.
- The #19378 author on ROCm (RX 6800 + MI50, RX 9060 XT + MI100): "the performance is bad vs. the `-sm layer` baseline". On 2x RTX 4090, the dense Gemma4 31B decoded **1.36x** faster, while pp2048 fell to 0.61x and the MoE model got slower.
- [#27825](https://github.com/ggml-org/llama.cpp/pull/27825) (merged 2026-09-15) turned on the internal AllReduce for HIP. On an RX 9070 at x16 plus an RX 6800 XT at **PCIe 4.0 x4**, Gemma4 31B Q6_K: tg 23.62 → 24.15 (+2 %), pp +16 %. RCCL is opt-in at build time (`-DGGML_HIP_RCCL=ON`), "disabled because … it was not universally beneficial" (multi-gpu.md). Whether the official `server-rocm` image builds with RCCL was not checked.
- 2x R9700 user, Gemma4 26B-A4B Q8_0: `-sm tensor` f16 KV 29.7 t/s vs `-sm layer` 19 t/s ([#23567](https://github.com/ggml-org/llama.cpp/issues/23567)). Same card, a different (MoE) model.
- A 5950X + dual-R9700 user runs "Qwen 3.6 27B Q8 … with tensor split" and reports trouble mixing tensor-split and layer-split processes on ROCm ([#28582](https://github.com/ggml-org/llama.cpp/issues/28582), open).
**Constraints and bugs** that apply to this stack:
- Architecture support: `qwen35` and `qwen4exp` are **not** on the deny list in `llm_arch_supports_sm_tensor` (`src/llama-arch.cpp`, master). `-sm tensor` was re-enabled for qwen4exp in [#28569](https://github.com/ggml-org/llama.cpp/pull/28569).
- `-sm tensor` needs flash-attn on (`llama-context.cpp`). Quantized KV is supported since [#23792](https://github.com/ggml-org/llama.cpp/pull/23792) (2026-06-01). llama.cpp's `docs/multi-gpu.md` still says it errors, so the doc is stale there. `--fit` is "not supported with `tensor`", so set `--ctx-size` by hand.
- **DFlash2 + `-sm tensor` crashes:** `GGML_ASSERT(src_ss[0].axis != GGML_BACKEND_SPLIT_AXIS_0)` ([#28777](https://github.com/ggml-org/llama.cpp/issues/28777), open). The ROCm variant ([#27829](https://github.com/ggml-org/llama.cpp/issues/27829)) was closed as stale, and the fix PR [#27858](https://github.com/ggml-org/llama.cpp/pull/27858) is open. Its tester still hit a different assert, and a commenter says "this is already a problem for ROCm". The workaround in #28777 is `-sm layer`.
- **MTP + `-sm tensor`:** reproducible lockups on CUDA ([#27122](https://github.com/ggml-org/llama.cpp/issues/27122), closed as stale). One ROCm report (2x V620, Q8_K_XL) saw one hard abort in ~67 h.
- **gfx1201 flash-attn garbage at some context lengths for Qwen3.8-27B** (head_dim 256): immediate EOS in bands, reproduced on **2x R9700, Linux, `--tensor-split 1.7,1`** ([#30884](https://github.com/ggml-org/llama.cpp/issues/30884), open). Another commenter could not reproduce it through `/v1/chat/completions`. Run a needle test on any new config.
**PCIe x4 chipset slot, estimated effects:**
- `-sm layer` decode: one hidden-state hop per token (5120 x 4 B ≈ 20 KB). Negligible. Prefill: ~10 MB per 512-token ubatch hop, ~3 ms at 3.9 GB/s. Small.
- `-sm tensor` decode: ~2 all-reduces per layer x 64 layers ≈ 128 small reductions per token. These are latency-bound, not size-bound. At an assumed 20-50 µs each through host memory, that is 2.5-6.4 ms per token, 10-25 % of a 25 ms token (estimate). This is the likely reason #28863 lands at 1.31x and not ~1.9x, and an x4-through-chipset link will not do better.
- `-sm tensor` prefill: ~2 x tokens x 5120 x 2 B per layer. For a 2048-token ubatch that is ~2.7 GB over 64 layers, ~0.7 s at 3.9 GB/s, so prefill is capped at roughly 3000 t/s plus compute (estimate). Expect prefill to get worse than layer split, as in the #19378 tables.
**Flags:**
- `-ts/--tensor-split` sets proportions. With `layer` the default split follows free memory. With `tensor` the default is an even split.
- `-mg/--main-gpu` applies only to `-sm none` (pick the card) or `row` (intermediate results). It does nothing for `layer`/`tensor`.
- Draft placement: `--spec-draft-device/-devd` (default: follows `--device`), `-ngld`, `-otd` (`common/arg.cpp`). Under `layer`, the output head sits on the last GPU. Place the drafter on one card and A/B both cards. No source documents which placement is better for DFlash2, which reads hidden states from several target layers (`n_extract=5`) spread over both cards.
## 4. Qwen3.8-Flash-Next `UD-Q4_K_XL`
**Model facts** ([unsloth model card](https://huggingface.co/unsloth/Qwen3.8-Flash-Next-GGUF), GGUF header of shard 1, and shard headers 2-4):
- `general.architecture = qwen4exp`, described as "A Preview of the Qwen4 Architecture". **MoE: 125B total, 6B activated, plus a 51B n-gram embedding and a 4B MTP head.** 48 layers, hidden size 2560, layout 12 x (3 x Gated DeltaNet + 1 x Qwen Sparse Attention), each layer followed by an MoE block. **512 experts, 10 routed + 1 shared**, expert FFN dim 640. Native context 262144. License `qwen-community-1.0`. The 27B is Apache 2.0 per [`qwen3.8-27b-quant.md`](qwen3.8-27b-quant.md).
- **UD-Q4_K_XL = 111.33 GB** in 4 shards (0.01 + 49.86 + 49.38 + 12.09 GB, [HF tree API](https://huggingface.co/api/models/unsloth/Qwen3.8-Flash-Next-GGUF/tree/main?recursive=true)). It splits into:
- `per_layer_token_embd` (n-gram table): **28.80 GB** IQ4_NL, read a few rows per token. The same size in every UD quant.
- routed experts `ffn_{gate,up,down}_exps`: **77.02 GB**
- everything else (attention/DeltaNet, shared experts, routers, hyper-connections, output head): **~4.8 GB**, mostly Q8_0
- **Bytes per token:** 4.8 + 77.02 x 10/512 (1.50) ≈ **~6.3 GB**, 2.7x less than the 27B Q4. The all-in-VRAM ceiling is 6.3 / 440 → ~70 t/s. Batch-1 MoE kernels gather scattered experts and reach less of peak, so the estimate is **~40-60 t/s if it fit in VRAM**. For scale, Strata reports 63 (prose) / 87 (code) t/s on one R9700 with this file and its own engine (§6).
- **Quality** (Qwen card, BF16): SWE-bench Pro 62.5 vs 61.7 for the 27B, SWE-bench Multilingual 81.0 vs 73.8, DeepSWE 58.7 vs 42.2, Toolathlon 73.5 vs 67.1. Unsloth's quant table ([run guide](https://unsloth.ai/docs/models/qwen3.8-next)) gives mean KLD: UD-Q2_K_XL 0.225, UD-IQ3_XXS 0.165, UD-Q3_K_XL 0.107, UD-IQ4_XS 0.084, **UD-Q4_K_XL 0.047**.
**Fit in 64 GB VRAM** (non-PLE = total − 28.8 GB, the PLE left on SSD via `--lazy-mode`, §5):
| Quant | Total | Non-PLE (needs VRAM) | Result on 2x32 GB |
|---|---|---|---|
| UD-Q4_K_XL | 111.3 | **82.5** | ~20-24 GB of experts must live off-GPU. **Does not fit** |
| UD-IQ4_XS | 93.7 | 64.9 | ~6-9 GB off-GPU after KV and buffers |
| UD-Q3_K_XL | 90.0 | 61.2 | Borderline, a few GB off-GPU |
| UD-IQ3_XXS | 82.0 | 53.2 | **Fits**, ~10 GB headroom |
| UD-Q2_K_XL | 78.9 | 50.1 | **Fits**, ~13 GB headroom |
KV is small. Only the 12 QSA layers have one: 12 x 2 KV heads x 256 x 2 (K+V) x 2 B ≈ 24.6 KB/token f16, ~6.4 GB at 262144 tokens, ~3.4 GB at q8_0 (estimate; ignores indexer keys).
**llama.cpp support status:**
- Merged: model support [#27742](https://github.com/ggml-org/llama.cpp/pull/27742) (2026-08-27), MTP [#29761](https://github.com/ggml-org/llama.cpp/pull/29761) (2026-10-01), `-sm tensor` re-enabled [#28569](https://github.com/ggml-org/llama.cpp/pull/28569), many QSA fixes through 2026-10-27 (e.g. [#31243](https://github.com/ggml-org/llama.cpp/pull/31243)).
- **Open bugs that hit this setup:**
- [#29149](https://github.com/ggml-org/llama.cpp/issues/29149): **HIP livelock during load on an R9700**, with both UD-IQ3_XXS and UD-Q4_K_XL. The root-cause comment places it in the mmap upload path; "the non-mmap path stages through pinned buffers instead". Try `--load-mode none`.
- [#29811](https://github.com/ggml-org/llama.cpp/issues/29811) and [#30860](https://github.com/ggml-org/llama.cpp/issues/30860): MTP asserts at startup.
- [#30887](https://github.com/ggml-org/llama.cpp/issues/30887): the DFlash2 drafter crashes on the first prompt (R9700, ROCm).
- [#30815](https://github.com/ggml-org/llama.cpp/pull/30815): QSA chunking, still open.
Usable, but rough on HIP.
## 5. Offloading to DDR RAM and SSD
**What llama.cpp offers** (master `common/arg.cpp`):
- `-ot/--override-tensor <regex>=<buft>` places matching tensors, e.g. `blk\.[0-9]+\.ffn_.*_exps=CPU`. `-cmoe/--cpu-moe` puts all experts on the CPU. `-ncmoe/--n-cpu-moe N` puts the experts of the first N layers on the CPU. These are MoE-only, so they do nothing for the dense 27B (already noted in `docs/architecture.md`).
- `--moe-cache-mib N`: an LRU cache in VRAM for host-resident experts, merged in [#29887](https://github.com/ggml-org/llama.cpp/pull/29887) (2026-10-07). On Flash-Next Q4_0 the PR measured 4090 25.0 → 39.4 t/s and 5090 30.8 → 67.8 t/s, on an EPYC host with RAM for all experts. "Small batches only (<= 32 tokens)".
- `-lzm/--lazy-mode {on,auto,off}`: "read the rows of such tensors from disk on demand instead of keeping them resident (requires mmap)". `auto` applies to tensors over 4 GiB, which **covers the 28.8 GB PLE table by default** (renamed in [#27969](https://github.com/ggml-org/llama.cpp/pull/27969)). Per [#30772](https://github.com/ggml-org/llama.cpp/pull/30772) (closed unmerged), mapped PLE pages still accumulate in RSS and single-threaded `madvise` can bottleneck NVMe.
- `-lm/--load-mode {auto,none,mmap,…}`. With mmap, a tensor that fits nowhere is paged from disk implicitly.
- **Pipeline parallelism turns off as soon as any tensor override is used** (`!has_tensor_overrides()` in `llama-context.cpp`, as described in the open PR [#29963](https://github.com/ggml-org/llama.cpp/pull/29963)). So `-ot`/`-ncmoe` plus two GPUs also loses the layer-split prefill overlap.
**Numbers for this box:**
- **Dense 27B:** no useful offload. Every weight is read every token, and DDR4 is ~51 GB/s against ~440 GB/s VRAM. Each GB moved to RAM costs ~20-25 ms/token on the CPU (estimate). With two cards the Q8_K_XL fits in VRAM anyway. The only RAM tier that helps is `--cache-ram` (prompt cache), as recorded in [`qwen3.8-27b-throughput-tuning.md`](qwen3.8-27b-throughput-tuning.md).
- **Flash-Next UD-Q4_K_XL, if RAM were free (hypothetical):** ~22 GB of experts on CPU = 29 % of experts, ~0.43 GB read per token from DDR4 at ~40 GB/s ≈ 11 ms. The GPU part ≈ (4.8 + 1.07) / 440 ≈ 13 ms. Layers are sequential, so the parts add up: **~24 ms ≈ ~40 t/s**, more with `--moe-cache-mib` (estimate). Prefill uploads the host experts once per ubatch: 22 GB over PCIe 4.0 x16 at ~25 GB/s ≈ 0.9 s per ubatch, so ~18 s of transfer for a 40k-token prompt at `-ub 2048` (estimate).
- **Flash-Next UD-Q4_K_XL, on this box:** RAM is full (§1). Even with `--cache-ram` cut to near zero, 32 GB minus OS, OmniRoute (up to 10 GB) and qdrant leaves maybe ~15 GB (estimate), less than the ~22 GB needed. The rest is paged from the CPU-attached Gen4 x4 NVMe. At ~0.43 GB/token and an assumed 3-6 GB/s for ~1-3 MB expert reads, that is **~70-140 ms/token, ~6-12 t/s**. Prefill re-reads ~22 GB per ubatch from disk, so **minutes per 40k-token prompt**. The page cache will also compete with the swap that is already happening. An outside data point: an RX 9060 XT 16 GB with 64 GB DDR4-3200 got "8 t/s on short context. Longer context 3 t/s" on llama.cpp ROCm ([#29149 comment](https://github.com/ggml-org/llama.cpp/issues/29149)). **Not viable.**
- **PLE table on SSD (any Flash-Next quant):** 32 rows per token (#30772) of 90 B each (Strata `AMD_HIP.md`), each on its own 4 KB page, so ≤128 KB of random IO per token. At 50 t/s that is a few thousand IOPS, trivial. Prefill touches ~32 pages per token, ~1.3 M random reads for 40k tokens. On NVMe at high queue depth that is a few seconds, but single-threaded `madvise` (#30772) can make it much slower. Strata measured +22.5 % prefill with the table in RAM against direct reads (§6). Decode impact: low. Prefill: measure it.
## 6. Strata
**Identity.** [github.com/Niko1221/Strata](https://github.com/Niko1221/Strata): "Qwen3.8-Flash-Next on any consumer hardware: one-click install for Windows / Linux. Strata inference engine, OpenAI/Anthropic API on localhost". C++, **MIT** ([LICENSE](https://github.com/Niko1221/Strata/blob/main/LICENSE)). Its `docs/HOW_IT_WORKS.md` says it is "Built with parts of llama.cpp / ggml (MIT)". It supports Qwen3.8-Flash-Next (ISTA-DASLab GSQ-RCO quants recommended, Unsloth UD-IQ4_XS…Q8_0 supported), Qwen3.6-35B-A3B and DeepSeek V4.1-Flash. **There is no dense Qwen3.8-27B support**, so it cannot replace today's model.
**How it tiers** ([`docs/HOW_IT_WORKS.md`](https://github.com/Niko1221/Strata/blob/main/docs/HOW_IT_WORKS.md), [`docs/MODELS.md`](https://github.com/Niko1221/Strata/blob/main/docs/MODELS.md)):
- **VRAM:** attention/DeltaNet, routers, shared experts, output head, MTP, KV ("from 64K: only its most-read part, the rest streams from RAM"), plus an **expert cache** that fills the rest of VRAM with the most-used experts and adapts at runtime.
- **RAM:** "all 24,576 experts, pinned". The CPU computes cache misses in place, concurrently with the GPU (AVX-512/AVX2). `--pcie-frac` splits misses between PCIe fetch and CPU compute. On gfx12 it defaults to 0.15 after a measured +9.9 % to +25.6 % decode on an R9700 (`docs/AMD_HIP.md`).
- **SSD:** the 28.8 GB n-gram table, "read a few rows per token through the OS cache". From 0.2.7 it keeps the table in RAM when there is room for it plus 8 GiB.
- **Low-RAM mode:** "setup maps them from the model files and keeps in RAM only what the card does not hold … With a small card most experts come from the SSD and it is much slower".
- **Multi-GPU** ([`docs/MULTI_GPU.md`](https://github.com/Niko1221/Strata/blob/main/docs/MULTI_GPU.md)): pipeline (layer) split, "not tensor parallelism". "No NVLink or peer-to-peer access is needed, cards on x4 or x1 slots work". Each card caches the experts of its own layers, so two cards hold about twice as many experts. `docs/AMD_HIP.md` documents HIP layer split for gfx1201 pairs and adds "A split is worth it when no single card holds the model's experts".
**AMD/ROCm:** the R9700 (gfx1201) is "Maintainer-tested", with Linux `hip-rx9070` engine packages in every recent release ([releases](https://github.com/Niko1221/Strata/releases)).
**RAM requirement for the requested file:** `docs/MODELS.md` lists UD-Q4_K_XL as "111 GB download, 72 GiB experts, **RAM for all experts ~88 GB**". With less RAM it "keeps as many as fit and reads the rest from the SSD (much slower)".
**Claimed vs verifiable:**
| Claim | Source | Host RAM | Verifiable? |
|---|---|---|---|
| UD-Q4_K_XL on one R9700: 63 t/s prose, 87 code, 32K prompt 1,110-1,320 t/s | `docs/MODELS.md`, "engines before 0.1.45.3" | not stated there | Self-reported |
| IQ3_S on one R9700: 87.6-101.9 t/s across 1K-128K | [community R9700 report](https://github.com/Niko1221/Strata/blob/main/bench/results/2026-10-08-community-r9700-linux/README.md) | **~503 GiB**, 2x EPYC 9334 | Has per-request JSON and hashes. "Single GPU still uses CPU experts, system RAM and PCIe; these rates do not establish desktop-CPU speed or minimum RAM" |
| R9700 + 9070 XT, 47 GB RAM, Coder IQ1_M: 45.5 / 48.3 t/s decode | `docs/AMD_HIP.md` | 47 GB | Self-reported |
| "125B+ MoE on a single 12GB GPU", "25-38 tok/s" | [stratallm.org](https://stratallm.org/) | n/a | The site calls itself "Unofficial Community Documentation". Its speed figures contradict each other and link no method. Ignore it |
None of these numbers were rerun for this doc. The project says its benchmark protocol "is still a draft" (`docs/COMMUNITY_BENCHMARKS.md`).
**Maturity:** repo created 2026-09-24. ~4,690 commits, ~3,594 of them by the one maintainer. ~36k stars, 895 open issues, a release roughly daily (0.1.40 → 0.2.7 in three weeks). Installation is `curl … | sh`, an AppImage, or Docker (`docs/DOCKER.md`, not read). It is young, single-maintainer and churning, and it would need its own container and gateway wiring here.
**Verdict for this box:** not worth it now. The 32 GB of RAM defeats its core design (all experts pinned in RAM). Low-RAM mode on 2x32 GB VRAM would still read ~20 GB of experts from NVMe for UD-Q4_K_XL. It cannot run the dense 27B that this stack is tuned around. **Revisit if RAM goes to 96-128 GB** (the board takes 128 GB). On a big-RAM host its self-reported R9700 Flash-Next numbers (60-100 t/s) are well above llama.cpp's 27B, so it would be worth a real A/B then.
## 7. Recommendation
**Primary: keep `Qwen3.8-27B-UD-Q4_K_XL` + DFlash2 and run two single-GPU replicas, one per card.**
Why:
- (1) Layer split gives a dense model no decode gain.
- (2) Tensor split is experimental on ROCm, measured at 1.31x, crashes with DFlash2, and would run over a PCIe 3.0 x4 chipset link.
- (3) Q8_K_XL roughly halves decode.
- (4) Flash-Next at a usable quant does not fit, and RAM offload pages from NVMe.
- (5) The workload is 2 parallel agents. The log already shows decode falling to p10 15 t/s while the other slot prefills ([`qwen3.8-27b-throughput-tuning.md`](qwen3.8-27b-throughput-tuning.md)). Two replicas remove that interference and double each slot's context.
Per-instance flags (two services, identical except the device). Each is today's command with these changes:
```text
--device ROCm0 # ROCm1 for the second service; confirm names with --list-devices
--n-gpu-layers 999
--ctx-size 262144 --parallel 1 # full 262144 per agent instead of 131072
--flash-attn on --cache-type-k q8_0 --cache-type-v q8_0
--cache-ram 6144 # per instance; both together must stay well under free RAM (§1)
--spec-type draft-dflash -md /models/Qwen3.8-27B-DFlash2-Q4_K_M.gguf --spec-draft-n-max 7
--jinja
```
`GPU_MAX_HW_QUEUES=1` stays on both services (`docs/architecture.md#gpu-access`). Gateway: register both backends in OmniRoute and make sure a conversation stays on one backend (prompt-cache affinity). Otherwise every switch is a full re-prefill. Whether OmniRoute can balance two connections per conversation was not checked.
**Independently of the second card: lower `LLAMA_CACHE_RAM`.** 24 GiB on a 32 GB host with a 10 GB OmniRoute limit oversubscribes RAM.
**Alternatives to A/B, in order:**
1. **Single fast stream:** Q4_K_XL with `-sm tensor -ts 1,1 -fa on --fit off --ctx-size <set by hand> --spec-type draft-mtp --spec-draft-n-max 2` (the built-in MTP head; DFlash2 crashes with `tensor`). Estimate ~34 t/s no spec, ~50 with MTP if stable. Watch for #27122-style lockups.
2. **Quality:** Q8_K_XL (or UD-Q6_K_XL at 25.3 GB) with `-sm layer`, DFlash2 placed with `-devd`, ~25 t/s estimated. Only if Q4 quality is shown to fail on your tasks.
3. **Flash-Next experiment:** UD-IQ3_XXS or UD-Q2_K_XL, all in VRAM: `-sm layer -ngl 999 --lazy-mode auto --load-mode none -fa on -c 131072`, MTP via `-md MTP/mtp-Qwen3.8-Flash-Next-shared-Q8_0.gguf --spec-type draft-mtp` once #29811/#30860 are fixed. Judge it on your own agent tasks. The KLD at these sizes is 3.5-5x that of UD-Q4_K_XL.
## 8. What to benchmark once the card arrives
1. **Topology:** `sudo lspci -vv` LnkSta for both cards (expect 16 GT/s x16 and 8 GT/s x4). Run `rocm-smi --showtopo` and `rocm-bandwidth-test` for host↔GPU and GPU↔GPU (does P2P work through the chipset?). Check the PSU and the case slot fit first.
2. **Per-card baseline:** `llama-bench -m Qwen3.8-27B-UD-Q4_K_XL.gguf -dev ROCm0` and then `-dev ROCm1`, `-fa 1 -ctk q8_0 -ctv q8_0 -p 512,8192 -n 128 -d 0,32768`. Decode should match. Load time and prompt-cache restores are where the x4 link shows.
3. **Split modes:** the same bench with `-sm layer` vs `-sm tensor -ts 1,1` for Q4_K_XL and Q8_K_XL. Compare tg against #28863's 1.31x and pp against layer.
4. **Server replay** (fixed ~40k and ~2k prompts, production temperature, concurrency 1 and 2) for: (a) two replicas, (b) one `-sm layer --parallel 2`, (c) `-sm tensor` + MTP. Record `timings.predicted_per_second`, `prompt_per_second`, draft acceptance, and the p10 decode under concurrency.
5. **Correctness:** a needle test at ~6-10k and ~60-90k tokens on each config, because #30884 reproduces on 2x R9700 with `--tensor-split`. Also check that DFlash2 output under `-sm layer` matches single-card output on a fixed prompt.
6. **RAM:** `free -h` and `vmstat 5` after an hour of agent traffic, before and after lowering `--cache-ram`. Swap-in should be ~0.
7. **Optional Flash-Next:** that UD-IQ3_XXS/UD-Q2_K_XL loads at all on HIP (#29149, try `--load-mode none`), tg/pp at 4k/32k, and prefill time with the PLE table on NVMe.
## Open questions
- Does OmniRoute support two backends with sticky routing per conversation?
- Does the `ghcr.io/ggml-org/llama.cpp:server-rocm` image build with `GGML_HIP_RCCL`? It only matters for `-sm tensor`.
- Does ROCm P2P work between a CPU-attached slot and a chipset-attached slot on B550? It is unverified, and the answer probably does not matter outside `tensor` mode.
- Is there a B550/X570 board with CPU x8/x8 bifurcation that fits this chassis? It would give both cards Gen4 x8. It is only worth it if step 3 shows `tensor` scaling well. No specific board was checked.
## Sources
- Hugging Face: [unsloth/Qwen3.8-27B-GGUF tree](https://huggingface.co/api/models/unsloth/Qwen3.8-27B-GGUF/tree/main), [its config.json](https://huggingface.co/unsloth/Qwen3.8-27B-GGUF/raw/main/config.json), [UD-Q8_K_XL file](https://huggingface.co/unsloth/Qwen3.8-27B-GGUF?show_file_info=Qwen3.8-27B-UD-Q8_K_XL.gguf), [unsloth/Qwen3.8-Flash-Next-GGUF](https://huggingface.co/unsloth/Qwen3.8-Flash-Next-GGUF) and its [tree](https://huggingface.co/api/models/unsloth/Qwen3.8-Flash-Next-GGUF/tree/main?recursive=true), [UD-Q4_K_XL shard 1](https://huggingface.co/unsloth/Qwen3.8-Flash-Next-GGUF?show_file_info=UD-Q4_K_XL%2FQwen3.8-Flash-Next-UD-Q4_K_XL-00001-of-00004.gguf), [z-lab DFlash2 GGUF](https://huggingface.co/z-lab/Qwen3.8-27B-DFlash2-GGUF), [incoai Flash-Next DFlash2 GGUF](https://huggingface.co/incoai/Qwen3.8-Flash-Next-DFlash2-GGUF). Tensor sizes come from parsing the GGUF headers (range requests) with a throwaway script.
- Unsloth: [Qwen3.8-Flash-Next run guide](https://unsloth.ai/docs/models/qwen3.8-next) (sizes, KLD table, MTP flags).
- llama.cpp master (fetched 2026-10-29): [`docs/multi-gpu.md`](https://github.com/ggml-org/llama.cpp/blob/master/docs/multi-gpu.md), [`common/arg.cpp`](https://github.com/ggml-org/llama.cpp/blob/master/common/arg.cpp), [`src/llama-arch.cpp`](https://github.com/ggml-org/llama.cpp/blob/master/src/llama-arch.cpp), [`src/llama-context.cpp`](https://github.com/ggml-org/llama.cpp/blob/master/src/llama-context.cpp). PRs: [#19378](https://github.com/ggml-org/llama.cpp/pull/19378), [#23792](https://github.com/ggml-org/llama.cpp/pull/23792), [#27342](https://github.com/ggml-org/llama.cpp/pull/27342), [#27742](https://github.com/ggml-org/llama.cpp/pull/27742), [#27825](https://github.com/ggml-org/llama.cpp/pull/27825), [#27858](https://github.com/ggml-org/llama.cpp/pull/27858), [#27861](https://github.com/ggml-org/llama.cpp/pull/27861), [#27969](https://github.com/ggml-org/llama.cpp/pull/27969), [#28569](https://github.com/ggml-org/llama.cpp/pull/28569), [#29761](https://github.com/ggml-org/llama.cpp/pull/29761), [#29887](https://github.com/ggml-org/llama.cpp/pull/29887), [#29963](https://github.com/ggml-org/llama.cpp/pull/29963), [#30772](https://github.com/ggml-org/llama.cpp/pull/30772), [#30815](https://github.com/ggml-org/llama.cpp/pull/30815), [#31243](https://github.com/ggml-org/llama.cpp/pull/31243). Issues: [#23567](https://github.com/ggml-org/llama.cpp/issues/23567), [#27122](https://github.com/ggml-org/llama.cpp/issues/27122), [#27829](https://github.com/ggml-org/llama.cpp/issues/27829), [#28582](https://github.com/ggml-org/llama.cpp/issues/28582), [#28777](https://github.com/ggml-org/llama.cpp/issues/28777), [#28863](https://github.com/ggml-org/llama.cpp/issues/28863), [#29149](https://github.com/ggml-org/llama.cpp/issues/29149), [#29811](https://github.com/ggml-org/llama.cpp/issues/29811), [#30860](https://github.com/ggml-org/llama.cpp/issues/30860), [#30884](https://github.com/ggml-org/llama.cpp/issues/30884), [#30887](https://github.com/ggml-org/llama.cpp/issues/30887).
- Hardware: [ASRock B550M Pro4 manual](https://download.asrock.com/Manual/B550M%20Pro4.pdf) (asrock.com's spec page returned no content to the fetcher), [ComputerBase B550](https://www.computerbase.de/2020-06/amd-b550-chipsatz-mainboard-test/), [Guru3D B550 PCIe](https://www.guru3d.com/review/amd-ryzen-3-3100-and-3300x-review/page-3/), [Puget R9700](https://www.pugetsystems.com/parts/Video-Card/AMD-Radeon-AI-PRO-R9700-32GB-16908/), [Tom's Hardware R9700](https://www.tomshardware.com/pc-components/gpus/amd-launches-radeon-ai-pro-r9700-to-challenge-nvidias-ai-market-dominance).
- Strata: [repo](https://github.com/Niko1221/Strata) (README, LICENSE, `docs/HOW_IT_WORKS.md`, `MODELS.md`, `MULTI_GPU.md`, `SECOND_GPU.md`, `AMD_HIP.md`, `COMMUNITY_BENCHMARKS.md`, read via the GitHub API on 2026-10-29), [releases](https://github.com/Niko1221/Strata/releases), [stratallm.org](https://stratallm.org/).
- This repo: `docker-compose.yml`, `.env.example`, `docs/architecture.md`, [`qwen3.8-27b-throughput-tuning.md`](qwen3.8-27b-throughput-tuning.md), [`qwen3.8-27b-quant.md`](qwen3.8-27b-quant.md), [`500k-context-2-parallel-agents-gpu-roadmap.md`](500k-context-2-parallel-agents-gpu-roadmap.md).