fix(ArcKernels): FP8 bank conflict + the dispatch threshold's provenance, rebased onto master - #215
Merged
Merged
Conversation
…512 stays Cherry-pick of 52a8750 (`fix/fp8-cublas-crossover`), resolved against `origin/master` @ 6ffdac7. The doc lands; the constant does not. `arc_fp8_cublas_min_m` defaulted to 512 and the doc said why outright: "the default is set to that measured point rather than to an interpolated crossover". `fp8_gemv_warp` owns M <= 4, so M = 5..511 fell through to `fp8_matmul_tiled` -- the kernel whose own comment says there is "no tensor-core instruction anywhere in it". A profile of the shipped default caught it at 7,525 launches for 17.6% of GPU time in one decode window. Swept on an H200 against V4-Flash at 52a8750, one binary and one env toggle (`ARC_FP8_CUBLAS_MIN_M`), aggregate tok/s, CLEAN ROWS ONLY (>=95% achieved concurrency, <15% derived-vs-measured spread): M=1 34.92 vs 25.27 cuBLASLt 0.72x <- the GEMV floor is real M=8 49.25 vs 64.95 cuBLASLt 1.32x M=16 45.61 vs 52.55 cuBLASLt 1.15x M=128 114.21 vs 130.76 cuBLASLt 1.14x (M=32, M=64 and M=256 came back dirty at this generation length and are excluded from the decision rather than averaged in.) 52a8750 concluded from those rows that the default should be 5. THAT CONCLUSION DOES NOT SURVIVE THE REBASE, so this commit keeps 512: * The sweep was taken at 01:47 on 2026-08-21. At 07:04 the same day, #200 (9ee4591) merged a tensor-core blockwise-FP8 GEMM, and `fp8_blockwise_matmul_impl` now selects it by default for everything `fp8_gemv_warp` does not own (`use_wmma`, ops.rs). Our shipped `weight_block_size` is [128, 128] and the kernel tiles N_BLK=64, K_BLK=128, so `fp8_wmma_eligible` passes: 128 % 64 == 0, 128 % 128 == 0. At M = 5..511 the native arm is no longer the scalar kernel the sweep measured. * Lowering the default to 5 would therefore route every M >= 5 into the dequantize path and make #200's kernel unreachable at every batch size that matters -- silently, because it compiles and the tests pass. That kernel's own docs already name the merge rule: three arms, not two, and "do not merge a threshold on the two-arm result". * The sweep also predates the rank-3 flatten in the next commit, so arm (a) was dispatching a B-way batched GEMV with m=1. Those rows understate cuBLASLt and have to be retaken regardless. * Arm (a) pays an uncached full-model `dequantize_w()` on every forward that arms (b) and (c) do not. Nothing here caches it, so the crossover this sweep found is the crossover of the uncached path. What settles the constant: re-run the M sweep with the flatten fix present, three arms on one binary (`ARC_FP8_CUBLAS_MIN_M` / `ARC_NO_FP8_WMMA` toggle all three with no rebuild), and take the first M at which dequantize+cuBLASLt beats the better of the two native kernels. Until that exists, moving 512 to 5 would be a second constant with no measurement behind it -- the exact defect this doc was written to record. Also recorded as a recurring failure, because 512 is the second such gate: `qtip::gather_policy`'s tile-fill predicate needed n >= 683 and kept the grouped GEMM unreachable in every decode step (worth 1.45x at B=128). Both were derived from a single working point and neither was ever swept. `ARC_FP8_CUBLAS_MIN_M` remains the kill switch in both directions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UMmjFy8TvsgypxVWNVhhC7
…nflict⚠️ NEVER COMPILED. The box went down (port 22 closed, 100% ICMP loss) before this could be built or measured. Committed so it is not lost; it must be built and A/B'd before any number is quoted from it. `fp8_matmul_tiled`'s inner product reads `s_weight[tx][k]` with `tx` varying fastest within a warp, so consecutive lanes sit `BLOCK_K + pad` floats apart. At the shipped BLOCK_K=32 (`TILE_K = 32`, blockwise_fp8_gemm.cu:381/399) a +4 pad gives stride 36; 36 mod 32 = 4, gcd(4,32) = 4, so the warp reaches only 8 of the 32 shared-memory banks — a 4-way conflict on every FMA of the k loop. A +1 pad gives stride 33, coprime with 32, so all 32 banks are hit. Output is bit-identical by construction: the k loop is bounded `k < BLOCK_K`, so the pad columns are written by nobody and read by nobody. The padding exists only to set the row stride. This does not change the kernel's ceiling. The inner loop still issues two shared-memory loads per single FMA with no register blocking, which bounds it near an eighth of even the scalar roofline. The real fix is a tensor-core blockwise-FP8 GEMM. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Code Metrics Report━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Language Files Lines Code Comments Blanks ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ C Header 5 305 210 52 43 CSS 2 1181 1036 34 111 CUDA 81 29464 20161 6264 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 212 47877 0 37224 10653 |- BASH 72 1655 1203 331 121 |- C 3 17 17 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 67 2058 1723 77 258 |- TOML 6 207 164 0 43 |- YAML 5 41 36 5 0 (Total) 53727 4766 37766 11195 ───────────────────────────────────────────────────────────────────────────────── Rust 681 340749 293058 18073 29618 |- Markdown 504 32196 471 27754 3971 (Total) 372945 293529 45827 33589 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Total 1352 515093 362988 97876 54229 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
heydryft
added a commit
that referenced
this pull request
Aug 21, 2026
…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
heydryft
added a commit
that referenced
this pull request
Aug 21, 2026
…ispatch engagement log, three-arm sweep harness, SASS probe (#218) * fix(ArcKernels): the cuBLASLt FP8 branch never flattened rank-3 to 2-D 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> * perf(ArcKernels): pow2 shift fast path for the tiled FP8 GEMM's per-element 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 * feat(ArcLab): ARC_LOG_FP8_DISPATCH — engagement lines proving WHICH FP8 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 * feat(ArcLab): three-arm FP8 threshold sweep harness — the measurement 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 * ci(ArcGate): free SASS probe for the FP8 dense-GEMM lane — LDS width 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 * fix(ArcGate): SASS probe — replace GNU-awk-unsupported \< \> ERE escapes 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 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Replaces #201, which was red for exactly one reason: its base was
release/openrouter-readyand theBase branchlane refuses any base that isnot
master. Every other lane on #201 was green. This branch is cherry-pickedonto
origin/master(6ffdac7aeat the time of writing), sorelease/openrouter-ready's history — including145bbbc0a, thedocs/engineering/OPENROUTER_READY.mdcommit that exists only becausegh pr createrefused an empty branch — does not come along. That file isabsent from this branch;
git diff --stat origin/master...HEADis two files.What landed, and what did not
#201 carried three commits. Two land; one was already fixed on master in a
better form, and cherry-picking it now would be a regression.
Landed —⚠️ NEVER COMPILED / must-be-A/B'd label is kept verbatim. Verified
b5de0f1d4, the 4-way shared-memory bank conflict. Unchanged, andits author's
the arithmetic against the shipped constants rather than the commit message:
blockwise_fp8_gemm.cu:623-624and:641-642instantiatefp8_matmul_tiledwith
TILE = 32,TILE_K = 32, soblockDim.x = 32and a warp is 32consecutive
tx. The inner product at:110readss_weight[tx][k], strideBLOCK_K + padfloats. Atpad = 4: stride 36,bank = (4·tx + k) mod 32takes 8 distinct values, 4 lanes each — a 4-way conflict on every FMA. At
pad = 1: stride 33,bank = (tx + k) mod 32, all 32 banks, conflict-free.Output is bit-identical by construction (the
kloop is boundedk < BLOCK_K,so the pad columns are written and read by nobody), and shared memory drops
9216 B → 8448 B per block. The predicted 562 ms → ~134 ms stays UNVERIFIED:
there is no nvcc on this host and this change was made with zero GPU.
Landed with the constant reverted —
52a8750f5, the dispatch threshold.The doc lands, the
512 → 5does not. Reasons in the next section.Dropped —
445b92063, the rank-3 flatten. Already on master in the generalform:
UnquantLinear::forwardflattens atmistralrs-quant/src/unquantized/mod.rs:155-161, landed by #202(
611d236c2), whose own comment names445b92063and says it "subsumes thecaller-side workaround". Applying the caller-side version now is not merely
redundant — master's flatten is deliberately gated
&& !a.device().is_cpu(),pinned by
cpu_forward_still_takes_the_broadcast_path_bit_for_bit(
unquantized/mod.rs:857), because flattening changes CPU F16 accumulationorder and re-baselines every V4 batched-vs-solo tolerance at once (that comment
records 1.57e-2 measured against a 1e-2 tolerance on x86).
445b92063is notdevice-gated, so it would reintroduce the CPU flatten for blockwise-FP8 layers
only — where no test is watching.
The threshold: shipping 512, and what would settle it
ARC_FP8_CUBLAS_MIN_Mstays at 512. Not because 512 is right — the doc thisPR lands says plainly that 512 was a single prefill point, and that
M = 5..511fell through to a kernel whose own comment admits it has "notensor-core instruction anywhere in it". It stays at 512 because the sweep
behind 5 no longer measures the arm that runs.
52a8750f5was authored 01:47 on 2026-08-21. At 07:04 the same day, #200(
9ee459191) merged a tensor-core blockwise-FP8 GEMM.fp8_blockwise_matmul_implnow selects it by default for everything
fp8_gemv_warpdoes not own(
use_wmma,ops.rs), and our shippedweight_block_size[128, 128]clearsfp8_wmma_eligibleagainst that kernel'sN_BLK = 64,K_BLK = 128(
128 % 64 == 0,128 % 128 == 0). So the sweep's "native" arm — the scalarfp8_matmul_tiled— is now reachable only withARC_NO_FP8_WMMA=1, andlowering this constant to 5 would route every
M >= 5into the dequantize pathand leave #200's kernel unreachable at every batch size that matters. Silently:
it compiles and the tests pass. That kernel's own docs already state the merge
rule — three arms, not two, and "do not merge a threshold on the two-arm
result".
Two more reasons the rows cannot be cashed as they stand: they predate the
flatten, so arm (a) was dispatching a B-way batched GEMV with
m = 1andunderstates cuBLASLt; and arm (a) pays an uncached full-model
dequantize_w()on every forward (~12.7 GB/step) that arms (b) and (c) do not — nothing in this
PR caches it, so 5 would pay that at every size.
The measurement that settles the constant: re-run the M sweep with the
flatten present, three arms on one binary —
ARC_FP8_CUBLAS_MIN_M/ARC_NO_FP8_WMMAtoggle all three with no rebuild —and take the first M at which dequantize+cuBLASLt beats the better of the two
native kernels. Moving 512 to 5 before that exists would be a second constant
with no measurement behind it, which is the defect the doc was written to
record. The sweep table, its clean-row criteria, and the
M=1 → 0.72xGEMVfloor are all preserved in the doc so the re-run starts from data, not memory.
A conditional threshold (5 when WMMA is ineligible, 512 otherwise) was
considered and rejected: it would make
ARC_NO_FP8_WMMA=1flip two things atonce and destroy #200's control arm.
Worth a separate change: cuBLASLt block-scaled FP8 is reachable
CUBLASLT_MATMUL_MATRIX_SCALE_BLK128x128_32Fexists at our pin — cudarc0.19.4 (
Cargo.lock:1333-1336, arriving via the candle git dep atCargo.toml:52-53), at~/.cargo/registry/src/index.crates.io-*/cudarc-0.19.4/src/cublaslt/sys/mod.rs:747(cfg
cuda-12090/cuda-13000) and:759(cfgcuda-13010/cuda-13020).CUBLASLT_MATMUL_DESC_A_SCALE_MODE/_B_SCALE_MODEare at:618-619,:655-656,:693-694(first gated atcuda-12080). Candle enablescuda-version-from-build-system, so the block-128 variant needs a CUDA 12.9+toolkit at build time; 12.8 gets the scale-mode attributes but not
BLK128x128_32F. Ourweight_block_size[128, 128]is an exact match for thedescriptor's geometry.
Our own wrapper only sets
*_SCALE_POINTERtoday(
mistralrs-quant/src/cublaslt/matmul.rs:182-200), but the generic setterset_matmul_desc_attribute(cudarc-0.19.4/src/cublaslt/result.rs:111) takesany descriptor attribute, so no cudarc bump is needed. The caveat from the
earlier check holds: A must also be FP8 — there is no bf16×fp8 mixed-type
path — so this needs an activation quantiser before it is usable. If that is
built, the dequantize disappears entirely and the threshold question above
changes shape. Not built here; reporting reachability only.