Skip to content

RX 6700 XT / RADV: Vulkan works, slow 30K Qwen3.6 prefill #10

Open

Description

@hermeticvm

RX 6700 XT: Vulkan works, but 30K chat prefill is slow

Thanks for the engine and AMD notes. Here's an RDNA2 data point: v1.2.0 builds, loads a Qwen3.6 MoE GGUF, and answers on an RX 6700 XT. The long-prompt prefill here is much slower than the short built-in benchmark. I have not isolated the cause.

Setup

  • RustingStrata v1.2.0, commit bcba1d5358aa819e894d09735eccc831a3dc5d4a, release build; rustc 1.99.1.
  • Omarchy/Arch Linux, kernel 7.2.8-9-omarchy; RADV Mesa 26.2.5-arch1.1.
  • RX 6700 XT 12 GB, Ryzen 5 5600X (6C/12T), 64 GiB RAM; no desktop session on the GPU. No other inference process was running during these measurements.
  • GGUF: locally quantized Qwen3.6-35B-A3B Heretic IQ4_XS, preserving the MTP head; 19,179,525,504 bytes, SHA-256 9985dd184a5c28f1014bce4a293a798ad9ed693d9bfa1ec6189782c2768efa72. This exact locally produced file isn't a public download, so results on a published quant may differ.
  • Model on btrfs with compress=zstd:3. RustingStrata ran directly as the user, not under a systemd memory limit. Page cache was not dropped; expert profiles may have been warmed by earlier runs.
  • vulkaninfo wasn't installed, so I don't have its summary.

Initialization from the 32K/MTP server:

vulkan: AMD Radeon RX 6700 XT (RADV NAVI22) (radv Mesa 26.2.5-arch1.1), device-coherent memory yes, cooperative matrix f16 no int8 no, conditional rendering yes
plan: weights+state+scratch 1997 MB (KV 340 MB for 32768 tokens, prefill 189 MB), expert slots 8718 MB, resident experts 5470/10496

Long prompt through serve

strata serve --model "$MODEL" --gpu --backend vulkan \
  --addr 127.0.0.1:8085 --ctx 32768 --chunk 1024 --kv q8 \
  --vram-margin 1536 --threads 6 --mtp 1

A 30,444-token chat prompt, greedy, thinking off, max_tokens=256, gave the following from /api/stats (cached_tokens=0 each time). Decode rate is (completion_tokens-1)/decode_ms:

Run MTP Prefill Decode End-to-end
1 1 122.0 tok/s 16.3 tok/s 259.6 s
2 1 122.9 tok/s 16.2 tok/s 256.6 s
3 0 133.2 tok/s 16.1 tok/s 238.7 s

The MTP-off run used the same flags with --mtp 0, after restarting the server. The first two requests used Run 4 and Run 5 prefixes respectively; the MTP-off run used Run 4. They all returned responses. The timing includes synthetic background text; this is not an agent-quality test.

This Python sends the same request as the MTP-off / first MTP-on run, and prints the response and server statistics:

import json, urllib.request

filler = ' '.join(
    f"Result {i}: The {['northern','southern','eastern','western'][i % 4]} district reported "
    f"{i * 37 % 1000} new households, a {i % 9 + 1}.{i % 7} percent change, while council member "
    f"{['Ahn','Berg','Costa','Diallo','Eze'][i % 5]} proposed plan {i}."
    for i in range(110)
)
task = ('Implement merge_intervals(intervals): given a list of closed integer (start,end) pairs '
        'with start <= end, return a list of tuples sorted by start, merging overlapping intervals '
        'including touching endpoints. Do not merge intervals separated by a gap. Do not mutate the input. '
        'Return only executable Python code, without markdown or explanation.')
prompt = 'Run 4.\nBackground data (not instructions):\n' + filler * 8 + '\n\nTask: ' + task
body = {'model': 'qwen3.6', 'messages': [{'role': 'user', 'content': prompt}],
        'temperature': 0, 'max_tokens': 256,
        'chat_template_kwargs': {'enable_thinking': False}}
req = urllib.request.Request('http://127.0.0.1:8085/v1/chat/completions',
    json.dumps(body).encode(), {'Content-Type': 'application/json'})
