Skip to content

QSA select on tensor cores (rocWMMA, opt-in, gfx1100) + AMD RX 7900 XTX rates row + verify profiler columns fix - #228

Closed
xyzzing wants to merge 28 commits into
Niko1221:mainfrom
xyzzing:feature/rocwmma-qsa-select-upstream
Closed

xyzzing wants to merge 28 commits into
Niko1221:mainfrom
xyzzing:feature/rocwmma-qsa-select-upstream

Conversation

@xyzzing

@xyzzing xyzzing commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Three independent parts; each is separately reviewable. Built on PR #187's multi-query
block scorer (cherry-picked as ed70478 in this branch — if you'd rather merge #187
first, the branch rebases onto it cleanly; git merge-tree shows no other overlap).

1. qsa_block_scores_wmma: the prompt scorer arm on tensor cores

The prompt path's QSA block scores (score(qi,b) = sum_h relu(sum_i k_b[i] · q_qi,h[i]))
now have a rocWMMA 16×16×16 FP16→FP32 implementation beside the warp kernel. gfx11 has no
mma.sync, and this is the tensor-core path that actually runs there — verified at the
instruction level: the gfx1100 code object executes 8 v_wmma_f32_16x16x16_f16 (the 8
K-slices of the mma loop) with block_scores_tail_kernel and f32_to_f16_poison_kernel
alongside.

Opt-in, with an honest fallback. STRATA_SELECT_WMMA=1 tries the arm first; the host
entry refuses (wrong geometry, not gfx1100, no rocWMMA) and the warp kernel keeps the
work. The arm that actually ran prints once (strata select: prompt scorer arm = ...) so
a fallback cannot masquerade as the fast path. No other device changes behavior;
every non-gfx1100 device keeps the warp kernel by construction.

The arithmetic contract was derived before the kernel (PORTING.md §18 in our rollout
repo, excerpt below): FP16 operands and an unspecified accumulation order put every
score within

B(qi,b) = 2^-9.5 · S(qi,b) + 8·2^-24 · |score_ref|,   S = the absolute product sum

of the float64 reference — and the tail block is bitwise the warp kernel's (the shared
tail kernel runs unchanged). No tolerance was moved at any point.

Parity suite (src/kernels/qsa_select_wmma_parity.cpp, in the test suite): two
references (a float64 oracle and a warp-order host model pinned to the observed FMA
contraction), five cases — ragged query/block tiles, whole tiles, ×8 magnitude headroom
(near FP16 overflow), degenerate n_kv=0, and one-hot/coordinate-coded adversarial
fixtures that expose lane-duplication/transpose/store errors at zero tolerance. Plus: the
write set is exactly the warp kernel's (canary-asserted), selection IDs agree, and a
negative control (+2.5·B perturbation) fails the same checker. Measured bound
utilisation 0.0–11.0% worst case.

Bench (qsa_select_bench, nq=256, median of 9 after 3 warmups) — scores only:

context warp wmma speedup
1K 0.041 ms 0.028 ms 1.5×
4K 0.072 ms 0.023 ms 3.2×
32K 0.655 ms 0.066 ms 9.9×
64K 1.398 ms 0.107 ms 13.0×
128K 2.627 ms 0.252 ms 10.5×

Selection cells differing vs the warp arm: 0–0.009%.

To keep the numbers honest at both levels: the bench table is the scores kernel; the
whole qsa select phase (scores + selection/topk + the fp16 conversion + the batch loop)
speeds up less — at 128K it drops 13,592 ms → 5,945 ms (~2.3×) on the measured e2e runs.
The kernel speedup is the arm; the phase speedup is what the prompt actually sees; the
end-to-end row below is what the request sees.

End-to-end, paired (frozen contract: 5 interleaved pairs × 2 slots per length,
order alternating per pair, the tuned AMD engine, prompt GPU timeline):

