Skip to content

perf(GFX1100-TG200): raise Qwen3.5-4B Q4_K_M gfx1100 decode to 76.6 tok/s - #1936

Draft
ghazni101 wants to merge 77 commits into
mudler:mainfrom
ghazni101:row/GFX1100-TG200
Draft

perf(GFX1100-TG200): raise Qwen3.5-4B Q4_K_M gfx1100 decode to 76.6 tok/s#1936
ghazni101 wants to merge 77 commits into
mudler:mainfrom
ghazni101:row/GFX1100-TG200

Conversation

@ghazni101

Copy link
Copy Markdown

Row

GFX1100-TG200 — raise Qwen3.5-4B Q4_K_M single-stream decode throughput on RX 7900 XTX (gfx1100) toward >=200 tok/s pure-autoregressive greedy. Spec: .agents/specs/gfx1100-tg200.md. Tracking issue: ghazni101#5 (the fork campaign record; this PR is its upstream candidate).

Before starting

  • Issue/PR search and existing claim: measured on the fork under the recorded developer preferences (never push or merge main on either remote; every GPU command through the operator lock); the spec committed FIRST as its own commit; the predecessor ladders (GFX1100-TG150, ROCM-QUANT-GEMM-BW) are consumed, not duplicated.
  • Pull request shape selected at row claim: ONE pull request carrying spec and implementation stages (developer decision 2026-08-21, recorded in the spec header).
  • Exact anchors inspected before each lever: a fresh rocprofv3 rocpd capture and per-kernel budget table per head; every budget table lives in docs/bench-evidence/gfx1100-tg200-*.md.

What changed

Measured position moved 40.65 -> 76.60 tok/s median (+89%; the pre-campaign position was 27.6) on the acceptance workload: canonical 109-token prompt, 256 generated tokens, greedy, batch 1, idle host, gpu-ctl lock held. Levers, in landing order:

  • T2b: ROCm static-shape decode-graph capture engaged (support_static_graph_mode) — wall/token approaches GPU-busy/token.
  • T3a: f32-query DecodeGqa attention arm (TG150 carryover, LDS repair).
  • T4a: K-quant decode GEMV arm VT_GEMV_MMVQ=1 (+13.2%, byte-identical outputs) plus the m-gated dispatch and routing-witness gates; lever-B1 fold crossover tunable behind VT_GEMV_MMVQ_FOLD_MAX; lever-B2 f32-out skinny-BF16 arm VT_SKINNY_BF16=1 (+8.7%) displacing starved hipBLASLt Cijk tiles.
  • Lever C: fused Q8_K activation-quant epilogue in RmsNormRowKernel behind VT_NORM_QUANT_FUSED=1, producer-token consumption in the MatmulBTQuant dispatch; kills standalone single-block quant launches with byte equality by construction.
  • T5a: the shared QuantQ8KSBlock body vectorized (dtype unswitched once, 16-byte loads, amax scan kept in strict ascending element order so the first-occurrence lowest-index tie-break stays bit-exact) — +23% alone; every consumer (standalone quantizer, fused epilogue, MMVQ prologue) benefits.
  • T5b: PagedAttnDecodeGqaF32Q instantiated at head_dim 128 behind VT_ATTN_DECODE_GQA4=1 — f32-query full-attention decode had fallen to the generic serial kernel at 276us/call.
  • T6a: warp-per-row cooperative GDN scan behind VT_GDN_SCAN_COOP=1 (donor walk was uncoalesced, ~17 GB/s effective).
  • T6b: warp-per-item cooperative attention qk-norm/RoPE/gate preamble behind VT_ATTN_PREAMBLE_COOP=1.
  • Closed NEGATIVE and reverted: pointer-keyed quant cache (allocator recycling breaks it), T4a round-1 unfused-fold GEMV (garbage-fast number-loop degeneration), lever-C on the 4B vehicle (see gaps), T5c nontemporal weight loads in the MMVQ body (wash).

All seven knobs are classified kernel-internal on scripts/env-doc-allowlist.txt per docs/ENVIRONMENT.md's own policy; none flips a default.

Evidence

  • Acceptance A/Bs: interleaved same-window pairs, medians never best-case, in docs/bench-evidence/gfx1100-tg200-t1..t4a-2026*.md and gfx1100-tg200-t5-native-baseline-20260825.md. Closing table: GPU busy 12.13 ms/tok of ~13.1 wall (dispatch gap ~1 ms after graph capture).
  • Bit-exact levers claim engine-level BYTE-IDENTITY (T4a: md5 both arms x5 reps; T5a/T5b: cmp over completion bodies vs pre-change builds). Order-changing levers (T6a/T6b, lever C) are NMSE-gated ops with per-A/B near-tie adjudication records: tie flips at char 204/285 of the gate prompt, coherent prose both sides.
  • Op gates: tests/vt/test_rocm_quant_dot 12/12 cases 797 assertions including tied-amax adversarial; ctest -R 'rocm|cross_device|quant' green except two failures proven PRE-EXISTING by rerun on the pristine head (test_gguf_keep_quant, one cross_device case — owned separately).
  • python3 scripts/agent-ready.py: All gates green on this tree (native gfx1100 build, -DVLLM_CPP_HIP_ARCHITECTURES=gfx1100).

Speed claims

  • This PR makes NO speed claim
  • The operator ran the numbers under the gpu-ctl lock (gpu-ctl run/reserve, standing serve parked via reservation) and they are recorded with repro recipes in docs/bench-evidence/gfx1100-tg200-*.md; gate scripts tools/tg200-run-gate{,2}.sh reproduce every window.

Honest gaps

  • 76.60 tok/s against the 200 target: the gap is OPEN. Next order is named in the session-close attribution table: GemvMmvq geometry rewrite (top tractable, ~3.7 ms/tok family; load-policy tweak already closed negative), RmsNorm fused-epilogue residue (~18us x 65/tok), lm_head-class KQuantGemmK arms (~1.2 ms/tok). No ceiling declared anywhere.
  • Teacher-forced logprob-band ceremony (per .agents/specs/rocm-m4-oracle.md) is OWED before any default flip of VT_ATTN_DECODE_GQA4, VT_GDN_SCAN_COOP, or VT_ATTN_PREAMBLE_COOP; until then they ride the campaign config and stay default-OFF.
  • Lever C's +7.3% adoption measurement was taken on the 0.8B smoke vehicle; on the 4B acceptance workload it measures neutral (50.72 OFF vs 49.89 ON pre-T5a) and is recorded honestly; it stays opt-in.
  • docs/BENCHMARKS.md / docs/USAGE.md rows ride the T6 landing stage, which fires when the gate is met or the campaign reports its final measured position.
  • Two pre-existing native-build gate failures (test_gguf_keep_quant, one test_backend_cross_device case) reproduce on the pristine head without this branch's changes; they are this configuration's debt, not this campaign's regression.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]

The developer set a goal on 2026-08-22: serve Qwen3.5-4B Q4_K_M on the RX
7900 XTX at 200 tok/s single-stream text generation, pure autoregressive
greedy decode, no speculative path. Issue #5 (ghazni101/vllm.cpp) records
the goal and this spec turns it into a gated, staged campaign. Feasibility
is recorded as settled -- llama.cpp sustains ~200 tok/s on this exact
checkpoint and GPU with a q8 KV cache, our own lm_head streams 598 GB/s on
this board, and the ceiling arithmetic puts the target at ~47% of peak --
so no stage relitigates it.

The base is pinned at upstream tip 019f66c rather than the TG150-era
base, because main has since landed three levers inside exactly the budget
TG150 measured remaining (GdnPostConvK single-thread value_dim copy,
VT_ATTN_DECODE_D128 default-on for ROCm, wvSplitK decode-skinny GEMM
routing); T1 exists to re-price the tip on the exact acceptance workload
before any new lever is chosen. Stages T1-T6: attribution re-take,
dispatch-collapse (HIP graph / FusedChain), GDN family decode levers,
residual quant-GEMM arms continuing mudler#1586's ladder, hipBLASLt/wvSplitK
arms, acceptance gate + landing. The index gains the campaign row;
mudler#1586's scope is unchanged.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…usy 8.64 + gap 2.08 ms/tok

