Skip to content

fix(ArcKernels): FP8 bank conflict + the dispatch threshold's provenance, rebased onto master - #215

Merged
heydryft merged 2 commits into
masterfrom
fix/fp8-dispatch-and-bank-conflict
Aug 21, 2026
Merged

heydryft merged 2 commits into
masterfrom
fix/fp8-dispatch-and-bank-conflict

Conversation

@heydryft

Copy link
Copy Markdown
Contributor

Replaces #201, which was red for exactly one reason: its base was
release/openrouter-ready and the Base branch lane refuses any base that is
not master. Every other lane on #201 was green. This branch is cherry-picked
onto origin/master (6ffdac7ae at the time of writing), so
release/openrouter-ready's history — including 145bbbc0a, the
docs/engineering/OPENROUTER_READY.md commit that exists only because
gh pr create refused an empty branch — does not come along. That file is
absent from this branch; git diff --stat origin/master...HEAD is 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 — b5de0f1d4, the 4-way shared-memory bank conflict. Unchanged, and
its author's ⚠️ NEVER COMPILED / must-be-A/B'd label is kept verbatim. Verified
the arithmetic against the shipped constants rather than the commit message:
blockwise_fp8_gemm.cu:623-624 and :641-642 instantiate fp8_matmul_tiled
with TILE = 32, TILE_K = 32, so blockDim.x = 32 and a warp is 32
consecutive tx. The inner product at :110 reads s_weight[tx][k], stride
BLOCK_K + pad floats. At pad = 4: stride 36, bank = (4·tx + k) mod 32
takes 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 k loop is bounded k < 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 → 5 does not. Reasons in the next section.

Dropped — 445b92063, the rank-3 flatten. Already on master in the general
form: UnquantLinear::forward flattens at
mistralrs-quant/src/unquantized/mod.rs:155-161, landed by #202
(611d236c2), whose own comment names 445b92063 and says it "subsumes the
caller-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 accumulation
order 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). 445b92063 is not
device-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_M stays at 512. Not because 512 is right — the doc this
PR lands says plainly that 512 was a single prefill point, and that
M = 5..511 fell through to a kernel whose own comment admits it has "no
tensor-core instruction anywhere in it". It stays at 512 because the sweep
behind 5 no longer measures the arm that runs.

52a8750f5 was 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_impl
now selects it by default for everything fp8_gemv_warp does not own
(use_wmma, ops.rs), and our shipped weight_block_size [128, 128] clears
fp8_wmma_eligible against that kernel's N_BLK = 64, K_BLK = 128
(128 % 64 == 0, 128 % 128 == 0). So the sweep's "native" arm — the scalar
fp8_matmul_tiled — is now reachable only with ARC_NO_FP8_WMMA=1, and
lowering this constant to 5 would route every M >= 5 into the dequantize path
and 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 = 1 and
understates 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_WMMA toggle 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.72x GEMV
floor 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=1 flip two things at
once and destroy #200's control arm.

Worth a separate change: cuBLASLt block-scaled FP8 is reachable

CUBLASLT_MATMUL_MATRIX_SCALE_BLK128x128_32F exists at our pin — cudarc
0.19.4 (Cargo.lock:1333-1336, arriving via the candle git dep at
Cargo.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 (cfg cuda-13010/cuda-13020).
CUBLASLT_MATMUL_DESC_A_SCALE_MODE / _B_SCALE_MODE are at :618-619,
:655-656, :693-694 (first gated at cuda-12080). Candle enables
cuda-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. Our weight_block_size [128, 128] is an exact match for the
descriptor's geometry.

Our own wrapper only sets *_SCALE_POINTER today
(mistralrs-quant/src/cublaslt/matmul.rs:182-200), but the generic setter
set_matmul_desc_attribute (cudarc-0.19.4/src/cublaslt/result.rs:111) takes
any 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.

heydryft and others added 2 commits August 21, 2026 15:36
…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>
@github-actions

Copy link
Copy Markdown
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
heydryft merged commit 250dd6d into master Aug 21, 2026
18 checks passed
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>
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.

1 participant