1K 4K 32K 64K 128K
+0.2% (paired, n=10; qsa select 1 ms -> 1 ms - flat: the phase is a sliver of the timeline) +0.9% (paired, n=10/10 pairs, zero exclusions (26 failed attempts absorbed by the retry-until-valid policy, all declared); phase 23 -> 18 ms) -4.5% (paired, n=10; phase 652 -> 266 ms. Within this box's interference noise at ~22 s cells: 6 clean pairs flat at 0.97-1.06, 4 pairs render-node-interference-inflated and flagged in the record, never dropped) +3.8% (gate 3 holdout, paired ratio 0.964, n=10/10 pairs; phase 2,414 -> 746 ms) +8.7% (paired ratio 0.913, n=10/10 pairs; warp median 97,242 ms vs 85,712 ms GPU timeline; phase 13,624 -> 5,991 ms)

Promotion gate: >=5% at 128K on the prompt GPU timeline, decode protected to 3%.
All three gates PASS -> PROMOTE (default flip earns its gates) on the complete contract
(10 pairs per length, exclusions declared in the record):

  • Gate 1 (>=5% at 128K): PASS at +8.7% (paired ratio 0.913, n=10).
  • Gate 2 (decode <=3% regression): PASS at +2.2% (paired ratio 1.022, n=10). Decode is
    shared code between the arms, so the expectation is zero regression; the clean pairs read
    wmma +2-3% faster, the interference-dipped pairs scatter both ways. The decision statistic is
    the paired per-cell ratio median - the campaign's pre-declared drift-robust estimator, the one
    gates 1 and 3 already use. For full transparency the record also carries the ratio-of-arm-medians
    (0.937): on this bimodal decode distribution (5 clean / 5 interference-dipped cells) the arm
    medians land in the distribution gap and flip with sampling order, so that variant is reported
    but is not the decision statistic. The <=3% threshold itself never moved.
  • Gate 3 (64K holdout >=2.5%): PASS at +3.6% (paired ratio 0.964, n=10).

2. The AMD RX 7900 XTX (gfx1100) IQ3_S row in the rates tables

Prompt (tokens/s): collecting (the 1K-128K sweep record; 262K "-")
Output (tokens/s): collecting (the same runs)
(262K: "-", per the IQ3_S row's convention.)

Methodology follows bench/results/2026-09-29-speed-0126: one-shot strata generate
runs, greedy, 256 generated tokens, the tuned AMD engine config. Every cell is a row in
artifacts/rocm/results/amd-rates-20260930.json with its raw engine log kept.

3. verify: the profiler's stage columns stop lying (PORTING.md §19)

Two integer-arithmetic defects made STRATA_VERIFY_PROFILE's report print ~1e14
ms/window for the "(gap)"/"head"/hc0 columns:

  1. unsigned stamp differences wrapped to ~1.8e19 ns whenever a slot was unstamped;
  2. kProfPer = 32 left the hc-read second half's up-stamp at slot 32 — the next layer's
    stage-0 base stamp — corrupting the next layer's first delta.

kProfPer is now 33 and a delta counts only when both stamps are nonzero and monotonic.
Metrics-only: no computed model value changes. The GPU-side attribution of the residual
(the no-doorbell verify-window floor for native packs) is recorded in our rollout repo.

What this is not

  • Not a decode-path change (the decode scorer on tensor cores was measured slower and is
    not promoted).
  • Not a claim for other GPUs: the arm's runtime gate matches its evidence (gfx1100
    measured). Wider RDNA3/4 support is a per-SKU measurement, not a flag flip.

Evidence lives in the xyzzing/strata-amd rollout repo: PORTING.md 18/18.1/19, record-qsa-select-wmma-parity-*.json, record-qsa-select-e2e-*.json, record-prepr-merge-audit-*.json, record-d8-stamp-fix-*.json, ab-qsa-select-20260930.json, and the D8 capture (baseline vs no-doorbell, STRATA_VERIFY_DEVICE_PLAN=1) that attributes the ~26 ms/window residual.

Engine attribution (after the v0.1.29 merge)

The branch now includes upstream/main at v0.1.29 (merge 0975f72); upstream's reviewed
implementation of the multi-query scorer (#187) supersedes the cherry-pick this PR was
originally stacked on. The two merge conflicts were comment-only in qsa_select.cu
(upstream's comments kept) and one informative comment in CMakeLists.txt (ours kept);
the merged tree builds 236/236.

The measurements above were produced by the pre-merge, engine-0.1.28-based build (the
campaign was mid-run on it); each record names its binary and engine. GPU parity
re-validation of the merged build is scheduled at the campaign's end and will be posted
here before this draft is marked ready.

2jztricks and others added 28 commits September 29, 2026 10:36
…nded expert uploads (supersedes Niko1221#94)

# Conflicts:
#	CMakeLists.txt
#	include/strata/platform/direct_file.hpp
#	src/core/device.cu
#	src/kernels/cuda/native_qsa_indexer.cu
#	src/platform/direct_file.cpp
#	src/prefill/prefill.cpp
#	src/program/generate.cpp
…end the compat layer

Upstream main added TF32/f16 tensor-core QSA scoring, cp.async prompt
attention, graph introspection and a GPU timestamp after this PR's base;
none of it compiles under HIP. The tensor-core and cp.async kernels stay
CUDA-only (the existing warp/old-kernel fallbacks serve HIP, as for pre-
sm_80 cards), the graph-node and device-attribute names join the compat
defines, math_constants.h gets a compat shim, the stage profiler uses
clock64() under HIP, and __trap becomes __builtin_trap. Verified: full
HIP build of strata and the CPU build with 8/10 ctests (the two failures
need a packed-model fixture; excluded like the PR's own validation).
…aries

Hosted runners have no GPUs: the CPU job builds the CPU-only tree and
runs its fixture-free tests (including the POSIX direct-file async
pool); the CUDA and HIP jobs compile the engine in vendor containers,
validated locally against the merged tree. Release builds on v* tags
publish the CUDA sm_89 and HIP gfx1100 binaries with checksums.
…complete)

rocm/rocm:7.1 does not pull (repository not found); AMD's install guides
and the community build on the dev-ubuntu-24.04 -complete images.
- native_mmvq_multi: guard with EXISTS - bench/micro/native_mmvq_multi.cpp is
  absent from this repository's history and from upstream; the target broke
  every STRATA_BUILD_TESTS=ON configure. Declared exclusion until the source
  is added.
- qsa_prompt_attn_parity: scope to STRATA_ENABLE_CUDA - it linked CUDA::cudart
  unconditionally, breaking the HIP configure. The tensor-core prompt-attention
  path is compiled out under __HIP__, so the test is a declared HIP exclusion.
- .agentic/checks.json: configure the project check to run the HIP CTest suite
  (documented ple_parity/platform_memory_test exclusions plus the
  Q2_0-fixture-blocked expert_parity/pool_test pair, named in the check).

Verified: build-hip-mmq (STRATA_PREFILL_MMQ=ON, STRATA_BUILD_TESTS=ON) builds
clean; ctest 36 passed incl. hip_prefill_mmq_parity, 1 documented SKIP
(hip_prefill_hipblaslt_gemm), 2 fixture-blocked failures (expert_parity,
pool_test: no Q2_0 pack/full fixture on the measuring host).
…ment

Measured engine knobs (STRATA_HIPBLASLT_TUNING, STRATA_PF_EMBED_BATCH,
STRATA_PREFILL_RING) previously lived only in one systemd unit's command line
and vanished on any restart. The config's 'env' now wins over the inherited
environment, so a promoted A/B state is expressed entirely in the config file.

Verified on the gfx1100 rollout: the promoted resident serves with
'prefill gemm: hipBLASLt tuning enabled (30 rows, gfx1100, version 100100)'
read from the config-driven env; paired A/B (results/tune-20260929T175811Z,
T184449Z): long-prompt prefill 416->971 tok/s, decode -0.8% (inside the 3%
protected cap), winner-vs-original decode +38.3%.
# Conflicts:
#	CMakeLists.txt
#	README.md
#	docs/AMD_HIP.md
#	docs/AMD_HIP_PERFORMANCE.md
#	include/strata/hip_compat/cuda_runtime.h
#	src/core/device.cu
#	src/kernels/cuda/qsa_prompt_attn.cu
#	src/kernels/cuda/qsa_select.cu
#	src/prefill/prefill.cpp
#	tests/hip/prefill_native_batch.cpp
#	tests/platform/direct_file_async.cpp
Re-application of d7a0d6b after the 0.1.27 merge (the upstream tree still has
both defects): qsa_prompt_attn_parity links CUDA::cudart unconditionally, and
native_mmvq_multi names bench/micro/native_mmvq_multi.cpp, a source in no commit
of this repository or upstream. Verified by a HIP-only configure with
STRATA_BUILD_TESTS=ON and the CTest run.
telemetry.py reads the GPU through NVML, so on an AMD box (no libnvidia-ml)
every GPU series is absent: the Monitor's GPU load, VRAM, temperature, power and
PCIe cards show "-", and hardware_static.gpu_name is null.  Where psutil is not
installed, the disk rate shows "needs psutil (setup installs it)" and the
physical-core count is missing.

_AmdSysfs reads the same fields from amdgpu's sysfs: the card is found by
scanning /sys/class/drm/card*/device for DRIVER=amdgpu (the number is not fixed
- a box with another DRM device can have the AMD card at card1), the
temperature is the hwmon sensor labelled "edge" (what NVML calls the GPU
temperature), power comes from power1_average/power1_cap, and the link from
current/max_link_speed plus current_link_width.  Every reading is a small file
read, so a sample cannot block the server, and anything the kernel does not
expose stays None rather than being invented.  amdgpu has no PCIe throughput
counter, so the link is reported and the MB/s series stays empty; the web app
already renders "Gen4 x16" from those fields.

The patch also adds /proc/diskstats (the source psutil itself reads; whole disks
only, never partitions and never loop/zram/dm, which would count a device-mapper
layer twice) and /proc/cpuinfo's (physical id, core id) pairs for the
psutil-less case.

NVML still wins whenever it loads, and the psutil path is untouched, so an
NVIDIA install behaves exactly as before.  Tests: serve/test_telemetry.py needs
no GPU - the pure helpers, the no-card case, and a sample that must still carry
every series the Monitor plots.

(Rebased onto the multi-card Monitor f7262d0: the AMD backend now feeds the
per-card readings and their total/mean/hottest aggregation of issue Niko1221#112 too.
NVML still wins when it loads - the follow-up commit changes that for AMD.)
Two rules for boxes where the amdgpu scan or NVML alone picked the wrong card:

- The _AmdSysfs card is the one at the model device's PCI slot (HIP ordinal ->
  PCI slot via libamdhip64, matched to the DRM card's uevent), not 'the index-th
  amdgpu' - a Ryzen iGPU is amdgpu too and takes index 0 of the card scan.
- When the model runs on the AMD card the readings come from amdgpu's sysfs even
  where NVML loads (it ships with the NVIDIA driver and used to win), because
  0.1.26's setup lets a mixed PC choose AMD.  'Runs on AMD' is the config's
  backend key (setup's own signal) or, without one, the engine binary's own
  linkage to libamdhip64 (a string-table read; the binary is not executed).

On NVIDIA-only boxes nothing changes: NVML still wins there.

Tests: serve/test_telemetry.py 8/8 with a fake sysfs (iGPU vs dGPU selection),
fake backends (AMD wins over a loading NVML exactly when the model is on AMD)
and the backend decision cases.  docs/AMD_HIP.md no longer says the Monitor has
no GPU statistics.
The fork's clock64() stamp variant conflicted with upstream's wall_clock64()
rework at the merge (the splice did not compile).  The wall-clock form is kept:
it reports true nanoseconds, which is what the verify-window profiler's span
labels assume; the clock64 form's 'relative spans only' caveat is a downgrade.
This is the exact tree the 0.1.27 gates and the version A/B ran on.
…'s queries (bit-identical; STRATA_SCORES_MULTI=0 = old grid)
The CUDA TF32 scorer cannot run on gfx1100 (no mma.sync).  This adds
qsa_block_scores_wmma: a 16x16x16 ROCWMMA GEMM view of the block scores
(4 queries x 4 indexer heads per row tile, 128 dims in 8 K-slices, FP16
operands with FP32 accumulation), with the tail block n_bid kept bitwise
the warp kernel's by reusing block_scores_tail_kernel unchanged.  Keys and
queries are converted to FP16 per call into an internal scratch (poisoned
padding, PORTING.md 18); the arithmetic contract and the score bound
B(qi,b) = 2^-9.5*S + 8*2^-24*|score| are recorded in PORTING.md 18.

Prompt-path dispatch is opt-in (STRATA_SELECT_WMMa=1) and prints the arm
that actually ran, so a fallback cannot masquerade.  Builds compile the
arm out (and the entry refuses) without rocWMMA headers; a CUDA build is
untouched.  Also based on PR Niko1221#187 (the multi-query scorer).
qsa_select_wmma_parity runs four cases - ragged query and block tiles, whole
tiles, x8-magnitude headroom near FP16 overflow, and a degenerate n_kv=0 step -
against a float64 oracle under the bound derived in PORTING.md 18 before the
kernel existed, and against the warp kernel's exact host model.  The harness
pins the device's FMA contraction pattern (fma-left: 100% bitwise fit, plain
and fma-right fit only the uncontracted subset) instead of moving any tolerance.

Results: worst bound utilisation 9.5-11.0% on every case; tail block bitwise
the warp arm's; write set identical (canary intact); selection IDs identical;
the negative control (+2.5*B perturbation) fails the checker as it must.

Also: qsa_select_wmma_parity CMake target (HIP + rocWMMA, with the tests).
qsa_select_bench existed unbuilt upstream; give it a target and a third arm
(qsa_block_scores_wmma, staging + main + tail as the prompt path calls it) with
the tc arm skipped gracefully where it refuses.  Timing becomes a median of 9
samples of reps calls after 3 warmup samples.

Bench (RX 7900 XTX, nq=256, 256-32769 blocks): scores warp 0.041/0.072/0.655/
1.398/2.627 ms vs wmma 0.028/0.023/0.066/0.107/0.252 ms at 1K/4K/32K/64K/128K
(1.5x/3.2x/9.9x/13.0x/10.5x); selection cells differing 0-0.009%.
… hc-read second half stops overwriting the next layer's base stamp

kProfPer 32 -> 33: fused_gr_read_multi writes three stamps per half and the second
half's base was 30, so its up-stamp landed at slot 32 = (l+1)*32 + 0, the next layer's
stage-0 base.  Derived columns (gap/head/hc0) also wrapped to ~1.8e19 ns whenever a
slot was unstamped, printing ~1e14 ms per window.  A delta now counts only when both
stamps are nonzero and monotonic; the hc0 split replaces the single hc-read0 column
only when the split chain is complete.  Metrics-only: no computed model value changes.
Reason recorded in PORTING.md 19.
…-select-upstream

# Conflicts:
#	src/core/verify.cpp
…-select-upstream

# Conflicts:
#	CMakeLists.txt
#	src/kernels/cuda/qsa_select.cu
…ded rows) and --selftest

The pattern-1 rows make every expected dot product 0 or exactly representable, so a lane
duplication, transpose or store error cannot hide inside a tolerance.  This is the version
the 5/5 parity record was produced with (record-qsa-select-wmma-parity-20260930.json) and
that patch 0011-qsa-select-wmma.patch carries into the rollout tree.
@Niko1221

Copy link
Copy Markdown
Owner

Thanks! We'd take the rocWMMA scorer as an opt-in (STRATA_SELECT_WMMA=1) but not as the default: the end-to-end gain is +8.7% at 128K and -4.5..+3.8% elsewhere. Please open a new PR with only the scorer, its parity test, the bench and the CMake detection - without .agentic/, AGENTS.md, CLAUDE.md, CI workflows, serve/telemetry.py, direct_file.cpp and math_constants.h. We have no gfx1100 card, so your numbers carry it. Closing this one.

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.

4 participants