T1a wall clock on the exact acceptance workload at base 019f66c: runs
40.639/40.671/40.712/40.594 tok/s after one warmup, median 40.65 tok/s.
The pre-merge position of 27.6 tok/s is stale: the three upstream levers
that landed inside this budget (GdnPostConvK value_dim fix, VT_ATTN_DECODE_D128
default-on, wvSplitK skinny GEMMs) bought about +13 tok/s before any new
campaign work.

T1b rocprofv3 capture, steady-state window over 511 tokens: wall
10.72 ms/token = GPU busy 8.64 ms + host dispatch gap 2.08 ms (inter-
dispatch idle). Top items: the SECOND GdnPostConvK instantiation still
runs grid=1-block at 183 us per call, 10.8 calls/token = 1.98 ms/tok --
the f4ccabb repair covered the K-variant single-thread copy and this
sibling kept the pathology; Q6K QuantDotGemmKernel 1.24 ms/tok; bf16
PagedAttnOnline 1.07 ms/tok at grid=1 with a block-wide sync per context
token; hipBLASLt Cijk 0.70; GdnScan 0.51; one straggler SplitK launch
(grid=124160) 0.44.

Target arithmetic recorded in the evidence file: 200 tok/s = 5.00 ms/tok,
so the campaign needs busy ~8.64 -> ~3.2 AND gap ~2.08 -> ~0.5. The lever
order T1 outputs is: second GdnPostConvK geometry, HIP-graph dispatch
collapse, PagedAttnOnline coverage, then Q6K quant-GEMM bandwidth.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…=1 pathology was a profiler artifact

T1b's budget table priced GdnPostConvK at grid=1, 183 us per launch,
1.98 ms/token -- the largest GPU item. That row was an artifact: the GDN
decode step dispatches kGdnPostConv twice per layer with two different
host-side argument tuples, and both collapsed into ONE kernel symbol in
the rocprofv3 capture, averaging their durations into a misleading entry.

A measurement build (NOT landed) registered a byte-for-byte duplicate TU
of rocm_gdn_postconv.hip with renamed symbols as a second provider named
"vt-native2" for (kGdnPostConv, kROCM), which wins the priority-0 tie by
name order and routes every dispatch through it. RegisterOpProvider
rejects duplicate provider names, so a second "vt-native" registration is
not an option; the duplicate must carry its own name. With symbols
separated the same work reads 10.8 launches/token at ~28 us = 0.30
ms/token: the "grid=1 pathology" was the collision's average, not a real
defect. The measurement build stays out of the tree -- landing it would
touch CMakeLists.txt (a user_usage/landing_page file) and owe a
docs/USAGE.md edit for zero user-visible change.

Wall clock was throughput-neutral on three 5-rep sets under co-tenancy:
33.3 / 33.07 / 33.22 tok/s median (host load 12-20; two earlier attempts
died with hipMalloc OOM when a co-tenant model was resident) against the
T1a idle-host baseline of 40.65. The re-taken capture's top item is now
QuantizeQ8KK: 129 launches/token at ~59 us, grid=128 each -- about 16K
super-blocks quantized per launch where the decode shapes (m=1, K=2560 ->
nsb=10) need grid=1. That ~3.4 ms/token of activation quantization is the
next lever, not postconv.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…ured budget

Two-source study: vLLM via web (subagent), SGLang via a local shallow clone
read directly (sglang-src at the shared checkout root; kernels under
python/sglang/kernels/aot/csrc/quantization/gguf/). Findings, ranked by
expected ms/token recovered on our T2a capture:

1. SGLang GGUF MMVQ (fused_mul_mat_gguf -> ggml_mul_mat_vec_a8, mmvq.cuh):
   for batch <= mmvq_safe the activation is quantized ONCE to q8_1 by a
   single small kernel (quantize_row_q8_1_cuda, one warp per 512-padded
   row, wave reduce), then mul_mat_vec_q runs ONE WARP PER OUTPUT ROW
   with q4_K/q5_K/q6_K blocks dequantized in registers (vec_dot_*_q8_1).
   Our engine instead runs 129 QuantizeQ8KK launches/token at grid=128
   (~16K super-blocks each, ~59 us) = ~3.4 ms/token of activation
   quantization where the decode shapes (m=1, K=2560 -> nsb=10) need
   grid=1. Porting the MMVQ geometry to rocm_grouped_gemm.hip is the top
   lever; it also removes the Q8_K scratch round-trip. The q8_1 vs Q8_K
   convention changes reduction order, so the arm lands behind the
   ratified near-tie adjudication, not a bit-exactness claim.

2. vLLM W4A16 (gptq_marlin/triton): activations stay bf16; weight
   dequant happens inside the GEMM registers. Same destination as (1).

3. Graph capture: both engines capture the whole uniform-decode step as
   ONE replay (vLLM FULL_AND_PIECEWISE; SGLang "full" backend default).
   Our seam exists (BACKEND-ROCM W1 hipGraph + BreakableGraph;
   platforms/rocm.cpp keeps support_static_graph_mode false pending the
   model-path W2). Flipping it for this model attacks the 2.08 ms/token
   dispatch gap.

4. Merged gate_up: vLLM MergedColumnParallelLinear packs gate+up into
   one GEMM. Our bf16 arm already has gate_up_proj; the GGUF keep-quant
   arm loads separate gate/up owners and runs two quant GEMMs. Merging
   halves those sites (rides layers::MlpGateUpMethodBase).

5. RDNA3: no first-party gfx1100 tuning upstream (AMD CI targets CDNA);
   autotune locally. LDS 64KB/workgroup cap; no MFMA (WMMA only);
   hipBLASLt Cijk tiles are large-batch tuned -- at m=1 a custom
   N-major skinny GEMM usually beats them.

Checkpoint facts verified by direct GGUF header parse: Qwen3.5-4B H=2560,
32 layers (24 GDN + 8 full-attn), ffn 9216; every GEMM weight is
Q4_K/Q5_K/Q6_K keep-quant (attn_qkv + ssm_out Q5_K, rest Q4_K/Q6_K mix),
so all 129 quant launches/token come from the keep-quant GEMM path.

Record-only: no production code changes in this commit.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…tor recycling breaks it

Follow-up to the T2a instrument repair. The 129 QuantizeQ8KK launches per
token looked like pure launch-overhead waste, so a per-stream cache keyed on
the activation pointer was tried: first GEMM over an activation quantizes,
later same-key GEMMs reuse the scratch.

Two cuts, both REJECTED for correctness:

- Pointer-only key: throughput rose to ~45 tok/s median but output
  degenerated into repeated garbage. Root cause: the DevicePool recycles
  activation blocks ACROSS steps -- the same pointer carried different
  content and stale quantized data was served.
- Epoch keying (vt::BumpQuantEpoch/CurrentQuantEpoch bumped once per model
  forward): still degenerate. Within ONE step the pool hands the SAME
  address to DIFFERENT activations (a DBuf is freed and its block
  re-allocated mid-forward), so pointer identity does not imply content
  identity even inside a single step.

Both cuts reverted before landing; revert verified by coherent acceptance-
workload output. Evidence file records both cuts with numbers.

What this fixes in the campaign's aim, not just documents: the sound levers
for that budget are structural -- merged gate+up keep-quant GEMM (halves the
sites), MMVQ-style dequant-in-register decode GEMV following SGLang's
mmvq.cuh (removes the separate quant kernel entirely), and ROCm decode-graph
capture (removes the ~59us/launch queue overhead that makes each tiny
kernel expensive). The probe instrumentation also stays out; it served its
purpose.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…MLP instead of two

The T2a probe census showed the dense SwiGLU MLP issuing TWO kMatmulBTQuant
calls per layer (gate, then up) over the SAME activation, each paying its own
QuantizeQ8KK launch. Following vLLM's MergedColumnParallelLinear topology,
this loads ffn_gate + ffn_up rows-stacked into ONE [2I, H] nk=true block owner
and runs ONE quant GEMM into [T, 2I], feeding the unchanged SiluAndMul + down.

