Repository navigation
perf(ArcKernels/ArcQuant): FP8 dense-GEMM lane — pow2 scale shifts, dispatch engagement log, three-arm sweep harness, SASS probe - #218
Merged
Conversation
Code Metrics Report━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Language Files Lines Code Comments Blanks ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ C Header 5 305 210 52 43 CSS 2 1181 1036 34 111 CUDA 81 29475 20161 6275 3039 Dockerfile 1 39 22 8 9 JavaScript 16 3546 2676 482 388 Jinja2 7 694 656 5 33 JSON 75 4896 4893 0 3 Makefile 1 6 5 0 1 Metal Shading Lan| 33 12224 9431 1142 1651 PowerShell 1 300 227 30 43 Python 147 15371 12677 824 1870 Shell 42 10304 6858 2769 677 Plain Text 4 3801 0 2479 1322 TOML 33 1498 1294 54 150 YAML 3 25 23 2 0 ───────────────────────────────────────────────────────────────────────────────── HTML 4 2687 2604 43 40 |- CSS 2 543 479 37 27 |- JavaScript 1 1233 1215 12 6 (Total) 4463 4298 92 73 ───────────────────────────────────────────────────────────────────────────────── Jupyter Notebooks 4 122 83 23 16 |- Markdown 1 60 30 22 8 |- Python 1 122 113 1 8 (Total) 304 226 46 32 ───────────────────────────────────────────────────────────────────────────────── Markdown 213 48932 0 38081 10851 |- BASH 72 1655 1203 331 121 |- C 4 19 19 0 0 |- CUDA 2 84 56 16 12 |- JSON 19 779 779 0 0 |- PowerShell 1 1 1 0 0 |- Python 23 1008 787 113 108 |- Rust 68 2063 1727 78 258 |- TOML 6 207 164 0 43 |- YAML 5 41 36 5 0 (Total) 54789 4772 38624 11393 ───────────────────────────────────────────────────────────────────────────────── Rust 681 340988 293252 18096 29640 |- Markdown 504 32562 471 28063 4028 (Total) 373550 293723 46159 33668 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Total 1353 516771 363188 99077 54506 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
The native branch flattens `[B, T, hidden]` to `[B*T, hidden]` before its GEMM. The dequantize + cuBLASLt branch did not — it passed `x` through untouched. V4's activation is rank-3, and bias is `None` on every V4 linear, so `UnquantLinear::forward` took `w.broadcast_left(B)` into `cublaslt.batch_matmul` with `stride_b = 0`. At decode T=1, making it a **B-way batched GEMM with m=1**: a batched GEMV wearing a GEMM's name, and the one shape cuBLASLt has no advantage in. The documented 27x was measured at prefill, where `[1, 512, hidden]` collapses to batch=1 and the call is a real GEMM. This invalidated the threshold A/B rather than merely slowing it: lowering `ARC_FP8_CUBLAS_MIN_M` selected the batched-GEVM shape, so the sweep was comparing the tiled kernel against a degenerate call and would have supported "cuBLASLt loses at decode" — a conclusion about a shape the change was never meant to select. Flattening makes the two branches comparable, which is the premise the A/B rests on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lement scale divisions
get_scale in fp8_matmul_tiled performs TWO runtime signed integer divisions
per weight element staged into shared memory (n / block_size_y, k /
block_size_x). Both divisors are kernel arguments, so nvcc expands each into
a ~15-20 instruction reciprocal-and-fixup sequence — paid once per element of
every weight tile, ~33.9G times per B=256 decode step by the source-derived
count. The shipped weight_block_size is [128, 128]: both powers of two.
This adds a POW2_SCALE template arm: the host launcher tests pow2-ness once
per launch, computes shift = log2(block_size) on the host, and the kernel
indexes the scale grid with two SHFs instead. Non-power-of-two geometries
keep the division path, unchanged.
Bit-identical by construction: for the non-negative indices used here,
n >> log2(d) == n / d for every input — same quotient, same scale word
fetched, same arithmetic after it. Dispatching on POW2_SCALE can never change
what an A/B leg measures, only how fast the scalar arm runs.
The C ABI (launch_fp8_matmul_{f16,bf16}) is unchanged; no Rust edits needed.
Compile-unverified locally (macOS has no nvcc); covered by the nvcc CI lane
(.github/workflows/cuda_compile_check.yaml), which builds this TU for
sm_80/sm_90 on every PR touching it.
The bank-conflict half of this lane's brief (+4 -> +1 tile padding) was
already landed by PR #215 and is an ancestor of this branch; the kernel's
stride-33 comment documents the bank arithmetic.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WK8VgocBUrr5djjE1ZfCNf
…P8 kernel served a forward Every threshold sweep on this lane needs an engagement assertion: a server-log line proving which kernel actually ran, per leg. None existed — a leg could silently measure the wrong arm (the ARC_NO_DEDICATED_DECODE incident produced two void A/Bs exactly this way, both arms running the same code). ARC_LOG_FP8_DISPATCH=1 (read by VALUE via env_flag_is_set, latched once per process) makes each dispatch path print ONE line per process: [arc-fp8-dispatch] path=<gemv_wide|gemv_warp|wmma|tiled|dequant_cublaslt> first_shape=mM_nN_kK Sites: the native dispatch chain in fp8_blockwise_matmul_impl (both dtype arms, mirroring the if/else exactly) and the ARC_FP8_CUBLAS_MIN_M divert to dequantize+cuBLASLt in BlockwiseFP8Linear::forward. Cost when unset: one latched bool read. When set: one relaxed atomic swap per call — no locks, so enabling it does not perturb the rates a sweep records. Registered in mistralrs-core/tests/capability_reachability.rs (Status::Live); the registry run passes (11/11). The Rust is cfg(cuda) — compile-unverified locally on macOS; covered by the cuda-typecheck job of the nvcc CI lane. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WK8VgocBUrr5djjE1ZfCNf
… allowed to move ARC_FP8_CUBLAS_MIN_M arc_fp8_cublas_min_m's doc and the WMMA kernel's doc both forbid merging a threshold from a two-arm result: the only sweep ever taken predates both the WMMA GEMM (#200) and the rank-3 flatten, so it compared cuBLASLt-as-batched- GEMV against the scalar kernel only. This script is the three-arm re-run they demand, on ONE binary, env toggles only, fresh server per leg (the gates are OnceLock-latched, so re-exporting into a live server would silently measure the previous leg). Design: the threshold candidates {5, 8, 64, 256, 512} are swept as the OFFERED DECODE BATCH M (a grid that varies only the env at one fixed batch routes every MIN_M <= batch leg to the same kernel and cannot name a crossover in M), and each ladder point runs all three arms: (a) cublaslt: ARC_FP8_CUBLAS_MIN_M=5 (b) wmma: ARC_FP8_CUBLAS_MIN_M=1000000, ARC_NO_FP8_WMMA=0 (c) tiled: ARC_FP8_CUBLAS_MIN_M=1000000, ARC_NO_FP8_WMMA=1 A leg counts only if ALL of: 1. ENGAGEMENT — its expected [arc-fp8-dispatch] path= line present in the server log and both rivals ABSENT (a WMMA leg whose log shows tiled is eligibility silently failing: VOID, loudly); 2. FLOOR — summed usage.completion_tokens >= MIN_TOKENS_FLOOR (default 1000); the rate divides by tokens the server SAYS it generated, never by requested max_tokens; 3. CANARY — greedy fixed-prompt stream vs the baseline leg (tiled @ M=5); cross-arm divergence is expected numerics and is REPORTED with its first index; a degenerate stream (<5 tokens / no finish_reason) voids the leg. The summary table names, per M, cublaslt vs min(wmma, tiled) and the FIRST clean M where cuBLASLt wins — the value ARC_FP8_CUBLAS_MIN_M may move to. No clean crossover => the default stays 512. Script only — NOT run here (no GPU; D14). House lock/preflight/provenance discipline copied from arcspec_token_identity_b8.sh. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WK8VgocBUrr5djjE1ZfCNf
…census + WMMA HMMA presence Two questions this settles with nvcc + cuobjdump alone, no GPU: 1. Whether the 4-way bank-conflict cost model for fp8_matmul_tiled describes the compiled artifact: the model is a claim about SCALAR 32-bit LDS in the inner product, and if ptxas had vectorized those shared loads to LDS.128 its arithmetic would be about instructions that do not exist. The probe prints a per-kernel LDS/LDS.64/LDS.128 census. (Note: the +1 pad that kills the conflict also forecloses LDS.128 on tile rows — scalar LDS is the EXPECTED reading on current source.) 2. Whether the never-executed tensor-core GEMM (blockwise_fp8_gemm_wmma.cu) actually contains tensor-core instructions: HARD FAIL if the TU has zero HMMA/HGMMA — the three-arm sweep would otherwise compare cuBLASLt against two scalar kernels while calling one of them WMMA. Teeth: exactly 4 fp8_matmul_tiled kernels asserted (2 dtypes x 2 POW2_SCALE instantiations — a dropped instantiation makes the census about a kernel not in the build), and every tiled kernel must show >=1 LDS or the extractor lost the function body. Wired into the nvcc CI lane as a step beside the QTIP spill gate, so it runs for sm_80 and sm_90 on every Rust/kernel PR; also runnable standalone: arc-tools/fp8_gemm_sass_check.sh sm_90 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WK8VgocBUrr5djjE1ZfCNf
…pes with cuobjdump -fun extraction
The probe's first CI run went red on itself, honestly: GNU awk treats \< \>
(and warns on \.) as plain characters in ERE, so the awk body-splitter's
counting regexes matched NOTHING, the per-function census returned zero LDS
for all 4 fp8_matmul_tiled instantiations, and the fail-on-zero guard refused
to trust its own zero — which is exactly what it exists for.
This revision removes regex body-splitting entirely: the mangled kernel names
come from the Function listing (awk '{print $NF}', no regex), and
`cuobjdump -sass -fun <mangled>` extracts each function's SASS directly. All
counting is grep -cE with POSIX classes and bracket expressions only — no
\< \> anywhere. Also, per review:
* The two failure modes are now DISTINGUISHED: FAIL[extractor] (no
instruction lines came back — the census is unverified) vs FAIL[zero-LDS]
(body extracted, genuinely no shared loads — the conflict model has no
subject). Body presence is judged on /*<hex>*/ instruction lines, so the
two cannot be confused.
* The VERDICT[tiled] line prints ONLY when the per-function census is
complete and non-empty; otherwise it prints NOT ESTABLISHED. A TU-level
cross-check line (whole cubin, includes the GEMV/MoE kernels) is always
printed, labeled informational.
* HMMA counting moved off the \< \> GNUism onto the same POSIX pattern.
Verified locally against a stub cuobjdump with a realistic SASS fixture:
happy path (4 kernels, LDSM excluded, LDS/LDS.64/LDS.128 counted per
function, scalar arithmetic correct, exit 0) plus three negative controls
(broken -fun -> FAIL[extractor] + verdict NOT ESTABLISHED; LDS-free body ->
FAIL[zero-LDS]; HMMA-stripped WMMA TU -> hard fail), all exit 1. The real
cuobjdump run is CI's — if -fun ever returns nothing there, the probe fails
loudly rather than lying.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WK8VgocBUrr5djjE1ZfCNf
heydryft
force-pushed
the
arcq/fp8-gemm-lane
branch
from
August 21, 2026 20:48
57659c8 to
4c9c07e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
FP8 dense-GEMM lane: scale-division fix, dispatch engagement log, three-arm sweep harness, free SASS probe
Parent systems: ArcKernels + ArcQuant/Legacy (blockwise FP8), ArcLab, ArcGate. Targets
release/openrouter-ready.At B=256 decode,
fp8_matmul_tiledserves all 7 dense projections per layer plus the lm_head — ~50% of the step in the last profile. This PR ships everything that can be shipped for that lane without a GPU, plus the instruments the box A/B needs, and refutes one premise of the brief that was already fixed upstream.What changed
Cherry-pick
445b92063(rank-3→2-D flatten before the cuBLASLt branch). It was NOT an ancestor of this base — it lives only onfix/fp8-cublas-crossover(PR fix(ArcKernels): FP8 threshold was 512 because 512 was the only point measured; cuBLASLt branch never flattened to 2-D #201). Without it, loweringARC_FP8_CUBLAS_MIN_Mon V4 decode selects a B-way batched GEMM with m=1 and the threshold A/B returns a confident false negative. Applied cleanly.Pow2 shift fast path for the tiled GEMM's scale indexing (
mistralrs-quant/kernels/blockwise_fp8/blockwise_fp8_gemm.cu).get_scaleperformed two runtime integer divisions per weight element staged into shared memory; the divisors are kernel arguments, so each expands to a ~15–20-instruction sequence. NewPOW2_SCALEtemplate arm: the host launcher tests pow2-ness once per launch and passeslog2(block_size_{y,x}); the kernel uses two shifts. The shipped geometry[128, 128]always takes it; non-pow2 keeps the division path. Bit-identical by construction:n >> log2(d) == n / dfor the non-negative indices used — same quotient, same scale word, same arithmetic after it. C ABI unchanged; no Rust edits needed for this item.ARC_LOG_FP8_DISPATCH=1engagement lines (ArcLab). Each FP8 dispatch path prints ONE line per process:[arc-fp8-dispatch] path=<gemv_wide|gemv_warp|wmma|tiled|dequant_cublaslt> first_shape=mM_nN_kK. Value-read viaenv_flag_is_set, OnceLock/LazyLock-latched, one relaxed atomic swap per call when on — it cannot perturb the rates a sweep records. Registered inmistralrs-core/tests/capability_reachability.rs(run: 11/11 pass).arc-tools/fp8_threshold_sweep.sh— the three-arm sweep thearc_fp8_cublas_min_mandfp8_wmma_enableddocs demand before the threshold moves. One binary, env toggles only, fresh server per leg (gates are latched per process). The threshold candidates {5, 8, 64, 256, 512} are swept as the offered decode batch M — a grid that varies only the env at one fixed batch routes everyMIN_M <= batchleg to the same kernel and cannot name a crossover in M — and each ladder point runs arms (a) cuBLASLt (MIN_M=5), (b) WMMA (MIN_M=1000000,ARC_NO_FP8_WMMA=0), (c) tiled (MIN_M=1000000,ARC_NO_FP8_WMMA=1). Per leg: engagement assertion (expectedpath=present, both rivals absent — a WMMA leg whose log showstiledis eligibility silently failing and is VOID, loudly), a minimum-token floor (rate divides by summedusage.completion_tokens, never by requested tokens), and a greedy canary vs the baseline leg (tiled @ M=5; cross-arm divergence is expected numerics and is reported with its first index — a degenerate stream voids the leg). The summary names the first clean M where (a) beats min((b), (c)); no clean crossover ⇒ the default stays 512. Not run here — no GPU (D14). No perf claim is made anywhere in this PR.arc-tools/fp8_gemm_sass_check.sh+ a step in the nvcc CI lane (.github/workflows/cuda_compile_check.yaml, beside the QTIP spill gate, sm_80 + sm_90): compiles the tiled TU and prints a per-kernel LDS/LDS.64/LDS.128 census (settles whether the 4-way-conflict model's scalar-LDS premise describes the compiled artifact — note the +1 pad that kills the conflict also forecloses LDS.128 on tile rows, so scalar LDS is the expected reading), asserts all 4fp8_matmul_tiledinstantiations exist, and HARD-FAILS if the never-executed WMMA GEMM contains zero HMMA/HGMMA.Premises verified / refuted against this base
[32][BLOCK_K + 4]" — the bank-conflict fix (+4 → +1, stride 33, with the bank arithmetic commented in-kernel) already landed via PR fix(ArcKernels): FP8 bank conflict + the dispatch threshold's provenance, rebased onto master #215 (250dd6d7f), which is an ancestor ofrelease/openrouter-ready. Nothing to do; this PR does not touch the padding.mistralrs-quant/kernels/blockwise_fp8/blockwise_fp8_gemm.cu; inner loop at :121 (pre-edit),get_scaledivisions at :38–39 — confirmed.fp8_gemv_max_m(default 4) atops.rs:914–928;use_wmmaatops.rs:1197–1200;ARC_NO_FP8_WMMAvalue-read atops.rs:1012;arc_fp8_cublas_min_m(default 512) atmod.rs:657–671— confirmed (some line numbers had drifted from the brief).445b92063not an ancestor: confirmed, cherry-picked (item 1).Follow-up note: per-tile scale staging in shared memory (deliberately NOT done)
With
TILE=32and anyblock_size_y % 32 == 0 && block_size_x % 32 == 0geometry (the shipped[128,128]qualifies), a weight tile spans exactly one scale block, so the whole tile's scale is a single scalar. Staging it would need: a per-k_tile__shared__ float s_tile_scalewritten by one thread, plus a__syncthreads()between that write and the load loop that reads it — a third barrier per k-tile that does not exist today (the current two sit after the load loops and after the FMA loop). After the shift fix, the per-element cost that staging would remove is a same-address__ldg(an L1 broadcast) — trading that for an extra barrier per tile is plausibly a wash or a regression, so it is written up here instead of shipped. If a box profile still shows the scale fetch after this PR, the contained shape is: computes_tile_scalefor tile t+1 during tile t (prefetch into the existing end-of-iteration barrier), uniformity-gated on the host likePOW2_SCALE.CI / verification
cargo check -p mistralrs-quant: pass. Scoped clippy lane (arc-bench arc-engine arc-cuda-graph arc-cli mistralrs-quant --tests --examples -- -D warnings): pass.cargo test -p mistralrs-core --test capability_reachability: 11/11..cuedit and allcfg(cuda)Rust are compile-unverified locally (macOS, no nvcc); the nvcc lane incuda_compile_check.yamlbuilds both for sm_80/sm_90 on this PR, and the new SASS step additionally asserts the 4 tiled instantiations.bash -nclean; embedded Python parses.🤖 Generated with Claude Code
https://claude.ai/code/session_01WK8VgocBUrr5djjE1ZfCNf