Repository navigation
apr run --gpu on qwen3moe Q4_K_M silently falls back to CPU (rc 14) instead of refusing by name — 0.69.1 must DECLARE it does not support qwen3moe on CUDA #3817
Description
Under epic #3710 and the operator's all-Q4_K-on-CUDA rule. This row exists because #3714 (the qwen3moe CUDA path) is moving to 0.70.0, and a release that does not support a model must SAY SO rather than quietly run it slowly.
The defect
apr run --gpu on a qwen3moe Q4_K_M file (Qwen3-30B-A3B-Instruct-2507, Qwen3-Coder-30B-A3B-Instruct) does not refuse. It returns rc 14 — "gpu was forced and selected, but its runtime attempt on this model failed and the generation ran on CPU" — and produces a CPU generation. Measured on lambda (RTX 4090), apr 0.69.0 (225b2a9ab) --features cuda, recorded on #3714: -v shows Architecture: Transformer [GGUF: qwen3moe] (48 layers) and no CUDA backend line before the refusal. apr qa gives rc 5 with no gates in the JSON — capability_match and golden_output both absent.
A user who passes --gpu and gets a silent CPU run at a fraction of the throughput they asked for has been told the wrong thing about their own hardware. This is the failure mode #3712 was filed to kill: the train can publish with "most models working" because absence reads as conformance.
It also fails the gate on its own terms. contracts/release-readiness-v1.yaml (#3715, landing with batch-1 / PR #3772) is explicit — a fallback receipt fails the gate naming the cell, exactly like a skip or an error:
a skipped, errored, fallback or stale receipt fails the gate naming the cell
Why a refusal and not a waiver or a RefusalCell
Both of the obvious escapes are closed, and closing them was deliberate:
- Waivers are gone.
release-readiness-v1.yaml:NO WAIVERS. EPIC: release train 0.69.1 — every capability ORACLE-PROVEN (CRUX cells) on gx10 and lambda; no defer #3710 originally permitted "a FAIL for the release unless the operator waives that cell by name"; that was removed. The standing operator ruling on contract exemptions (2026-08-21) points the same way. - A
release:RefusalCellis not available for "unimplemented." The shape admits a refusal only where the model does not FIT, and requires bothrelease:verdict in [refused]andrelease:arithmeticNamed in [true]. Qwen3-30B-A3B at Q4_K_M is roughly 18 GB against the 4090's 24 GB, and gx10's unified memory fits it outright, so citing memory arithmetic to mean "we did not build the path" would be a false arithmetic claim — worse than an honest failure. (The LONG rungs at the declared 262144 context legitimately do not fit lambda, and those remain genuine RefusalCells. That is a subset, not this row.)
So the honest shape is: 0.69.1 does not claim qwen3moe, the inventory says so, and the binary says so at load, by name.
## done_when
-
apr run --gpuon both qwen3moe Q4_K_M files refuses pre-load, by name, naming the architecture (qwen3moe), citing qwen3moe (Qwen3-30B-A3B, Qwen3-Coder-30B-A3B, Q4_K_M) has NO working CUDA path: apr run --gpu falls back to CPU (rc 14), apr qa emits no gates — blocks 0.69.1 under the all-Q4_K-CUDA rule #3714, and stating that CUDA support is 0.70.0. It never runs on CPU under--gpu. Exit code is a refusal code, not 14-after-fallback. -
apr runwithout--gpuis unchanged and still works on CPU (the CPU MoE path shipped in apr chat (CPU) is architecture-blind: never enters the MoE path for any qwen3_moe GGUF, and returns rc=0 on the failure #3367, closed 0.69.0). A refusal that also breaks the working path is a regression, not a fix. -
apr qaon a qwen3moe file emits its gates as refusals with reasons, not as an empty JSON withcapability_matchandgolden_outputabsent. Absence is the bug; a named refusal is the fix. - Every OTHER architecture's
--gpubehaviour is byte-identical. Prove it with a case table, not by inspection: at minimum one dense Q4_K (qwen2.5-coder), one Qwen3.5 rung, and one non-qwen model. - A planted mutant that removes the refusal turns a test RED, and the test names what it caught. Restoring the CPU fallback under
--gpumust fail. - The qwen3moe files are removed from lambda's and gx10's 0.69.1 host inventory, and the release notes plus README state by name that 0.69.1 does not support qwen3moe on CUDA, with a pointer to qwen3moe (Qwen3-30B-A3B, Qwen3-Coder-30B-A3B, Q4_K_M) has NO working CUDA path: apr run --gpu falls back to CPU (rc 14), apr qa emits no gates — blocks 0.69.1 under the all-Q4_K-CUDA rule #3714. Landing the refusal without the inventory and the note leaves the gate still owing the cell.
- Measured and recorded, because it decides how much of the above is even needed: whether 30B-A3B at Q4_K_M fits the 4090 at the short rungs (4k/8k/20k), read from the host receipt's measured GPU memory — weights + KV-per-token × rung + workspace. If it does not fit, those cells become legitimate RefusalCells and the inventory change shrinks accordingly.
Notes for whoever takes it
- Work already pushed and ownerless, for reference but NOT for folding into 0.69.1:
PMAT-3714-r1-on-3750@01989323dandPMAT-3714-r2-on-v2@6f6fa9a83(aprender-eb, receipted, 0/3 seats). Alsosalvage/2026-09-22/PMAT-3714-r2-parity@0e211ebef— that ref is the only copy of that work; do not delete it. - Batching is the default: stop at the receipt (
docs/audits/impl-PMAT-<id>-receipt.md), push the branch, do not open a PR and do not arm. The cop folds. - The roadmap guard needs a fragment in the same diff:
pmat work add "<title>" --github-issue <N>, thenpython3 scripts/lib/roadmap_fragments.py adopt <ID>, thenmake roadmap-aggregateand stage BOTH the fragment anddocs/roadmaps/roadmap.yaml. The aggregate is judged at the COMMITTED head, so commit before re-running the guard.
Filed by aprender-cop, 2026-09-22, on the operator's decision to move #3714 to 0.70.0.
Activity
fix(compute): drop the unused GemmOp import from matmul_cublas (--fea…
fold #3817: apr run --gpu on qwen3moe refuses by name instead of fall…
fix(gate) #3828: the ladder measured ONE verb while the contract decl…
fix(gate) #3828: the anti-shrink floor had no way to admit a REASONED…
fold #3801b: apr code's embedded fallback takes the production detect…
fold CRUX slices 1+3+4 (#3739, #3795, #3797): the harness, NOT slice …
contracts: the universe was a GLOB, so 10 of 28 held models were invi…
docs(#3077, alfredodeza): the GPU-vs-CPU support table, DERIVED not w…
noahgift commentedon Sep 23, 2026
closed by triage (aprender-dd, #3988 FIXED row, cop OK): fixed by
a5024aa72, all in v0.69.1. Evidence:run --gpuon qwen3moe refuses by name. Also owned by #3710's MoE rowPMAT-3817: apr run --gpu on qwen3moe refuses pre-load by name instead…