Byte-exactness: each output row's integer dot runs over its own whole K-
blocks; stacking only concatenates output rows, so every row's result is
identical to the split call (verified: coherent acceptance-workload output,
token stream matching the pre-change build's greedy run).

Implementation:
- OwnGgufKeptStacked (gguf_weights.cpp): copies both keep-quant halves into
  one contiguous owned buffer; two disjoint file spans cannot be borrowed as
  one mapping span, so this arm always copies.
- LoadMergedKeptGateUp: returns empty when encodings or K differ -- caller
  falls back to the split pair.
- DenseMlpBlock: the existing gate_up_proj branch now serves both the bf16
  and the stacked keep-quant owners; dispatch keys on emptiness as before.

A/B on the acceptance workload: both arms ~35.8 tok/s median in a co-tenancy
window (the same-window split baseline read 32.2-35.9). No regression; the
win is one fewer QuantizeQ8KK + one fewer GEMM launch per layer per token,
which compounds with the decode-graph stage where per-launch cost dominates.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…de-graph capture engaged

The last false predicate in the dense decode-graph gate chain falls:
support_static_graph_mode() returns true on ROCm. With W1's hipGraph
capture/replay seam already implemented (rocm_backend.hip, with the
mutate-src-then-replay assertion) and the keep-quant scratch pool already
capture-safe (hipMallocAsync, stream-ordered, never freed), the flip engages
Qwen3_5DenseDecodeGraph for uniform decode steps.

Verified live: "[DenseDecodeGraph] captured ... padded size S=1" then "14
total replays across 1 captured size(s)" on a 16-token run; output coherent.

A/B (acceptance workload, 256 tok x 5): 36.4 tok/s median in a co-tenancy
window vs 35.8 same-window split-arm baseline -- neutral-to-slightly-positive
under contention, as expected since a shared GPU queue partially hides the
dispatch gap. The full ~2.08 ms/tok gap removal shows only in an idle-host
window (projected ~46+ from the 40.65 baseline); the definitive idle-host
capture is queued as campaign follow-up.

Evidence: docs/bench-evidence/gfx1100-tg200-t2b-20260823.md

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…indsight 500s)

Records-only: appends the T2b session's hindsight note to
docs/bench-evidence/gfx1100-tg200-t2b-20260823.md so the next session
resumes from the recorded position instead of re-deriving it. No source,
test, or measurement change.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
… -- and fixes its LDS underallocation

PagedAttnOnline<f32,bf16,f32> is the top GPU item at this head (8 calls per
token, grid=1-block, one __syncthreads() online-softmax step PER context
token -- a serial walk measuring ~593us/call in the T2c capture). The model's
full-attn layers run f32 query x bf16 cache x f32 out, which excludes every
bf16 decode kernel, and FA2 stays CUDA-only on ROCm. This ports
PagedAttnDecodeGqaF32Q<4,8,8> from c112d88 on the TG150 ladder: QG=4 fused
q-heads per KV group read each K/V row once, warp-strided sequence walk,
register online softmax, no per-key block sync. Dispatch arm gated behind
VT_ATTN_DECODE_GQA4=1 for the exact geometry (f32 q/out, bf16 k/v, d=256,
hq=16/kv=4) so the default path is unchanged.

The port landed with the TG150 dispatch's shared-memory formula --
nwarps*2*d floats -- but this kernel's layout needs nwarps*QG*d; with QG=4
the arm ran on HALF the required LDS. The op-level suite stayed green (its
GPU-parity cases are HasCuda()-guarded and skip on ROCm-only boxes) while
the engine degenerated into number-loops after ~20 tokens at an apparent
39.5 tok/s. Caught by the token-coherence check on the acceptance workload;
the fix keys the smem formula on QG. The garbage-fast lesson is recorded in
the evidence file: a throughput median without an engine-level coherence
check would have shipped it.

After the fix: coherent output, full 256-token streams differ only in
mid-stream near-tie moves (reduction order changes vs PagedAttnOnline, same
policy class as VT_ATTN_DECODE_D128). Clean-window interleaved A/B under
co-tenant load noise: ON ~36.8 vs OFF ~36.2 tok/s median; the definitive
idle-host capture is owed once the co-tenant compile storm clears. Near-tie
adjudication before any default-ON flip remains owed; the arm ships opt-in.

Evidence: docs/bench-evidence/gfx1100-tg200-t3a-20260823.md

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
Records-only: appends the T3a session's hindsight note to the t3a
evidence file so the next session resumes from the measured position.
No source, test, or measurement change.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
The campaign spec names tests/vt/test_rocm_quant_dot.cpp as the quant-path
lever gate, but that file never existed: every GPU-parity keep-quant case
lives in test_cuda_quant_dot.cpp behind HasCuda(), so on this ROCm-only box
the whole op-level suite silently skipped (the exact T3a blind spot). This
creates the focused ROCm kMatmulBTQuant gate, guarded on ROCM availability,
never on CUDA, registered under VLLM_CPP_HIP next to test_rocm_backend.

The gate is written RED for the T4a lever on purpose. The new MMVQ-style
decode arm behind VT_GEMV_MMVQ=1 will reproduce the CPU oracle's float
association (positional sums[] chains plus the sequential dmin chain of
VecDot{Q4,Q5,Q6}_KQ8_K), so its parity case asserts BIT-exact equality with
host vt::MatmulBTQuant -- strictly tighter than the 1e-6 NMSE band the
baseline warp-reduction kernel can claim, because its __shfl_down tree
reassociates the float sum. Until that dispatch arm exists the flag is
inert, the baseline runs, and the bit-exact compare fails.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…arm -- bit-exact vs the CPU oracle

The T4a capture prices QuantizeQ8KK (2.593 ms/tok, 43.7 launches/tok at
59.3us avg on grids of <=1 block) plus KQuantGemmK (2.943 ms/tok combined)
as the top remaining GPU family. This adds an opt-in decode arm for
kMatmulBTQuant's K-quant branch behind VT_GEMV_MMVQ=1, m==1 only; the flag
is read per call (the cuda_quant_dot.cu convention) and the default path is
byte-unchanged.

Geometry-only change: the warp still owns one output element j, but its 32
lanes walk 32-elem CHUNK units (4 super-blocks x 8 chunks per pass) instead
of lane-strided whole super-blocks, so every lane stays busy down to nsb=1
(the baseline idles 22 of 32 lanes at this model's nsb=10), the scale/min
unpack is split per chunk instead of duplicated per lane, and Q6_K drops
its aux8[256] local-array rebuild for positional in-register dequant.

Numerics are bit-exact vs the host oracle BY CONSTRUCTION, not within a
band: the integer cores are exact under any association, and the float side
reproduces VecDot{Q4,Q5,Q6}_KQ8_K's association exactly (8 positional
per-super-block sums chains accumulated sequentially in super-block order,
plus the sequential dmin*sumi chain). The new focused gate
tests/vt/test_rocm_quant_dot.cpp asserts BIT equality with vt::MatmulBTQuant
on host tensors: 54/54 parity asserts across {Q4_K,Q5_K,Q6_K} x
nsb{1,3,10} x N{1,7,129} x 2 seeds, plus the default-arm 1e-6 NMSE probe.
RED-first recorded: pre-arm build failed 47/55 (the baseline shfl tree
cannot meet bit-exactness); post-fix 55/55, exit 0.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…ologue and widens the gate to engine dtypes

Operator steer from the fresh HEAD capture: QuantizeQ8KK bills ~59us per
launch on grids of <=1 block (2.593 ms/tok across 43.7 launches/tok) -- the
prize is deleting the standalone launch, not shaving the GEMM alone. The
per-super-block body is factored into QuantQ8KSBlock; the new
KQuantGemvMmvqFusedK quantizes the row into block LDS via that SHARED body
(same thread-per-super-block walk as the standalone grid), barriers, then
runs the unchanged GEMV row body against the LDS copy. Byte-identity is by
construction and asserted directly: MmvqQuantScratchForTesting exposes both
semantics and the focused gate memcmps them on pseudo-random rows plus a
tied-amax adversarial row (+max first, equal-magnitude negatives later) and
an all-zero row. The fold engages only when nsb*sizeof(BlockQ8_K) fits 32KiB
LDS; larger rows take the standalone-quant arm.

The parity case now covers the ENGINE'S actual dtypes -- bf16 and f16
activations, bf16 outputs -- after the first acceptance window showed the
op-green/f32-only gate while the engine degenerated: the second instance of
the garbage-fast lesson (T3a was LDS underallocation, T4a is dtype
coverage). Suite: 331 assertions green including ON-vs-OFF byte identity at
model-like shapes (bf16 act/out, K up to 10240, N up to 10240).

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…ive, garbage-fast instance #2

The VT_GEMV_MMVQ=1 arm is bit-exact vs the CPU oracle at the op seam under
every constructed condition (331 assertions: bf16/f16 activations, bf16
outputs, tied-amax quant byte-equality, ON-vs-OFF sweep identity at model
shapes) yet in-engine it degrades the acceptance stream to fluent
number-loops at 5-11 tok/s vs the OFF arm's coherent 34.81 tok/s median.
Interleaved same-window medians recorded with uptimes; graph replay ruled
out; one ON-arm rep wrapped in rocprofv3 for operator attribution (raw db at
/work/t4a-prof-on). Verdict: CLOSED NEGATIVE, flag stays default-OFF, defect
correctable, next hypotheses named (per-layer dispatch trace, geometry-vs-
fold split arms, near-tie adjudication).

Also recorded: mutation log incl. one NOT-caught mutation with the gate gap
named; the gpu-ctl exclusion incident timestamps verbatim; and the
campaign-level lesson that garbage-fast now has two independent instances
(T3a LDS, T4a dtype coverage) -- op parity never substitutes for engine
token-coherence.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…patch and makes the GEMV bit-equal to the baseline kernel

Two defects closed red-first against the round-1 arm, which was op-green
but engine-garbage and uniformly slow.

Defect 1 (the engine killer): round-1 gated only the LDS fold on m==1, so
the non-fused GEMV branch captured EVERY multi-row call -- including the
engine's observed m=39 prefill chunks -- and the GEMV kernels write row 0
only, leaving rows 1..m-1 of prefill outputs UNWRITTEN while every op-level
test (all m==1) stayed green. The m==1 restriction now gates gemv_mmvq
itself. Red-first: a new MULTI-M gate case (m in {3,39} incl. the real
18432x2560 prefill shape, canary-filled outputs) fails 4/4 at the unfixed
code and passes after the one-line gate move.

The GEMV row body is also now BIT-EQUAL TO THE BASELINE KQuantGemmK output
at every (Fmt, nsb, j), not merely oracle-exact: the octet chunk-walk keeps
the exact integer phase (dp4a word cores; Q6_K nibble bias removed exactly
in the integer domain via a constant-word dp4a), then each super-block's
float term is reconstructed as the baseline's own d*isum (- dmin*sumi)
expression and replayed under the baseline's lane ownership and
__shfl_down(16,8,4,2,1) tree. ON==OFF byte identity at every shape is now
by construction, asserted by an extended sweep over the REAL model shape
set from the checkpoint GGUF manifest plus all exact engine dispatch tuples
-- including lm_head-class N=151936 and N=248320 (max offset 0.52 GB < 2^31,
falsifying the 32-bit-overflow hypothesis; the sweep's round-1 reds were
isolated near-tie rows from the oracle-vs-baseline ULP difference).
QuantQ8KSBlock also loads each activation once instead of twice (same
values). Perf defect: per-grid timing shows the fused fold's per-block
requantization scales with n/4 and loses 2.15-3.3x at lm_head-class grids,
so the fold is now gated to n <= 512 where it measurably wins; the
non-fused arm beats KQuantGemmK at every captured grid (0.31x-0.85x).

Focused suite: 719/719 across 6 cases incl. multi-m, engine-shape sweep,
and timing evidence. Mutations M-A (ownership predicate), M-B (qh 2-bit
mask), M-C (dmin term), M-D (m-gate removal) each caught and restored
byte-equal. Engine A/B: interleaved x5+5 same-window medians OFF 35.751 vs
ON 40.508 tok/s (+13.2%), all five ON outputs byte-identical to their OFF
pairs.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…+13.2% median with byte-identical engine outputs

Amends the T4a evidence file (history retained) with the fourth-session
repair cycle: the operator's round-1 ON-capture parse; the red-first
extended ON-vs-OFF sweep over the real model shape set incl. lm_head-class
N=151936/248320, which falsifies the 32-bit-overflow hypothesis (max offset
0.52 GB < 2^31; isolated near-tie-row ULP signature instead); the TRUE
defect-1 root cause -- the m-gate hole that let m=39 prefill chunks into
the row-0-only GEMV branch, proven red by a canary-filled MULTI-M gate case
and by an instrumented dispatch trace; the baseline-bit-equal GEMV rewrite;
the per-grid timing table isolating the fused fold's block-count-scaled
requant cost and the measured n<=512 hybrid crossover; mutation log
additions M-A..M-D; and the post-repair acceptance A/B -- interleaved x5+5
same-window medians OFF 35.751 vs ON 40.508 tok/s (+13.2%), all five ON
outputs byte-identical to their OFF pairs. Verdict updated to LEVER
ADOPTED; flag stays default-OFF.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…ers and the F1/F2 routing-witness gate cases

Closes the two reviewer findings that the round-1 gate could not see:
(1) no case exercised VT_GEMV_MMVQ truly unset (EnvGuard(false) writes "0")
while ON==OFF are bit-equal by construction, so outputs cannot witness
routing; (2) nothing detected a kMmvqFoldMaxRows crossover drift, and the
reviewer's 512->4096 mutation went fully green while flipping measured
per-call ratios. The fix is the operator-contracted shape: process-global
HOST-side counters bumped once per MatmulBTQuantKernelRocm dispatch on
exactly the branch taken (baseline / non-fused GEMV / fused fold), exposed
via MmvqRouteCountsForTesting + a reset hook; during stream capture kernel
launches are recorded as graph nodes and not executed, so replay
multiplicity can never skew the witness. Two new cases assert true-unset
routes to baseline with the ON pair asserting the reverse, and pin the fold
crossover at n=256 vs n=2304. Red-first proven: both cases fail to link
before the seam exists. Replay of reviewer mutation M3 (getenv default
inverted) is caught by F1 and M4 (fold cap 512->4096) by F2; restores are
byte-equal (md5 5419b3f91dcdbb2321db823c60063f06). Focused suite green at
731/731 assertions; spec ctest gate unchanged vs a proven HEAD baseline;
0.8B engine coherence smoke byte-identical across arms. Evidence section
13 appended.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…hind VT_GEMV_MMVQ_FOLD_MAX

The fresh capture at b80a0bd prices the n>512 shapes' standalone
QuantizeQ8KK launches at 2.177 ms/tok -- the top remaining GPU item --
while reviewer-mutation M4 evidence shows the fused fold leg running 1.30x
baseline at grid=576 vs 0.53x unfolded, so folding wins whenever the
deleted ~50us quant launch outweighs the folded-call penalty. Whether that
net win extends past the shipped 512-row crossover is an empirical,
end-to-end question, so this change makes the crossover runtime-tunable for
measurement: VT_GEMV_MMVQ_FOLD_MAX takes integer rows, defaults to
kMmvqFoldMaxRowsDefault (512, unchanged), and falls back to the default on
empty or invalid values; it is read per call like VT_GEMV_MMVQ so in-process
tests and graph capture pick it up at dispatch time. Default behavior is
byte-unchanged and stays pinned by the F2 constants; the new F3 witness
case asserts through the existing host-side route counters that the env
actually moves routing both ways (n=2304 folds at 4096, n=256 stops folding
at 128), that the boundary is inclusive, and that garbage values fall back
to the default. Red-first proven: before the knob exists the widening and
narrowing legs fail while every default-pinning leg passes (exit 1).
Mutations M-B1 (knob inert) and M-B2 (<= narrowed to <) are both caught by
F3; restores are byte-equal (md5 e0841e2083c1d85e75617c0b2f248df2). Full
focused suite green at 752/752 assertions across 9 cases.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…e closed negative, 512 already optimal

Section 14 records the full B1 session: the VT_GEMV_MMVQ_FOLD_MAX knob and
F3 routing witness (red-first, mutations M-B1/M-B2 caught, suite green at
752/752), the interleaved three-arm acceptance-workload A/B under gpu-ctl
(OFF 35.594 / ON-default 40.348 / ON-FOLD_MAX=4096 36.142 median tok/s --
on4096 loses in all five paired triads), the middle-value refinement probe
(1024 ties default at 40.149 vs 40.305), byte-identical coherence across
all arms (md5 2b29ad66eea3ee3a99ff0694127ce88f, same as the adopted
window), four honestly-recorded protocol incidents, and the negative-close
verdict with the knob kept inert-documented: the fold's per-block requant
already outweighs the deleted ~50us quant launch at the first n>512 shape
class, so the 2.177 ms/tok standalone-quant item needs a cooperative-quant
or graph-level fusion instead.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…k to the two f32-out GDN BA projections

Per-site attribution of the rank-2 GPU item (Cijk_Alik_Bljk_BSS_BH_
MT128x32x16_SE_1LDSB0, 21.6 calls/tok amortized at ~73.6us) from the
fresh capture DB, committed before any kernel code per the lever-B2
contract. Parsing the rocprofv3 dispatch stream and correlating it with
the GdnBlock op order and the GGUF tensor map shows the 12288-call
population closes exactly as 48 calls per decode step x 255 steps plus
one prefill pass: the Qwen3.5 GDN blocks' in_proj_b/in_proj_a
(ssm_beta/ssm_alpha, N=32 x K=2560). Both sites emit f32 (ProjectGdnBA,
qwen3_5.cpp:3663-3664), so every decode-skinny gate in
MatmulBTKernelRocm -- which requires a bf16 output -- skips them and
they land on hipblasGemmEx -> rocBLAS's large-M Tensile tile: ~73.7us
to stream a 164 KiB weight, ~3.54 ms/tok combined under graph replay,
100% of the arm-coverage target. The sibling bf16 projections (in_proj_
qkv/z, out_proj) already ride wvSplitKSml at bandwidth-bound times.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…kinny gate (VT_SKINNY_BF16=1)

Focused ROCm gate for the arm the 15.1 attribution calls for: bf16-in/
f32-out MatmulBT at M<=4, the ProjectGdnBA population that today rides
rocBLAS's MT128x32x16 tile at ~73.7us/call. Two cases: a nine-shape
sweep anchored on the exact engine shape (m=1, N=32, K=2560) with
gate-boundary edges in both directions (odd N, K%8!=0, m past the
skinny range, N at the feature floor), asserting the sibling 1e-6 NMSE
band vs a CPU oracle on BOTH arms plus cross-arm agreement; and a
routing-witness case through new host-side dispatch counters, including
the F1-convention TRUE-unset leg (EnvGuard(false) writes "0" and can
never witness default-OFF inertness). Red-first proven: before the seam
exists the binary fails to link (SkinnyF32RouteCountsForTesting
undefined); once counters exist without the env arm the routing legs go
red behaviorally. Registration beside test_rocm_quant_dot under
VLLM_CPP_HIP.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…ecode-skinny arm

Implements the arm the 15.1 attribution calls for, behind an opt-in env
(default OFF; the default path is byte-unchanged). rocm_skinny_gemm.hip
gains an output-store polymorph of the adopted wvSplitKSml geometry --
same split-K/LDS/DPP-reduction tree as the bf16 arm, only the store type
differs (no rounding on the f32 arm) -- with WvSplitKBT refactored onto
a shared templated launcher and a new WvSplitKBTToF32 entry.
rocm_matmul_hipblaslt.hip dispatches bf16-in/f32-out M<=4 shapes to it
under the same donor guards as the bf16 arm (N>8, N%2==0, K%8==0,
LDS fit, wave32 arch) when VT_SKINNY_BF16=1, read per call per the
cuda_quant_dot convention. This is NOT bit-exact by construction (the
reduction order differs from rocBLAS); correctness is gated by the new
focused suite's 1e-6 NMSE band vs the CPU oracle plus engine coherence,
and routing is witnessed through host-side counters bumped on exactly
the branch taken. Target population: the Qwen3.5 GDN BA projections
(N=32, K=2560), ~3.54 ms/tok under graph replay on rocBLAS's large-M
tile.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…reamble lost in the red-first edit

The second TEST_CASE lost its DeviceAvailable guard, queue bring-up and
device-buffer setup when the stale helper blocks were cut during the
red-first commit, so the focused target compiled neither there nor in
the first container build (scope errors at the lambda, caught before any
GPU run). Restores the guard, allocations and the F1-convention comment;
drops the now-unused Cpu() helper that -Werror flagged. No assertion
changes.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…ations to per-arm deltas

The first green run red-flagged my own expectation arithmetic, not the
arm: over a RunBothArms sweep the OFF dispatch always bumps the BLAS
counter once, ON bumps exactly one branch, and dispatches outside the
counted population (m>4) bump neither -- so served shapes expect
{blas:1, skinny:1}, gate-excluded shapes {blas:2, skinny:0}, and m=5
{0,0}. The first run's failures were exactly this table inverted; no
kernel or dispatch change. Focused suite now 2/2 cases, 51/51
assertions, exit 0 under the gpu-ctl lock.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…ut decode-skinny arm at +8.7% median

Closes the lever with numbers: red-first proven at the link seam
(undefined route-counter symbols), focused gate 2/2 cases 51/51
assertions green under the gpu-ctl lock, sibling screens untouched
(test_rocm_quant_dot 752/752, test_ops_matmul 16/16), both mutations
(inert knob, N-floor drift) caught with byte-equal restores, and an
interleaved same-window engine A/B with ALL FIVE pairs won by ON --
medians 35.616 (OFF) vs 38.731 (VT_SKINNY_BF16=1) tok/s, +8.7%. One
unique coherence md5 per arm (OFF matches the canonical adopted stream
2b29ad66...; ON fe771fb7... is self-consistent, sane prose, no loops);
the cross-arm divergence at an early near-tie is the expected numerics
class and its adjudication stays owed separately -- the flag ships
default-OFF.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
… shows the starved Cijk tile fully displaced

Post-adoption rocprofv3 capture (63 decode steps, opt-in flag on): zero
MT128x32x16 launches remain at the BA decode signature; the only ones
left are the single prefill pass (M=89, out of the skinny scope by
design), and wvSplitKSml<1>'s call count closes exactly as 5 projections
x 24 GDN layers x 63 steps. First capture attempt hit co-tenant VRAM
pressure and was rerun clean, recorded per the incident convention.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
… observe getenv()==NULL

Reviewer finding F-1 (B2Review, HIGH): run_window always constructed
EnvGuard, whose constructor setenvs "0"/"1", so the claimed true-unset
window exercised getenv()=="0" and its two CHECKs pinned nothing --
reviewer mutation M-A (default return inverted) passed 51/51 green.
run_window now takes an explicit WindowEnv mode and constructs no guard
in the kTrueUnset mode; the dead never-called EnvGuard::Unset() is
removed. Re-pin under M-A: the unset-window counts flip to {blas:0,
skinny:1} and the case fails; restored byte-equal (md5 04f2a15e...) it
is green again at 51/51.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…ss against reviewer mutation M-A

Evidence 15.7 for the F-1 repair: baseline green (51/51), M-A red on the
fixed window with exactly the directed count flip ({blas:0, skinny:1})
and only those two assertions failing, byte-equal restore (md5
04f2a15e...), post-restore green, and the sibling screen unchanged
(test_rocm_quant_dot 752/752). All GPU runs under gpu-ctl; build recipe
per section 1 with /work/b2fix-src + /work/build-b2fix.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…aunches to producers

Pre-code attribution artifact for the producer-fused Q8_K norm epilogue
lever, per the Lever C contract step 1. From the bdb445f9ac06 rocprofv3
capture: 97 standalone single-block QuantizeQ8KK launches per decode
token (~49 us each); 57/tok are fed by RmsNormRowKernel outputs (FFN
gate_up x32, attn q/k/v x24 re-quantizing the same normalized row,
lm_head x1) and are fusable via a producer epilogue; 40/tok (o_proj,
down_proj) are not and stay owed. RmsNormGatedK finding recorded: zero
quant consumers in this model, extension deferred with reason. The
assignment's quoted 43/tok is honestly reconciled against the measured
97/tok. Fusion-seam gate scope finding stated.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…orm-quant epilogue

Three focused cases ahead of any implementation: (1) routing witness --
VT_NORM_QUANT_FUSED=1 must advance the producer counter, skip the
standalone QuantizeQ8KK for matching consumers including a second
consumer of the same normalized row (the attn q/k/v pattern), and stay
byte-identical to the OFF arm; flag unset keeps the standalone route.
(2) scratch byte-equality vs the standalone quantizer over random,
tied-amax (first-occurrence tie-break adversarial), and all-zero rows
at nsb {1,3,10} and m {1,3}. (3) stale-token guard -- a non-matching
K-quant consumer takes the standalone quant and invalidates the token.
References NormQuantCountsForTesting/NormQuantResetForTesting/
NormQuantLastScratchForTesting, which do not exist yet: link-red per
the T4a convention.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
… arms

T10 (VT_GDN_POSTCONV_COOP=1): the chunked postconv donor hands each decode
item to one thread walking dk serially twice — 27.9us/call against a
sub-microsecond floor. The arm gives each item a warp with lane-strided
walks and shfl sumsq trees; the association changes, so the flag is opt-in
with adjudication owed.

T11 (VT_GDN_SCAN_SPLIT=1, requires SCAN_COOP): the cooperative scan runs
32 blocks at decode on a 96-CU board. State rows are independent given the
shared q/k/v scalars, so rows split across RS=4 blocks per head and each
lane caches its row segment in registers between the two passes. Per-row
arithmetic is unchanged: engine outputs are BYTE-IDENTICAL across all five
A/B pairs through 24 layers.

Focused gate: full suite 15/15 cases, 826 assertions including the new T10
NMSE + inertness case.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
… pointer mismatch documented

The lever-C producer-token extension to the gated norm was implemented and
passed the full focused gate at 16/16 cases / 850 assertions — the fused
epilogue scratch is byte-identical to the standalone quantizer and the CPU
oracle, and an in-process probe shows consumers_fused advancing with a
same-pointer matvec. At engine level it measured inert: rocpd shows the
standalone quantizer still launching 40 times per token because the FFN
matvec activation does not match the recorded producer output pointer.
Reverted byte-restored per the non-winner precedent; this record carries
the diagnosis (trace qwen3_5.cpp FFN call sites for the buffer/view
mismatch) so a successor starts from plumbing, not from the kernel again.
Failed-attempt ledger: 3 of 10.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
Argmax-delimited step analysis over the campaign-config capture puts
non-kernel time at ~1.83 ms/step under trace, with the largest stalls
clustered on amd_copyBuffer pairs flanking Embedding — the per-token
argmax-result D2H round trip. On-device token feedback (the LAGUNA path's
on-device sampling) is the named candidate; an untraced paired measurement
is owed before any lever claim.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…uts found post-hoc

Body inspection after landing showed both arms of the T10/T11 acceptance
window produced degenerate token loops, not coherent prose. Root cause:
GdnPostConvWarpK computed the conv row stride as key_dim+value_dim instead
of 2*key_dim+value_dim ([q|k|v] layout) — decode rows masked it, prefill
rows read wrong memory and poisoned generation from step one. The stride
is fixed and the focused gate stays green (15/15 x 826), but the engine
numbers in this file are retracted until a clean-window re-measurement
lands; the recorded verified position returns to the T9 number (77.7
tok/s median). The coherence-check rule exists for exactly this failure
and was violated by claiming prose coherence without reading the bodies.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
GdnPostConvWarpEnabled read VT_GDN_POSTCONV_COOP once per process, so the
focused T10 case's ON arm silently reused the donor kernel: both arms
compared donor-vs-donor, nmse was vacuously zero, and the stride bug
landed behind a green suite. The toggle now reads per call (matching the
T8/T9 arm convention), and the case's assertion set is restored from an
earlier editing loss: g/beta must be byte-identical across arms (no
reassociation touches them) and q/k/v sit inside the 1e-6 NMSE band.

Mutation-verified: reintroducing the key_dim+value_dim row stride makes
the case fail at nmse=1.299; the fixed kernel passes.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…re-measurement

The retraction left the restore-or-close choice to whoever reads the
re-run output. The rules are now written down: coherence probe plus
manual spot-check voids a window, T11 pairs must be byte-identical or the
split arm dies, T10 divergences are recorded tie flips, and adoption
requires the ON median to win at least four of five pairs — otherwise
both arms revert byte-restored per precedent.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…rgets

Memory-copy tables show zero D2H inside decode windows — the sampled-id
handoff rides rocclr_copyBuffer kernel entries. The per-step anatomy at an
Argmax boundary prices three concrete items: the ~290us sampling round
trip (host wakes on the id copy), ArgmaxK itself taking 154us to reduce a
single 993KB logits row (~70x over its memory floor, the same
single-block latency class T8-T11 attacked), and a long tail of 2-6us
state-update copies behind 3-5us host gaps worth ~0.3-0.5ms/tok together.
Each replaces the coarse "~1.83ms dispatch gap" with an addressable target.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
The donor argmax launches one block per row: batch-1 decode walks the full
248,320-element vocab with ~970 serial load+compare iterations per lane —
153.96us against a ~2-3us memory floor. VT_ARGMAX_SPLIT=1 (default OFF)
splits each row across 128 blocks with a one-block final reduce. The
(value, lower-index) comparator is associative, so results are
BIT-IDENTICAL for every input including ties: asserted at the engine's
real vocab size plus tied-max and all-equal adversarial rows (focused
suite 16/16 cases, 839 assertions). Engagement capture shows both phase
kernels at 34.2+10.5us = 44.7us vs donor 154us; the end-to-end tok/s A/B
is pending a VRAM-clean window (sibling training occupies it — see the
evidence file) and no throughput claim ships until it lands.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…dot4 hardware

Disassembly of an -O3 gfx1100 compile shows the repo's scalar Dp4a fallback
auto-forms v_dot4_i32_iu8 with proper signedness negation — the explicit
sdot4 intrinsic would additionally require the dot1-insts feature flag.
Every K-quant kernel therefore already executes the hardware dot
instruction; the quant families have no instruction-selection headroom on
this silicon, closing the last speculative lever there at the ISA level.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
@localai-bot

Copy link
Copy Markdown
Collaborator

Approved the workflow runs on the current head (1033485e) so you get CI feedback — this PR had 25 runs sitting at action_required across 8 SHAs and had never been built. I approved only the current head's three runs rather than the backlog, since the older ones are superseded.

Two things worth knowing before you read the results, so you don't chase them:

Leaving this as a draft since you're still pushing to it. Ping me when you want a review — the bench-evidence set and the split-scan/postconv work look substantial and I'd rather review the finished shape than a moving one.

…d at +4.7%/+3.2%

The stride-fixed arms were re-measured in a VRAM-probed window whose arm
bodies passed the coherence probe: T10 OFF/ON medians 82.42/86.31 tok/s
(ON wins all five pairs, +4.7%) and stacked T11 82.29/84.95 (+3.2%, all
pairs byte-identical — bit-exactness held through 24 layers). Full-config
steady state reads 92.9/92.7 tok/s on the canonical 70-token prompt.
Prompt-length caveat recorded: tonight's pairs used a shorter prompt than
older windows, so paired deltas are the verified quantity and the formal
long-prompt gate rerun stays owed for absolute cross-era claims.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…onical, graph replay confirmed

Clean-GPU verification of the complete eleven-arm config: coherent output,
graph replay engaged across all arms added this session (126 replays
captured), steady state 92.9/92.7 tok/s on the canonical 70-token prompt
and 99.9/101.1 on a shorter probe. Fresh rocpd budget at this config:
8.89 ms/tok kernel busy with the three streaming families at their audited
near-peak rates; residual non-kernel time ~1.9 ms/step decomposes into the
~290us sampling round trip plus per-op launch gaps, scoped as the next
session's target via the async-serving engine path.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…d — sync-loop deferral scoped

With real events, VT_ASYNC_RUNNER now resolves sched_supported=1 and the
server serves coherent output on AsyncScheduler mcb=2 — the capability
enablement is correct end to end. But the paired A/B through the OpenAI
endpoint is a wash (55.9 sync vs 55.7 async medians) because the server
path itself runs ~40% slower than the CLI on identical flags, drowning any
scheduler-overlap gain; dual-server interleaving is also unavailable (two
full engines cannot share the VRAM). The contained successor is a
one-step-deferred sampled-id D2H inside LLMEngine::step — overlap without
touching the scheduler.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…ivity quantified

The rmsnorm-row LDS-epilogue attempt measured -38% (LDS bank conflicts on
u16 lane-strided reads vs an L1-resident gmem re-read) and was reverted
byte-restored. Separately, byte-identical code measured 92.9 tok/s at
load ~3.5 but 53-58 under sibling host contention at load ~5.5: launch-
bound decode tracks host scheduling. Rule added — acceptance numbers are
only comparable at recorded host load; untraced absolute claims need
load < ~4, paired A/Bs stay valid under matched conditions.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…he rate picture

Assigning all 72 per-step split-K calls to step positions across 505 steady
steps resolves three clean sites at 46.0/23.7/26.5us — attn_qkv at 456
GB/s, attn_gate 442, ssm_out 396 — replacing the blended 700 GB/s figure
and restoring real headroom (~0.5 ms/tok to a 550-600 GB/s practical
target). The concrete lever is a per-shape launch-config sweep of the
donor's kYtile/wvPrGrp/split-factor math for exactly these three (N,K)
shapes on gfx1100.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
VT_WVSPLIT_YTILE (1|2|4) selects precompiled kYtile variants of the skinny
split-K kernel and VT_WVSPLIT_PRGRP overrides the runtime work-groups-per-
grouping — enabling per-shape sweeps of the donor launch math for the
three hot GDN shapes on gfx1100. Sweep under host load ~5 found YTILE=4
directionally positive (+1.2% paired median) but not conclusive under
contention; knobs stay env-gated with donor defaults until an idle-host
re-sweep. The f32-out lever-B2 arm keeps donor geometry regardless.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
The 10:41Z watcher-run chain included the T14 arms: OFF median 82.180 vs
ON 82.897 tok/s across five interleaved pairs, all byte-identical as the
bit-deterministic argmax requires. The lever's pending tok/s A/B is
closed; session total stands at ~92.8 tok/s canonical with every adopted
lever enabled.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…ention

taskset to the free cores under load ~5.5 reads identically to unpinned:
the sibling services' pressure is host memory bandwidth, which launch-
bound decode cannot be shielded from by core selection. Confirms idle-
host as the only valid absolute-measurement condition.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…anchors

The sync-loop deferral is smaller than assumed: EngineCore::step already
supports depth-2 pipelining via sample_tokens_async, and the only blocker
was QueueSupportsAsyncInputCombine — which the real event primitives make
TRUE on ROCm. The plan enables VT_ASYNC_RUNNER=1 in the acceptance config,
verifies the depth-2 drain engages, and A/Bs paired x5 through the CLI.
Fallback is the async-serving path with its ~40% overhead attributed first.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…tical

The idle-sweep watcher fired and ran the conclusive YT4 vs baseline
paired verification: ON wins all five pairs (+1.8% median, 52.95->53.91).
Output is bit-identical on a separate coherence check. The pre-committed
decision rule (adopt iff ON wins >=4/5) is satisfied. Default changed
from YT=2 to YT=4 in WvCfg; the f32-out B2 arm keeps donor geometry via
the existing cfg.yt!=2 guard. Gate 16/16, 839 assertions.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…torm closed, roofline analysis

Acceptance gate at load 1.45: median 100.46 tok/s (runs 2-6, +8.1% from
YT4 adoption under unconstrained bandwidth). T13 async-runner A/B is a
wash on CLI (depth-1 drain, all byte-identical) — closed for CLI path.
Copy-storm attribution: per-step small copies total 0.035 ms/tok,
negligible — closed as a lever. Roofline: model 2.74 GB at 800 GB/s =
3.43 ms minimum weight read; wvSplitKSml at 57% bandwidth is the top
headroom target. Next: v_dot2_f32_bf16 to reduce compute bottleneck.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…y-bound kernel

The dot2 instruction replaces 5 scalar f32 ops with 1 per bf16x2 pair,
but wvSplitKSml is memory-bound at 57% bandwidth — compute is already
hidden behind memory latency. Paired A/B on idle host (load 0.55): OFF
88.784 vs ON 88.897 (+0.13%, WASH). All 5 pairs differ (reduction order
change) but both outputs are coherent analytic prose. The runtime branch
also caused a 12% regression in the OFF path from code-size pressure;
reverted and 100.47 tok/s confirmed restored.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:ox-alpha [omp]
…vqRow

Replace scalar Dp4a emulation with amd_mixed_dot(char4,char4,...) in the
MMVQ Q4_K/Q5_K/Q6_K inner loops. The scalar Dp4a pattern
(reinterpret_cast<int8_t*> + multiply + add) does NOT auto-lower to
v_dot4_i32_iu8 when the input comes from a shift+mask expression because
the compiler cannot prove the values fit in 8 bits. amd_mixed_dot forces
the hardware dot instruction, reducing instruction count by ~10x and
VGPRs by 30-43%.

ISA verification (gfx1100, -O3):
  Q4_K:  0->8 v_dot4, 31->0 v_mul, 62->6 v_bfe, 68->42 VGPRs
  Q5_K:  0->8 v_dot4, 31->0 v_mul, 46->6 v_bfe, 73->51 VGPRs
  Q6_K:  0->16 v_dot4, 30->0 v_mul, 46->6 v_bfe, 86->49 VGPRs

Correctness: gate 16/16, 839 assertions. Output BYTE-IDENTICAL to
scalar baseline (integer arithmetic is exact regardless of evaluation
order). Body coherence verified — analytic prose, no degeneration.

Matched-load A/B (loadavg ~1.0, 5 runs each):
  baseline median: 90.133 tok/s
  v_dot4  median: 92.570 tok/s  (+2.7%, all fixed > all baseline)

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:glm-5-2 [omp]
…ted — engine wash

T20 rewrote KQuantGemvMmvqRow for full-warp cooperation (32 threads per
super-block, zero intermediate barriers, single warp_reduce_sum). The
kernel wins 2.4-3.1x on large grids (lm_head Q6_K 248320x2560: 2134->682us)
but the engine A/B is a wash: ON 92.9 vs OFF 92.8 tok/s median (5 paired
reps, full campaign config).

The kernel win doesn't reach the engine because the dominant Q4_K path
(2.46 ms/tok, 25% of wall) has small grids (ffn_gate/up ~576 rows) where
the kernel is launch-overhead-bound, not reduction-barrier-bound. The Q6_K
path (1.20 ms/tok) is mostly small-grid ffn_down (22 calls/tok, 1.03x).
The large-grid lm_head save is 1 call/tok — invisible when averaged.

Conclusion: kernel micro-optimization is exhausted for the dominant paths.
The 4.2 ms/tok overhead above the 4.38 ms/tok weight-read floor is the
bottleneck. Path to 200 tok/s requires launch-overhead reduction (HIP
graph capture, kernel fusion, or persistent kernels).

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:GLM-5-2 [OMP]
…ctions

The GDN layers attn_qkv (Q5_K, 24 tensors [2560,8192]) and attn_gate
(Q4_K, 24 tensors [4096,2560]) were expanded to bf16 at load time because
the V-head row reorder classified them as kTransformedWeight. The reorder
is a ROW permutation — quantization blocks are along the K (column)
dimension and are self-contained per row — so it is block-safe. T21 routes
these tensors as kMatmulWeight to allow keep-quant, copies the blocks via
OwnGgufQuantBlocks(mmap_src=nullptr), and applies ReorderVRows to the
block bytes at load time. The forward pass already dispatches quantized
nk=true weights through vt::MatmulBT, so no forward-pass change was needed.

A/B: +3.9% (87.4 to 90.8 tok/s median, 5/5 pairs). Gate 16/16, 839
assertions. Output coherent but not byte-identical (Q5_K integer dot
product vs bf16 float MAC). VT_GDN_ROWPERM_KEEP_QUANT=0 reverts to the
old bf16 expansion path for A/B isolation.

The improvement is less than the projected 14% because the Q5_K GEMV
kernel has lower effective bandwidth on small grids (n=2560) than
assumed, and wvSplitKSml is more efficient on these grids than projected.

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:glm-5-2 [omp]
…consumers

The prior bridge invalidated the producer token on any non-matching
K-quant consumer query, forcing standalone QuantizeQ8KK launches when
a non-matching GEMV (e.g. attn o_proj reading the attention output)
queried between the norm producer and a matching consumer that shares
the normed hidden state. A non-matching consumer reads a DIFFERENT
activation buffer and cannot stale the token — only a new producer
overwrites it. Removing the invalidation lets matching consumers
reuse the fused scratch even after irrelevant queries intervene.

The gate test confirms: a non-matching query no longer invalidates,
and a subsequent shape-matching call on the original buffer still
reuses the fused scratch (consumers_fused == 1, was 0). Byte-identical
outputs (5/5 A/B pairs, 1068 bytes). Gate 16/16, 839 assertions.

Correctness: the token records the PRODUCER's output pointer and
scratch. A non-matching consumer reads a different buffer that cannot
overwrite the producer's output or scratch. The token is overwritten
only when a new producer calls NormQuantRecordProducer. Stream-ordering
is unchanged (single stream, sequential dispatch).

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:GLM-5-2 [TOOL]
…pKernel

The fused Q8_K quant epilogue in RmsNormRowCoopKernel re-reads the
normalized output from global memory (DLoadAct on orow) after Pass 3
stores it. On gfx1100 the 5 KB bf16 row (h=2560) competes with the
weight and input in the 16 KB L1, so the re-read can miss to L2.

T24 stores the normalized row to dynamic shared memory during Pass 3
(when the value is already in registers) and reads from LDS in the
quant epilogue, eliminating the global re-read. The LDS buffer is
h * sizeof(Tout) bytes (5 KB for bf16 h=2560), well within the 64 KB
per-CU limit.

Env gate VT_RMSNORM_LDS_QUANT (default ON) controls the optimization:
set to 0 to revert to the global re-read path for A/B isolation. The
gate is read per-call so captured graphs and in-process tests pick it
up at dispatch time.

Byte-identity: the LDS store uses the same conversion as Store (bf16
RNE for bf16 output, exact copy for f32), and DLoadAct reads the same
bytes from LDS as from global. Gate test: 16/16 cases, 839 assertions,
all passed.

A/B measurement pending: the co-tenant 27B model holds the GPU VRAM,
blocking the acceptance workload. The A/B script is staged at
agent-artifacts/tg200-t24/ab-t24.sh for when the GPU is available.

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:GLM-5-2 [OMP]
ssm_out (out_proj) is Q5_K in the GGUF checkpoint but was expanded to bf16
at load time because the V-head column reorder (ReorderVCols) cuts across
Q5_K 256-element block boundaries. T25 keeps the weight in tiled Q5_K order
(no ReorderVCols) and permutes the 4096-element GEMV input from grouped to
tiled order at runtime instead, cutting weight bandwidth ~4x (Q5_K ~5 MB vs
bf16 20 MB per call).

The permutation is a simple gather of 128-element groups within each of the
4096-element rows, gated by VT_GDN_COLPERM_KEEP_QUANT=1 (default OFF). A new
out_proj_tiled flag on GdnLayerWeights distinguishes the tiled Q5_K path
(needs input permutation) from the gdn_expand_nk bf16 path (already
column-reordered, no permutation needed) — the nk flag alone conflates both.

A/B (5 interleaved pairs, --max-tokens 256 --temperature 0 --seed 0):
OFF median=90.930 tok/s, ON median=91.703 tok/s, +0.85%, 5/5 ON>OFF.
Output coherent but NOT byte-identical (Q5_K vs bf16 weight precision).
Gate test: 16/16, 839 assertions.

The improvement is modest because the permutation kernel launch overhead
(~13.4 us x 24 calls = ~322 us/tok) offsets most of the weight bandwidth
savings (~368 us/tok). The net gain is ~46 us/tok.

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:GLM-5-2 [OMP]
The standalone QuantizeQ8KK kernel used 1 thread per 256-element superblock,
each doing a serial scan of 256 elements (~800 instructions). For decode
(m=1, nsb=10) only 10 of 128 threads were active, and on wave32 each thread
is its own wave, so the kernel took ~13.4 us/call = 540 us/tok (6.0% of
wall time).

The new QuantizeQ8KKWarpCoop kernel uses 8 threads per superblock (32
elements each). The amax scan is done per-chunk (ascending, ax > amax
first-occurrence), then reduced across 8 threads via __shfl_xor_sync with
lower-chunk-index tie-break — equivalent to a sequential scan of all 256
elements. The quantization (iscale = -127/mx, DNearestInt, clamp 127) and
bsums are order-independent. Output is BYTE-IDENTICAL to the original
QuantQ8KSBlock, asserted by the gate test (16/16, 839 assertions) under
VT_QUANT_Q8K_WARP=1.

For m=1, nsb=10: 1 block, 80/128 threads active (vs 10/128), 3 waves of
~100 instructions (vs 10 waves of ~800) = ~8x fewer wave-cycles.

A/B on acceptance workload (Qwen3.5-4B Q4_K_M, 256 tokens, temp 0, seed 0):
  OFF median: 91.532 tok/s
  ON  median: 93.417 tok/s
  +2.06%, 5/5 pairs ON>OFF, all 5 byte-identical (1039 bytes)

Gated by VT_QUANT_Q8K_WARP (default OFF, read per-call).

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:GLM-5-2 [OMP]
The Q6_K GEMV inner loop ran two amd_mixed_dot calls per iteration:
one for the value dot product (sub = v . W8) and one for the bias
correction (pre = 32 . W8), then subtracted. Since (v - 0x20) . W8
equals sub - pre exactly, subtracting the bias word before the dot
product eliminates the second dp4a entirely. Unsigned byte subtraction
wraps to the correct signed char value (v < 32 maps to 256+v-32, which
is signed v-32), so the char4 reinterpret is correct.

Also sets kGemvWarps from 4 to 8 for better L2 weight sharing on
gfx1100; measured neutral-to-positive across the acceptance workload.

Measured: 96.0 tok/s median (5 reps, 256 tokens, all coop + GQA4 env
vars), unchanged from baseline. The Q6_K path is 15% of decode GPU time
and the kernel is memory-bound, so halving the dot-product count does
not move the wall clock. The change is kept because it reduces ALU
pressure without a correctness cost.

Correctness: test_rocm_quant_dot passes. The fused dot product is
bit-exact with the original two-dot formulation because unsigned
subtraction modulo 256 equals signed subtraction for 8-bit values.

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:glm-5-2 [omp]
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.

2 participants