Repository navigation
[job] Qwen3.8-Flash-Next on Strix Halo: prefill and TTFT #144
Copy link
Copy link
Open
Labels
jobA piece of work anyone can pick up. Shows on atlasinference.dev/#jobsA piece of work anyone can pick up. Shows on atlasinference.dev/#jobs
Description
Status (updated 2026-10-01 19:30Z)
State: step 3 (prefix caching) is moved first, because the agentic data says it's the bigger lever. Steps 1–2 (cold prefill rate) still stand.
- Measured on the Windows 1007-turn replay, Strix Halo Windows: Flash-Next port on main + MoE prefill transpose (2k TTFT ~2x) (supersedes #54) #80 tree, no prefix cache: TTFT median 64.5 s / p90 112 s vs 3.9 s of decode, so 92% of agentic wall time is re-prefill.
- Draft strix-windows: Flash-Next launchers + prefix caching on by default (stacked on #80) #149: Windows Flash-Next recipe with prefix caching on, 4 Marconi slots (~0.45 GB). It fits by shrinking KV from ~97k to ~77k tokens. GB10 reference: [job] Qwen3.8-Flash-Next: fast multi-turn TTFT with prefix caching #69, 0.74 s warm vs 18.1 s cold at 27k.
- Next: strix-windows: Flash-Next launchers + prefix caching on by default (stacked on #80) #149's winbox gates (poisoning gate → ST-995 → replay) right after Strix Halo Windows: Flash-Next port on main + MoE prefill transpose (2k TTFT ~2x) (supersedes #54) #80 merges. Then step 1's prefill profile at 2k/8k/32k, for first-turn and long-document TTFT.
Why: long prompts are the agentic workload, and time-to-first-token on Strix Halo is dominated by prefill. Measured on winbox (Windows 11, ROCm 10, VGM 32 GB), C=1:
| 2k first token | ≈ prefill rate | fingerprint | |
|---|---|---|---|
| before #80's MoE transpose | — | 70–75 tok/s at 2k and 7k | fnext-win-r2, #57 status |
| #80 | 9.56–9.66 s | ≈ 210 tok/s | #80 bf06515a7 / c6efb5f60, boot-7g recipe, 2026-09-28 |
The Windows recipe also runs with prefix caching off (--ssm-cache-slots 0), so every agentic turn re-prefills its full history.
Done bar (serve-amd.ps1 -Model qwen3.8-flash-next and the Linux equivalent):
- prefill ≥ 1,000 tok/s at 8k and at 32k, i.e. TTFT ≤ 8 s at 8k and ≤ 33 s at 32k;
- ppl, needle and kl_drift unchanged, plus ST-995 and the 1007-turn replay (AGENTS.md platform rule);
- warm multi-turn TTFT: prefix caching with SSM snapshots on by default, inside the Windows commit wall (~84.5 GiB at VGM 32 GB).
Plan, each step gated by the one before:
- Measure (no code): prefill profile at 2k / 8k / 32k. Split out the MoE grouped GEMM, the transpose, dequant, attention/QSA, the GDN chunk kernels and mHC. Report achieved TFLOP/s against the WMMA peak.
- Move the MoE grouped GEMM and the dense projections onto WMMA tiles tuned for gfx1151 wave32. Keep a persisted per-shape plan cache for any BF16 shapes that go through hipBLASLt, so there's no online heuristic search.
- Prefix caching: size the Marconi SSM snapshot pool to fit beside the KV budget and turn it on in the recipe. Measure warm TTFT on the agentic replay.
Related: #74 (GB10 long-context prefill), #72 (Strix 27B prefill, the w4a16_gemm_t_m128 finding), #69 (GB10 warm TTFT).
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
jobA piece of work anyone can pick up. Shows on atlasinference.dev/#jobsA piece of work anyone can pick up. Shows on atlasinference.dev/#jobs
Activity
AzeezIsh commentedon Oct 1, 2026
A measurement for step 3, from the 1007-turn Windows replay on #80 (winbox,
577F510D, no prefix cache): across the last 400 turns, TTFT median is 64.5 s (p90 112 s) and decode is 3.9 s. On the agentic workload, 92% of wall time is re-prefill.So step 3 (prefix caching) is the larger lever on that workload than steps 1–2. The tail-snapshot restore turns a turn into "new tokens only": on GB10 #69 measured a 27k warm turn at 0.74 s vs 18.1 s cold. Cold prefill rate still matters for first turns and the
ST/needle legs.AzeezIsh commentedon Oct 2, 2026
Same root cause and fix as the 27B prefill comment on #72 (#72). For Flash-Next specifically, the routed-expert kernels are the worst offenders:
moe_w4a16_fused_gate_up_tandmoe_w4a16_grouped_gemm_ptrtable_trun ~687 instructions per 32-K step for 16 WMMA (43/WMMA).AzeezIsh commentedon Oct 2, 2026
The F16-operand GEMM twins discussed above are now up as draft #155: dense m128, routed MoE and w8a16 FP8, plus engine wiring. 914/914 tests pass. It isn't merge-ready until the GPU bench (strix job 144, also built on winbox) shows the speedup and accuracy, and then the AMD gates.
AzeezIsh commentedon Oct 4, 2026
Step 1 done: Windows Flash-Next prefill attribution.
Source: Claude's traced prefill on winbox, 2026-10-02 23:07Z.
93a99cc094(the Strix Halo Windows: Flash-Next port on main + MoE prefill transpose (2k TTFT ~2x) (supersedes #54) #80 landing tree), spark577F510D, Flash-Next NVFP4.SERIAL=1, thinking off,ATLAS_TRACE_LAUNCH=1+_SYNC=1.moe_w4a16_*_t_k64)fp8_gemm_t_m128,fp8_fp8_gemm_t_m128)w4a16_gemm, N=512, K=2560)qsa_prefill_attn_g66–71 ms per layer-chunkdense_gemm_bf16_pipelined: GDN out_proj, HC down, PLE)inferspark_prefill*)Not in the GPU sum:
qsa_block_poollaunches. QSA ingest copies the raw-key column withcopy_d2d_2d_async, and on everyatlas_scaletarget that falls back to onecuMemcpyDtoDAsyncper row: 2,048 per attention layer per chunk, 111,750[d2d]ops in the trace. SYNC inflates this, so an unsynced trace is running now to price it.cuBLASLt BF16 GEMM live … hipBLASLt shim), which the launch trace doesn't record.Root causes found in code:
weight_loader/qwen35/load_layers/attention_arms.rscallspredequant_for_prefillunconditionally, so prefill takes the FP8 arms ofcache_skip_qkv.rs/paged_qkv.rs/paged_oproj.rs. The GDN sibling (linear_attn_arms.rs:522-528) already skips this onatlas_hip, because HIP has no fast FP8 GEMM. The NVFP4 arm just below (w4a16_gemm_t_m128) ran the 12288×2560 q shape at 21.6 TF/s in strix job 146, about 6 ms. Today's FP8 arm takes 76 ms, or 149 ms in chunk 0, where activations are FP8-quantized too. Skipping the copies also frees ~0.6 GB._t_k64kernels.w4a16_gemmgathers single bytes across 64 rows with two barriers per 16-K step, and its time jumps 9× between M=1109 and M=2048.cudaMemcpy2DAsync. The per-row loop is only needed for SCALE.Plan (Devin): a HIP prefill train on winbox, built from local branches. Nothing gets pushed until the platform suite passes.
Measurements:
Two checks on strix today: the router twin's bit identity (microbench) first, and the same trace-based attribution for the 27B (#72).
AzeezIsh commentedon Oct 4, 2026
HIP prefill train: A/B result on winbox. Prefill TTFT is −47% to −51%. The byte-identical half is −10% on its own.
The four commits are local and unpushed (AMD rule) until the suite passes:
w4a16_gemm_s64.Fingerprint: winbox plan p6, Flash-Next NVFP4, p4's PF recipe (util 0.90, seq 32768, prefill 2048, kv bf16, batch 1, MTP 1,
PREFIX_CACHE=0). 3 rotated rounds, TTFT from the server'sDoneline,max_tokens 16. Trees: P011639c39= main + #152 + #153; P24205250e= P0 + C1 + C3; P4239b9a79= P2 + C2 + #155 + C4.Engagement, from a SYNC-traced P4 prefill. Same 9,302-token prompt as the original attribution:
[d2d]ops 111,750 → 126.Per-launch times:
_t_k64→_t_f16a)_t_k64→_t_f16a)w4a16_gemm→w4a16_gemm_s64)fp8_*→w4a16_gemm_t_m128)No
fp8_gemm_t_m128/fp8_fp8_gemm_t_m128launches remain.Where P4's 14.6 s goes, i.e. the next levers:
qsa_prefill_attn_g61–65 ms per 2k chunk-layer; GB10's TC2 kernel has no gfx1151 port).ATLAS_GDN_PIPEis unmeasured on gfx1151).dense_gemm_bf16_pipelined: 2.33 s. That's the GDN out_proj at 8.4 TF/s (7.6 ms for 64 GFLOP) plus HC down; it's the clearest remaining inefficiency.Chunk 0 also still pays ~4 s of host time on the first request after boot (PLE fault-in).
AzeezIsh commentedon Oct 9, 2026
winbox memory was the blocker. Now unblocked (2026-10-09)
What was wrong
hipInforeported ~77 GB.crates/atlas-kernels/hip/libcuda_hip_shim.cpp, writes past roughly 63 GB faulted the context (hipErrorLaunchFailure).ATLAS_UMA_COMMIT_LIMIT_GB = "96"(scripts/strix-windows/win_serve_flashnext_nvfp4.ps1). When that variable is set, the shim reports it as total and free, so preflight loggedGPU 0: 96.0 GB total, 96.0 GB freewithout measuring anything. That number was never real on this box.What it caused
Server live.atlas-p5\out\serve-fnext-*.log,atlas-p5c\out\serve-fnext-{w2-*,p10,p10r3}.log.serve-fnext-fnext-20261006-100144.log, ended inhipErrorLaunchFailure.What changed
hipInforeportstotalGlobalMem: 107.92 GB.Before the next measurement
ATLAS_UMA_DRIVER_RESERVE_GBdefault were both measured under the old 32 GB carve-out.ATLAS_UMA_COMMIT_LIMIT_GBin the launchers from that measurement, not a hand-picked 96 (it may now under-report).Box notes:
AzeezIsh commentedon Oct 28, 2026
NVFP4 prefill GEMM on gfx1151: one LDS buffer plus B-fragment reuse gives +15–28%, bit-identical. strix, isolated kernel bench.
Why. rocprofv3 on
w4a16_gemm_t_m128_f16a(strix s40, 27B 4k prefill) reads:So it's occupancy-bound, not bandwidth-bound. Its double-buffered tiles take 41 KB of LDS, and only ~3 CTAs fit a WGP's 128 KB.
Change (branch
perf/hip-w4a16-f16a-single-lds,89a26b846+d6057db77):VGPRs drop from 256 (+2 spilled, 12 B scratch) to 248 with no spill.
Measured (strix s45 / s45b): one process, all variants interleaved in rotating order, 15 rounds × 5 reps, medians. The output hash is identical to main on every shape.
What didn't work (recorded per the neutral rule):
w8a16_gemm_n_m128is more accurate (relL2 vs fp64: 2.3e-3 → 1.6e-3) but 5–15% slower. Not proposed.Method note: separate-process GEMM benches on strix drifted 6–50% between runs (DPM clocks,
autolevel). Only in-process interleaved medians are usable for kernel A/Bs on this box.Now running: end-to-end prefill A/B of
test/prefill-2against main. It contains this change plus the same single-LDS change for the routed-MoE F16 twins (perf/hip-moe-f16a-single-lds9be597d00, 31 → ~15.6 KB). 27B on strix s46, Flash-Next on framework f61. Arms are interleaved ×3, and greedy and logprob probes must be identical.AzeezIsh commentedon Oct 28, 2026
Flash-Next NVFP4 prefill on gfx1151 Linux: profile, plus four bit-identical changes that didn't move it. Framework, main
8863dc5e5.Where the time goes (f60, SYNC launch trace, cold 7,380-token prefill, 8.77 s summed GPU, 10.47 s wall):
qsa_prefill_attn_g(QSA attention, 12 layers)moe_w4a16_fused_gate_up_t_f16a+grouped_gemm_ptrtable_t_f16a(routed MoE)delta_h_vfused,recompute_wu,fwd_o)w4a16_gemm_t_m128,w4a16_gemm_t)hc_pre_stage_bf16,hc_post,hc_pre_mix)End-to-end A/Bs, interleaved ×3 at ~1.9k / ~7.4k / ~12.5k. All arms identical on greedy and logprob probes unless noted.
test/prefill-2)ATLAS_F16_M128_MIN_NK=0, so attention projections use the F16 twin776320110, the k64 kernels' fix ported)ATLAS_MOE_PREFILL_EXACT_TILES=0(no per-layer D2H offsets sync)So for Flash-Next neither the MoE GEMM's occupancy nor the per-layer sync is what limits prefill. Steady state stays ~717 tok/s at 7.4k, against Gufo's 1,721 at 8k depth. Wall exceeds summed GPU time by ~1.5 s at 7.4k. f64 is an unsynced launch trace to attribute that host time.
MTP past the QSA bound is fixed by default. Branch
perf/hip-qsa-verify-active-default(88cffac78) withATLAS_QSA_VERIFY_ACTIVEunset (f58): MTP1 decode 18.75 tok/s at 8k and 17.64 at 32k cached depth, 0 "mtp declined: qsa_active" lines. On main the same rows decode at 12.1 tok/s. The branch is ready; the owner pushes it.AzeezIsh commentedon Oct 28, 2026
Flash-Next prefill is GPU-bound too: the "1.5 s host gap" was a measurement artifact (framework f65 + f64).
f65: rocprofv3 kernel + memcpy trace (no counters, unserialized) of a warmed 6,649-token prefill on
test/prefill-7. Span 9.53 s, GPU busy 9.37 s, idle 158 ms (1.7%): 89 ms is one H2D copy pair at request start, and the rest is <50 µs launch gaps. The earlier "wall 10.25 s vs 8.77 s GPU" comparison set an unsynced wall against SYNC-mode per-kernel sums, which undercount. Neither the per-layer MoE offsets sync (f63 neutral) nor host launch overhead costs anything measurable.f64 (unsynced launch trace) and the f60 copyBuffer storm: the 298k
__amd_rocclr_copyBuffer(0.8 s) andmoe_transpose_u8_batchedin f60's rocprof are boot-time weight transposes. They appear 0 times in any request.True kernel durations, warmed 6,649-token prefill:
qsa_prefill_attn_gmoe_w4a16_fused_gate_up_t_f16a+grouped_gemm_ptrtable_t_f16aCijk_*, GDN projections + HC)w4a16_gemm_t_m128+w4a16_gemm_tNext lever (in flight): HIP never had a tensor-core QSA kernel.
try_kernel("qsa_attn_tc2")returned 0 and prefill always ran scalar_g. Branchperf/hip-qsa-attn-tc2881c0b550adds a native HIPqsa_attn_tc2.cu. It's strix-hip's validatedinferspark_prefill_paged_64WMMA body with gb10 tc2's head tiling and gathered keys, and keeps the same entry name, arguments and grid, soATLAS_QSA_ATTN_TC2=1selects it with no Rust change. s65 (strix: relL2 + timing against_g) and f66 (framework: end-to-end A/B + probes) are queued. Not bit-identical, so the gate is ppl + needles, as for gb10 tc2.AzeezIsh commentedon Oct 28, 2026
HIP tensor-core QSA attention: −10% / −12% Flash-Next prefill at 7.4k / 12.5k (framework f66). Branch
perf/hip-qsa-attn-tc2881c0b550(off maindb442e2d4), one build, binary4f10b99cb042.mainarm =ATLAS_QSA_ATTN_TC2unset (scalarqsa_prefill_attn_g, the same path as main);test=ATLAS_QSA_ATTN_TC2=1. Cold TTFT, interleaved ×3._gr1 / r2 / r3Faults: 0 in all six serves.
Numerics: the probes are identical below the bound (short / code / 2k; 2k mean |Δlogprob| 0.011). The 10k probe diverges at char 218 (mean |Δlogprob| 0.145). That's expected for any non-bit-identical change above the bound: QSA top-k selection is chaotic, and gb10's tc2 diverges the same way. As there, the gate is ppl + needles, not greedy identity. f67 is running:
scripts/ppl.pydense region (6k-char windows, must match) and above-bound (44k-char, ~11k-token windows);The reference is gb10 tc2: dense 0.000%, above-bound −0.019%, needles 12/12. If it holds, gfx1151's default can flip (it's still opt-in pending exactly this measurement:
qsa_attn_tc2_decision(..., gfx=true)→ off).AzeezIsh commentedon Oct 28, 2026
HIP QSA tc2 passes gb10 tc2's gate (ppl + needles), framework f67.
perf/hip-qsa-attn-tc2881c0b550, binary4f10b99cb042. Ref arm =ATLAS_QSA_ATTN_TC2unset (scalar_g); test ==1.scripts/ppl.pyoverdocs/._gppl.pyprintsFAILfor the above-bound region. It's the per-token clause, not the ppl ratio (1.0008 against a 1.02 limit): one token moved 5.37 nats; mean |Δlp| is 0.090. Any non-bit-identical change past the bound trips this, because QSA top-k selection is chaotic. It's why gb10 tc2 was gated (and defaulted on) by ppl + needles rather than per-token or greedy identity. The engine is deterministic run-to-run, so this isn't noise; it's the expected selection drift. For comparison, gb10 tc2's above-bound ppl was −0.019%.Default flip, as a separate commit for the owner to take or leave:
62e4d801don the same branch makesqsa_attn_tc2_enabled()default ON for native HIP (cfg!(atlas_scale) && !cfg!(atlas_hip)as the "gfx" argument). Non-HIP SCALE builds stay off (unmeasured), andATLAS_QSA_ATTN_TC2=0restores scalar. f68 is checking that the unset-env path now runs tc2.Correction to the f66 comment: the branch is based on main
87df79b2f(=db442e2d4+ #228, decode-only), notdb442e2d4. Both A/B arms ran the same binary, so the numbers stand.