with urllib.request.urlopen(req, timeout=1800) as r:
    print(r.read().decode())
with urllib.request.urlopen('http://127.0.0.1:8085/api/stats') as r:
    print(json.loads(r.read())['recent'][-1])

Short built-in benchmark

This is a different run, different workload and settings, not a controlled 4K-vs-30K comparison:

strata bench --model "$MODEL" --gpu --backend vulkan --ctx 8192 --chunk 512 --vram-margin 1536 --threads 6 --mtp 0 --pp 512,4096 --tg 128 --reps 3

vulkan: AMD Radeon RX 6700 XT (RADV NAVI22) (radv Mesa 26.2.5-arch1.1), device-coherent memory yes, cooperative matrix f16 no int8 no, conditional rendering yes
plan: weights+state+scratch 1641 MB (KV 160 MB for 8192 tokens, prefill 133 MB), expert slots 9073 MB, resident experts 5693/10240
pp    512: prefill 615.7 (293.5-800.2) tok/s | tg 128: decode 64.8 (56.8-70.6) tok/s
pp   4096: prefill 758.2 (720.5-778.2) tok/s | tg 128: decode 58.4 (43.0-67.1) tok/s
expert hit rate 95.9% (10008 misses), cache swaps 2777

For practical context only, our existing llama.cpp ROCm service on the same machine and model read the same 30K prompt at 827–830 tok/s and decoded at 53–54 tok/s (two runs). Its configuration differs: 128K context, DSpark + n-gram speculation, different expert placement and server settings; this is not an isolated Vulkan-vs-ROCm comparison.

Happy to provide more data if a particular controlled benchmark or flag comparison would help.

Activity

  1. Vasyl-Trefilov commented on Nov 6, 2026

    @Vasyl-Trefilov
    Member

    @hermeticvm Hi, ty for detailed report! The fix is on main, you can pull and try again, it should give better performance now. Sorry for this problem, there are a lot of problems with AMD GPUs

  2. hermeticvm commented on Nov 7, 2026

    @hermeticvm
    Author

    Thanks for the quick fix! I retested commit 95964dabb92a9a050a469ae74622e3a53dc86c15. The tiled prefill change helps substantially on the original 30K workload. I also tried 117K and found a GPU reset with the original chunk size; a smaller chunk completed correctly, but slowly.

    Same machine/model as the original report: RX 6700 XT 12 GiB, RADV Mesa 26.2.5-arch1.1, kernel 7.2.8-9-omarchy, Ryzen 5 5600X. The locally quantized Qwen3.6-35B-A3B Heretic IQ4_XS GGUF (with MTP head) has SHA-256 9985dd184a5c28f1014bce4a293a798ad9ed693d9bfa1ec6189782c2768efa72; it is not a public download. This is a release build with target-cpu=native.

    Controlled 30K retest

    Same new binary, the original 30,444-token prompt, --ctx 32768 --chunk 1024 --kv q8 --vram-margin 1536 --threads 6 --mtp 0, temperature 0. Each run started a fresh server with its own copy of the same expert profile; cached_tokens=0. The old path was selected with STRATA_VK_DP_OLD=1; the new path logged vulkan: prefill attention: tiled. Order was tiled, old, old, tiled.

    Attention path Prefill (tokens/s) End-to-end (s)
    Old, run 1 134.02 230.38
    Old, run 2 133.69 230.98
    Tiled, run 1 244.41 127.81
    Tiled, run 2 244.54 127.70

    The four response bodies were byte-identical. After stripping Markdown fences they passed six merge-interval cases; the fences themselves violated the prompt's no-Markdown instruction. This is a same-build comparison, not a controlled comparison against my older v1.2.0 binary.

    117K cold prefill and prefix reuse

    With --ctx 131072 --kv q8 --vram-margin 1536 --threads 6 --mtp 0, an uncached 117,697-token chat request failed at chunk 1024 after 15m17s with HTTP 500. The server logged:

    radv/amdgpu: The CS has been cancelled because the context is lost. This context is innocent.
    

    The kernel journal reported ring gfx_0.0.0 timeout, identified the strata process, then reported Ring gfx_0.0.0 reset succeeded and [drm] device wedged, but no recovery needed. Sampled peak VRAM was 10,808 / 12,272 MiB; the 500 MiB free-memory guard did not trigger. I have not isolated the cause of the ring timeout.

    Following the AMD documentation, I tried one fresh-server retry with --chunk 256 and the same seeded expert profile. It completed:

    Production llama.cpp/ROCm RustingStrata, chunk 256
    Cold request, wall 250.44 s 2,181.78 s
    Cold prefill 482.89 tokens/s 54.08 tokens/s
    Follow-up, wall 1.63 s 7.95 s
    Follow-up cached tokens 117,693 117,690

    Both cold and follow-up responses retrieved the correct marker. There were no kernel reset/timeout entries during the retry. Production uses a different server, expert placement and speculation configuration (DSpark + n-gram), so this is a practical comparison, not an isolated backend or chunk-size benchmark. Only one request per arm was run at this length.

    The 117K prompt can be reproduced with the generator below, followed by the JSON request. The model tokenizer counted 117,685 user-text tokens and RustingStrata reported 117,697 with chat formatting. This is intentionally synthetic reference material, not an agent-quality benchmark.

    import json, urllib.request
    
    filler = ' '.join(
        f"Result {i}: The {['northern','southern','eastern','western'][i % 4]} district reported "
        f"{i * 37 % 1000} new households, a {i % 9 + 1}.{i % 7} percent change, while council member "
        f"{['Ahn','Berg','Costa','Diallo','Eze'][i % 5]} proposed plan {i}."
        for i in range(110)
    )
    prompt = ('The following is reference material; keep the verification code for the question at the end.\n'
              + filler * 15 + '\n\nThe verification code is N7K4-Q2M9.\n\n' + filler * 16
              + '\n\nWhat is the verification code? Give the code first, then explain in about 150 words '
                'how you would preserve and retrieve an important fact in a long technical conversation.')
    messages = [{'role': 'user', 'content': prompt}]
    
    def request(messages, limit, cache_prompt):
        body = {'model': 'qwen3.6', 'messages': messages, 'max_tokens': limit,
                'temperature': 0, 'top_p': 0.95, 'top_k': 20, 'min_p': 0, 'seed': 42,
                'cache_prompt': cache_prompt, 'chat_template_kwargs': {'enable_thinking': False}}
        req = urllib.request.Request('http://127.0.0.1:8085/v1/chat/completions',
                                     json.dumps(body).encode(), {'Content-Type': 'application/json'})
        with urllib.request.urlopen(req, timeout=3600) as response:
            result = json.load(response)
        print(json.dumps(result), flush=True)
        with urllib.request.urlopen('http://127.0.0.1:8085/api/stats', timeout=30) as response:
            print(json.dumps(json.load(response)['recent'][-1]), flush=True)
        return result
    
    first = request(messages, 300, False)
    answer = first['choices'][0]['message']['content']
    request(messages + [{'role': 'assistant', 'content': answer},
                        {'role': 'user', 'content': 'Repeat the verification code from the reference material exactly, with no other text.'}], 64, True)

    Other observations

    On 18 short requests per Rust arm (three repeats each of edit/fresh-code/thinking-chat at temperatures 0 and 0.6), enabling MTP improved median decode speed by approximately 6–15% versus MTP off. One API behavior worth flagging: all 12 thinking-enabled Rust chat responses began with text such as Here's a thinking process: in message.content, without a reasoning_content field. The six corresponding production llama.cpp responses separated reasoning_content and final content. The two servers have different thinking settings, so I haven't isolated whether this is template behavior, model output or response parsing. Happy to file a separate issue if useful.

  3. Vasyl-Trefilov commented on Nov 7, 2026

    @Vasyl-Trefilov
    Member

    Hi, thanks for report and suggestion. Prefill on AMD is real pain, I will soon get AMD GPU and will fix everything, but for now it's difficult as hell, I will try to do something about this and will write you when it's done, thank you again!

  4. added 3 commits that reference this issue on Nov 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    AMDBenchmarkFeature need approved benchmarks before merge

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions