Skip to content

0.70.1 P0: Qwen3 on GB10 (sm_121) is correct but ~30x slow — ship sm_121 SASS, re-test batched prefill #4590

Closed as not planned

Description

@noahgift

Known issue in 0.70: on GB10 (sm_121), Qwen3 (the non-3.5 models) runs correctly but about 27–34× slower than Qwen3.5. At 8k think-on, Qwen3-1.7B took 967s, Qwen3-8B 2163s and Qwen3.5-9B 63–80s (#3715 pilots, apr 0.70.0-dev 8d021f6).

Two causes, both measured:

  1. There is no native sm_121 device code. cuobjdump --list-elf shows no fatbin: the kernels ship as PTX (sm_70/75/90), and the driver JIT-compiles them for sm_121.
  2. At cc>=120, prefill runs one token at a time by design (select_prefill_path, sm12x default). On GB10, batched prefill gave wrong output on 2 of 4 prompts (FALSIFY-CPU-GPU-009 / PMAT-810).

Operator ruling (2026-09-28): 0.70 de-claims Qwen3 on GB10. apr warns, the release notes list it as a known issue, and only the Qwen3×GB10 cells leave #3715's required set.

0.70.1 P0:

Refs #3715

Activity

  1. added this to the 0.70.1 milestone on Sep 28, 2026
  2. noahgift commented on Sep 28, 2026

    @noahgift
    MemberAuthor

    Operator ruling 2026-09-28 (0.70.1 P0; the de-claim stands for 0.70). Root-cause hypotheses, tested in parallel, receipts only, ≤1 h each:

    • H1 PTX-JIT: CUDA_FORCE_PTX_JIT=1 on lambda → is batched prefill wrong? + build with sm_121 SASS → fixed?
    • H2 aarch64 char signedness: byte-diff host-produced buffers gx10 vs intel, same input.
    • H3 unified memory: compute-sanitizer racecheck/synccheck on batched prefill; force explicit copy + sync.
    • H4 CUDA 13: same kernel built with CUDA 12 on gx10.
    • H5 kernel-variant selection: force gx10's variant on lambda.

    Owner: aprender-6b (+ L1 aprender-85 for KTEST F-4 sm_121 SASS). Deliverable: which hypothesis the evidence confirms, then fix + a planted falsifier.

  3. noahgift commented on Sep 28, 2026

    @noahgift
    MemberAuthor

    Scope added (operator 12:50): GB10 decode is slower than the bandwidth ceiling. Receipt (aprender-85, gx10:~/kreg-4590/): Qwen3-1.7B Q4_K_M, cold 69.4s vs warm 68.5s → runtime JIT ruled out as the cause of slowness (H1 stays open for wrong answers only). Decode ~85 ms/step vs ~4 ms floor for 1.1G weights (~20x). H2–H5 continue (owner aprender-6b).

  4. noahgift commented on Sep 28, 2026

    @noahgift
    MemberAuthor

    #4590 H1–H5 receipts (aprender-6b, 2026-09-28 10:30–11:20Z)

    Verdict: none of H1–H5 is confirmed, because the defect they explain no longer reproduces. With the current binary, GB10 batched prefill is correct. It matches serial token-for-token and is bit-identical to the RTX 4090. The 30× slowdown is the serial-prefill policy itself: two stacked rules keep Qwen3 on GB10 serial.

    Subject: apr 0.70.0 (8d021f61e) on both hosts. It is the same binary as the #3715 pilots. gx10: GB10 cc 12.1, driver 590.48.01, CUDA 13.1. lambda: RTX 4090 cc 8.9, CUDA 13.0. Both dlopen libcublas 13.6.0.2. Greedy, --temperature 0, and only the generated text is compared (timing lines stripped).

    The premise (PMAT-810 / FALSIFY-CPU-GPU-009), re-measured on gx10

    model prompts tokens batched == serial (gx10) gx10 GPU == lambda GPU
    Qwen3-1.7B-Q4_K_M 4 (#4590 set) 24 4/4 4/4
    qwen2.5-coder-1.5b-q4_k_m (the PMAT-810 model) 4 (the PMAT-810 prompts: haiku autumn, primary colors, is_prime, France) 64 4/4 4/4
    Qwen3-8B-Q4_K_M 4 (PMAT-810 prompts) 64 4/4 4/4 serial; lambda batched p0 fell back to CPU (GPU OOM in prefill_extract_first_token, 0.1 GB free VRAM on the shared 4090), the other 3 match
    Qwen3-1.7B needle (passphrase at line 1) ~0.5k / ~2k / ~8k tokens 16 3/3 correct (serial at ~8k: timeout at 1200 s) 3/3

    Speed on the same needle (wall, includes load): lambda ~8k tokens serial 148.7 s vs batched 7.0 s. gx10 ~2k tokens 323.6 s vs 21.1 s; ~8k tokens >1200 s (timeout) vs 54.7 s. gx10 was shared with the :8091 shadow serve and the #3715 pilots, so these wall times are indicative, not a bench.

    Hypotheses

    • H1 PTX-JIT: not confirmed. Every apr kernel is PTX JIT-compiled on both hosts. The binary carries no fatbin, and 85 found 300/300 cached cubins are sm_121.cubin on gx10. So "native sm_121 code" already runs, and batched is correct under it. CUDA_FORCE_PTX_JIT=1 on lambda is not measurable: cublasCreate_v2 fails with CUBLAS_STATUS_ALLOC_FAILED, the batched F2 probe errors, and the run falls back. The SASS arm (KTEST F-4) still matters for JIT cold-start cost (85's cold/warm split), not for correctness.
    • H2 aarch64 signedness: refuted on this input. apr tensors --stats --json (310 tensors: host dequant mean/std/min/max/nan) is byte-identical between gx10 (aarch64) and lambda (x86_64). The token count (24) and trace output are identical, and all GPU outputs are bit-identical across hosts. The one cross-host difference is the CPU path on one prompt (haiku, --no-gpu): x86 and aarch64 CPU disagree with each other, and both GPUs agree with each other. That is a CPU SIMD near-tie, not a GB10 GPU defect.
    • H3 unified memory: refuted. compute-sanitizer 2026.2.1, BATCHED_PREFILL=1 ([PREFILL-PATH] batched cc=121), qwen2.5-coder-0.5b: synccheck 0 errors, memcheck 0 errors, racecheck 0 hazards. Racecheck was filtered to regex=(scatter|rope|attn|attention|prefill|batch|rmsnorm|kv), which matches batched_*, kv_cache_scatter, flash_attention* and batched_rope_neox_* in the gx10 cubin cache. Unfiltered racecheck is OOM-killed at 57.9G, and again under a 24G cap, on unified memory. cuBLAS GEMMs were not in racecheck scope. The output under every tool was correct ("4").
    • H4 CUDA 13: not testable as stated, and moot. gx10 has no CUDA 12 toolkit (/usr/local/cuda-13{,.0,.3} only). apr's device code is PTX JIT'd by the driver, independent of the toolkit, and lambda runs the same CUDA 13 cuBLAS 13.6 and is correct. There is no CUDA-13-only discrepancy to explain.
    • H5 kernel-variant selection: refuted. gx10's resolved set is Q4K=MWV (all GPUs since FALSIFY-Q4K-ADA-PARITY-001), FP16 prefill GEMM ([#3785] … FP16 (cc=121)), and manual decode graph ON (trueno#243 ✓ Manual graph). With BATCHED_PREFILL=1, Qwen3 on lambda resolves to that same set: FP8 is off via the QK-norm rule, the prefill is FP16, and the graph is on. Its outputs are bit-identical to gx10's.

    Why Qwen3 × GB10 is slow: two serial rules, both still in force at origin/main

    1. select_prefill_path (crates/aprender-serve/src/cuda/gpu_profile.rs:509): cc >= 120 gives Serial ("sm12x default"). This is a correctness guard for FALSIFY-CPU-GPU-009, which does not reproduce above. That defect was measured 2026-06-16 on a v0.49-era build. e237ea751c (PMAT-886a), 9750e9179b and 75b178a93f all landed since and touch this path. I have not bisected which one fixed it.
    2. GpuProfile::disable_fp8_for_qk_norm (gpu_profile.rs:1135): a QK-norm model (Qwen3) goes Batched to Serial on every GPU. Its own doc says the reason is VRAM: the FP16 HGEMM cache OOMs Qwen3-8B plus the CPU twin on 24 GB. FP16 batched is correct for Qwen3; only FP8 is broken (FP8 batched prefill fails CPU parity at the post-prompt decode step — catastrophic on per-head-QK-norm models (Qwen3-8B cos −0.10), marginal on Qwen2.5 (0.92) #3483). GB10's 128 GB unified memory does not have that constraint.

    Lifting rule 1 alone does not help Qwen3, because rule 2 flips it straight back to serial.

    Proposed fix + planted falsifier (0.70.1)

    • Rule 1: drop the sm12x Serial default. Batched becomes the default everywhere, and BATCHED_PREFILL=0 stays the opt-out.
    • Rule 2: keep "FP8 off for QK-norm". Replace "and serial" with a memory predicate: go serial only when free device memory is below the FP16 prefill-cache estimate. That keeps the 4090 OOM guard and releases GB10.
    • Falsifier FALSIFY-CPU-GPU-009b (on-device, gx10): default apr run with no env vars on Qwen3-1.7B and qwen2.5-coder-1.5b, using the 4 PMAT-810 prompts plus the ~8k needle.
      • It must print [PREFILL-PATH] batched, match BATCHED_PREFILL=0 token-for-token, and finish the ~8k needle ≥10× faster than serial.
      • Planted: re-inserting the cc >= 120 → Serial arm must turn it RED (on the path line and the speed ratio).
    • Unit side: select_prefill_path_table's (121, None) → Serial rows flip, and the planted revert fails them.

    Harness and receipts: lambda:/mnt/nvme-raid0/tmp/6b-4590/ and gx10:/mnt/nvme-raid0/tmp/6b-4590/. Files: repro.sh, cmp.sh, long.sh, h2.sh, h3d.sh, plus per-run summary.txt and *.err.

  5. added 7 commits that reference this issue on Sep 28, 2026
  6. modified the milestones: 0.70.1, 0.71.0 on Oct 4, 2026
  7. noahgift commented on Oct 14, 2026

    @noahgift
    MemberAuthor

    Folded into #5311 as a checklist row (TRIAGE-100, 2026-10-14): the open-ticket cap is 100, so this ticket is now tracked on that master and closed as not planned. Nothing is lost: reopen it when its work resumes, and an open PR for it reopens it. Pushed branch: 6b/4590-gb10-batched-prefill, 6b/4590-on-integ.

    Agent: aprender-clerk

  8. noahgift commented on Oct 14, 2026

    @noahgift
    MemberAuthor

    Reopened: this ticket carries the operator's own words, and TRIAGE-100 keeps those open unless their owner agrees to the fold. The checklist row on #5311 is withdrawn; this ticket stays the record. Re-linked under #5327.

    Agent: aprender-clerk

  9. noahgift commented on Oct 14, 2026

    @noahgift
    MemberAuthor

    #4590 re-receipt on GB10: rebased fix, hand-off (aprender-wmainred, 2026-10-14)

    Hand-off: the cop moved this row to unowned (#5327) at 14:43Z. The branch below is local and unpushed; this comment is the full state.

    Subject. I took aprender-6b's bca4987d21 (2026-09-28) and cherry-picked it onto main 8c3b19c8c1. The rebase drops main's known_issue_warning sm12x notice, because the fix makes it false. It leaves the capability-ladder de-claim of Qwen3 on gx10 untouched, because restoring that is a gate change and needs its own receipt. The result is commit 6e0a3ffa9e, tree 0f317227ac. I built it on gx10 (GB10 cc 12.1) with --features cuda and got apr 0.71.3 (497fde05d). The baseline is main apr 0.71.2 (98a370669). All runs are greedy with 16 tokens, and I compare only the generated text.

    • cargo test --release -p aprender-serve --features cuda --lib -- gpu_profile: pass.
    • Prompts: the 4 PMAT-810 prompts per model. Columns: branch default (br), branch BATCHED_PREFILL=0 (brs), main default (main).
    model path on br br == brs br == main
    Qwen3-1.7B Q4_K_M batched (default) 3/4 3/4
    Qwen3-8B Q4_K_M serial ("fp16 prefill cache exceeds free vram") 4/4 4/4
    qwen2.5-coder-1.5b Q4_K_M batched (default) 4/4 4/4
    Qwen3.5-0.8B Q4_K_M (control) n/a (qwen35 path) 4/4 4/4

    Findings

    1. Batched is not token-exact on Qwen3-1.7B, prompt p3 ("What is the capital of France?"). Batched gives The capital of France is Paris. and serial gives The capital of France is **Paris**. The divergence is at the markdown-bold token, which looks like a near-tie. It is still a FALSIFY-CPU-GPU-009 miss on one of 16 prompts. 6b measured 4/4 on 0.70.0, so a re-bisect is needed before the default flips. Per CLAUDE.md rule 6, the next step is to vary the neighbouring prompts and compare with lambda's 4090 on the same commit.
    2. The free-VRAM predicate keeps GB10 serial, which is the case this fix was meant to release. Qwen3-8B picked serial on every prompt, and so did the ~2k-token Qwen3-1.7B needle. On unified memory, the "free" figure leaves out reclaimable page cache. At read time the host showed 80 G free, 23 G buff/cache and 102 G available, so the predicate depends on how much page cache the host holds. CRUX parity spike: apr serve vs ollama on Qwen3.5 (0.8B/2B/4B/9B/27B) — measure gap, name techniques, file fixes #4220's 27B fp16 prefill prewarm hits the same mechanism on GB10. Fix direction: on an integrated or unified device, size against available host memory (or cudaMemGetInfo plus reclaimable), not raw free.
    3. No speed claim. The 2k needle took 84.0 s on br, 84.3 s on brs and 84.3 s on main, with the same answer (PELICAN-42) on all three. All three ran serial, so this measures nothing about batched.

    Next for the owner: (a) the fix for finding 2, then re-run the needle and Qwen3-8B; (b) the p3 near-tie bisect. Patch: rebased 6e0a3ffa9e on top of 8c3b19c8c1. 6b's original fix is bca4987d21, which is reachable from the repo.

  10. modified the milestones: 0.71.0, 0.73.0 on Oct 14, 2026
  11. noahgift commented on Oct 14, 2026

    @noahgift
    MemberAuthor

    Folded into #5327 as a checklist row (TRIAGE-100, 2026-10-14). Under the cop's ruling, tickets with a P0 label or the operator's own words that are sub-rows of an existing master are folded, with the operator's text quoted on the master exactly as written. Priority and owner carry over on the row.

    Owner la-0.71: if you object, say so here or in the inbox and this ticket is reopened. Work on it continues as before; a PR for it reopens it.

    Agent: aprender-clerk

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High priorityowner:la-0.71Owned by the 0.71 look-ahead slotsize:MK̂ 3-4 h

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions