Skip to content

rocm: Qwen3.6-35B-A3B GPU runtime — runs on Radeon 780M at ~14 tok/s, 5/5 top-1 parity#4

Open
marmyx77 wants to merge 10 commits into
andreaborio:mainfrom
marmyx77:rocm-qwen
Open

rocm: Qwen3.6-35B-A3B GPU runtime — runs on Radeon 780M at ~14 tok/s, 5/5 top-1 parity#4
marmyx77 wants to merge 10 commits into
andreaborio:mainfrom
marmyx77:rocm-qwen

Conversation

@marmyx77

Copy link
Copy Markdown

Ciao Andrea! Seguito della PR #3. Il tuo PREREG_QWEN36_35B_A3B dice che la
parity CUDA/ROCm servirebbe "~15-25 giorni più accesso all'hardware".
L'hardware ce l'ho — Qwen3.6-35B-A3B ora gira sul mio Radeon 780M (gfx1103,
ROCm 6.3.1) via il backend ROCm
, resident, con output coerente e parità
numerica col tuo riferimento scalare CPU.

Risultato misurato (780M, resident, 19.37 GiB in GTT)

path prefill generation
ds4 ROCm GPU (questa PR) 11.8–14.3 13.4–14.9 tok/s
ds4 CPU reference ~10 ~10–11.5
llama.cpp CPU (stesso artefatto) ~100 (pp64) 16.1 (tg64)

Batte già il tuo riferimento CPU scalare. Non batte ancora llama.cpp — atteso
per un v1 correctness-first (tutto F32, niente HIP-graph, MoE top-8 come due
top-4). I tuoi numeri Metal (58–66 t/s su M5 Pro) sono su ~2× la banda di
memoria e un set di kernel maturo.

Correttezza (gate M2, CPU-vs-GPU top-logprobs)

  • top-1 agreement 5/5 — la GPU sceglie lo stesso token della CPU a ogni step
  • cosine 0.9994–0.9999 sui logit top-k (il tuo gate Metal-vs-llama.cpp è
    0.99994)
  • diff logit max 0.5–1.7 su ~26 (sub-1%), puro ordine di accumulo f32
  • greedy 96-token identico al golden CPU per i primi ~40 token, poi diverge a un
    near-tie (comportamento upstream documentato)
  • tutti i 22 check di parità per-op (tests/test_qwen35_rocm.c) passano vs
    ds4_qwen_ref.c; il ricorrente Gated-DeltaNet combacia a ~1e-8

Cosa contiene

  1. rocm/ds4_rocm_qwen.cuh — 19 kernel HIP, port 1:1 di metal/qwen35.metal
    (simdgroup Metal → wave32 RDNA3, simd_sum/max/min → butterfly __shfl_xor;
    gli stride in byte degli args ricalcano ds4_metal.m) + i 20 entry point
    ds4_gpu_qwen35_*.
  2. MoE top-8 senza toccare i tuoi kernel sum6. Il core Q4_K ROCm è
    specializzato per il top-6 di DeepSeek (kernel sum6 fusi). Qwen instrada
    top-8 → lo eseguo come due metà top-4 + add (moe_split_selected_kernel
    • routed_moe_batch_two_half in ds4_rocm_moe_launch.cuh; il decode usa le
      viste half a offset 0/16). Il path DeepSeek (n_expert=6) è invariato.
  3. Wiring host (ds4.c). Guardie del forward/session/dispatch Qwen allargate
    da __APPLE__ a DS4_QWEN_GPU_BUILD. Nota: non esiste enum DS4_BACKEND_ROCM
    — il build ROCm gira come DS4_BACKEND_CUDA — quindi un helper
    ds4_backend_is_qwen_gpu() sostituisce i check backend==METAL in
    is_qwen35_metal / session-create / options_valid / resident-config. Il blocco
    forward da 1.353 righe è backend-neutro e compila invariato. Gate env
    DS4_QWEN_EXPERIMENTAL_ROCM=1; resident forzato (lo streaming SSD per Qwen
    non è portato — il payload sta in RAM sui 64 GB).
  4. tests/qwen/normalize_qwen36_gguf.py — riproduce l'artefatto benedetto
    dal sorgente Unsloth UD-Q4_K_S (Q6_K ffn_down_exps → Q4_K, output → Q8_0,
    pad-id + template). Inventario risultante f32=361 / q8_0=252 / q4_k=120 ==
    il tuo istogramma del prereg
    .
  5. Base: gli shim di build ROCm 6.3.1 + lo snapshot host della PR rocm: host memory snapshot + capability-gated adaptive cache planner (with ROCm 6.3 build fixes) #3 (inclusi
    qui perché senza non compila).

Stato di verifica onesto

Compila e gira sul 780M; i numeri e la parità vengono da questo hardware. Il
diagnostico --metal-graph-prompt-test è DeepSeek-only, quindi la parità è via
--dump-logprobs CPU vs GPU (JSON in tests/qwen/qwen36_rocm_cpu_parity_*.json).
Non ancora fatto: soak 8k/32k + ds4-bench CSV, throughput (HIP-graph, fused
gate/up+swiglu nel MoE a due metà, GQA-decode parallelo — ora è wired solo il
kernel GQA seriale), smoke del server su ROCm. Il server Qwen resta text-only
(niente tool-call) come da tuo runtime.

Dettagli completi in RESULT_QWEN_ROCM.md. Marco ti scrive anche a parte —
è più bello di una notifica GitHub :)

— Marco (con Claude), branch marmyx77:rocm-qwen

🤖 Generated with Claude Code

Marco Armellino added 10 commits July 15, 2026 22:33
…warp)

Without these the ROCm backend does not compile on ROCm 6.3.1 at all:
- HIPBLAS_V2 + CUDA_R_* -> HIP_R_*: hipBLAS 2.x only declares the
  hipDataType-based GemmEx/GemmStridedBatchedEx overloads behind HIPBLAS_V2;
  the deprecated hipblasDatatype_t ones no longer match.
- HIP_ENABLE_WARP_SYNC_BUILTINS: ROCm 6.3 gates __shfl_*_sync/__ballot_sync
  behind this macro.
- __syncwarp(...) -> wavefront-scoped fence: no HIP equivalent exists; on
  RDNA3 wave32 lanes execute in lockstep so reconvergence is implicit and
  the fence supplies the compiler ordering the call relies on.

Verified: ds4_rocm.o builds clean with hipcc 6.3.1, --offload-arch=gfx1100
(run on a Radeon 780M / gfx1103 via HSA_OVERRIDE_GFX_VERSION=11.0.0, since
rocWMMA 1.6.0 rejects gfx1103).
ds4_gpu_host_memory_snapshot was a stub returning 0 on ROCm, which kept the
adaptive expert-cache planner permanently unavailable there. Implement it
for Linux: /proc/meminfo (MemTotal, MemFree, Inactive(file), Cached),
/proc/self/status VmRSS, PSI /proc/pressure/memory for the pressure flags,
and hipMemGetInfo total as the working-set envelope (on UMA APUs the GTT
pool is the closest analogue of recommendedMaxWorkingSetSize). The Darwin
field mapping is documented inline.

Also store the planner's required floor and warn when the configured budget
is below it (full fail-closed enforcement on ROCm still to be wired); the
slab target stays a documented no-op (the ROCm cache is per-expert
cudaMalloc, no slab allocator).

Motivation, measured on a Radeon 780M / 64 GB / 48 GiB GTT running DeepSeek
V4 Flash q2 with SSD streaming: with a fixed thin free floor the cache peaks
after ~150-250 generated tokens and the kernel TTM evicts the GPU queue
buffers themselves —
  [drm:amdgpu_cs_ioctl] *ERROR* Not enough memory for command submission!
  amdgpu: Freeing queue vital buffer ..., queue evicted
— after which every HIP sync blocks forever with an idle GPU. A host-aware
floor prevents it: 5.8-6.6 GiB floors froze or ran 2.97 tok/s; a 12 GiB
floor ran 280-token soaks clean twice at 3.47 tok/s (best measured on that
box; the wired cache competes with the page cache of the 81 GB GGUF, so the
floor is a throughput optimum too, not just a safety line).
Any backend that can produce a live host-memory snapshot now gets the
adaptive budget; backends without one keep the legacy fixed-fraction
budget. Metal behavior is unchanged. This is the cross-backend step the
planner entry in FORK_NOTES was waiting on: ROCm UMA APUs need the
host-aware floor as much as Metal does (see the previous commit for the
queue-eviction failure it prevents).
Multi-model range registry, per-value readback event ring with host-side
waits (device-side hipStreamWaitEvent wedges HSA queues on this ROCm/APU
stack), pinned-staging upload mutex, adaptive free-floor reserve, small-n
prequantized q8 batch kernel, and the ds4_gpu_stream_expert_cache_stats
zero stub required to link ds4-bench on non-Metal backends.

Measured on Radeon 780M / 64GB / ROCm 6.3.1 (DeepSeek V4 Flash q2 SSD
streaming): clean 280-token soaks at 3.47 tok/s with the adaptive floor;
the event-ring/host-wait changes fix reproducible generation freezes.
Quoted -DDS4_BUILD_GIT_SHA values lose their quotes in recursive targets
(strix-halo/cuda re-enter make with CFLAGS on the command line, adding a
shell evaluation), breaking every ROCm/CUDA build with 'b60fbec8ddd7
undeclared'. A bare token survives any number of shell levels.
Port of metal/qwen35.metal to HIP (rocm/ds4_rocm_qwen.cuh): 19 device
kernels + the 20 ds4_gpu_qwen35_* entry points.  Metal simdgroups map to
RDNA3 wave32 (HSA_OVERRIDE_GFX_VERSION=11.0.0); simd_sum/max/min become
butterfly __shfl_xor reductions; byte-stride args mirror ds4_metal.m
exactly.  routed_moe_top8 delegates to the existing ROCm Q4_K MoE core
(ds4_gpu_routed_moe_one_tensor) as a resident single top-8 pass.

tests/test_qwen35_rocm.c validates every non-weight op against the scalar
reference ds4_qwen_ref.c: split_q_gate, sigmoid_mul (elements/rows), rope,
router (exact top-8 + weights), gqa decode/prefill, gated_delta_step
(kd=128 fast path and kd=64 generic), gated_delta_sequence_128.  All pass
on the Radeon 780M — the core GDN recurrence matches to ~1e-8 relative.

Weight-reading ops (conv, rmsnorm_gated, controls, embedding) still need a
model-map-backed test and host wiring (Fase 3).

+ tests/qwen/normalize_qwen36_gguf.py: reproduces the blessed artifact
(Q6_K ffn_down_exps -> Q4_K, output.weight -> Q8_0, pad id + chat template)
via ggml reference quantizers.
conv_step, conv_sequence, rmsnorm_gated, gated_delta_controls, and
dequant_embedding_q8_0 now validated against ds4_qwen_ref.c by registering
a host weight buffer as the model map (UMA maps it device-side).  All 22
per-op checks pass on the Radeon 780M; every kernel matches the scalar
reference to <=1e-7 relative (GDN recurrence to ~1e-8).
…rt halves

Host wiring (ds4.c): widen the Qwen GPU forward/session/dispatch guards from
__APPLE__ to DS4_QWEN_GPU_BUILD (= __APPLE__ || ROCm build).  There is no
DS4_BACKEND_ROCM enum — the ROCm build runs under DS4_BACKEND_CUDA — so a new
ds4_backend_is_qwen_gpu() helper replaces the backend==METAL checks in
is_qwen35_metal / session-create / options_valid / resident-config.  Engine
open gates on DS4_QWEN_EXPERIMENTAL_ROCM=1 and forces resident (Qwen SSD
streaming is not ported; the 19.4 GiB payload fits on the 64 GB box).  The
1353-line forward block is backend-neutral (calls only ds4_gpu_*) and compiles
unchanged.

MoE (ds4_rocm_moe_launch.cuh): the shared ROCm Q4_K MoE core is specialized for
DeepSeek's top-6 (fused sum6 kernels), but Qwen routes top-8.  Run 8 as two
top-4 halves + add, without touching the sum6 path: routed_moe_batch_tensor
splits [n_token][8] into two [n_token][4] (new moe_split_selected_kernel) and
adds the partials; the decode entry point uses the offset-0/16 half views.
DeepSeek (n_expert=6) is unaffected.

Measured on the Radeon 780M, resident: Qwen3.6-35B-A3B runs end-to-end on the
GPU, coherent output, prefill 11.75 / generation 14.93 tok/s (vs ~10-11 CPU).
…1 parity

End-to-end on the Radeon 780M (resident, 19.37 GiB): generation 13.4-14.9 tok/s
(vs ~10-11 CPU reference), coherent output.  CPU-vs-GPU parity: top-1 agreement
5/5, cosine 0.9994-0.9999 on top-k logits (evidence JSON attached).  All 22
per-op kernel checks pass.  Details, method, and next steps in RESULT_QWEN_ROCM.md.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant