Skip to content

perf(cuda): generalize Gemma 4 batched prefill + flash attn + decode graphs to dense Q4_K models #156

Description

@pekkah

Goal

Bring the Gemma 4 CUDA performance work to other dense models that fully offload via CudaForwardPass — primarily Qwen3-8B Q4_K_M. The batched-prefill orchestration and CUDA-graph machinery are already largely generic; they are just structurally gated behind _isGemma4Like. This issue generalizes them.

Out of scope: MoE/GDN models (separate CudaHybridForwardPass / CudaHybridGdnForwardPass), and Vulkan.

Background

What the Gemma 4 work (#136/#141/#142/#146/#147/#149/#154) splits into:

Two config knobs that looked like blockers are already first-class: attnScale (kernels do <=0 => 1/sqrt(headDim), Gemma hardcodes 1f) and hp.FfnActivation (Silu vs GeluApprox, batched body hardcodes GeluTanhMul).

Work Item A — Generalize batched prefill + flash attention (biggest win, low risk)

Three hard Gemma assumptions in GpuLayerBatchedTrunkGemma4 (CudaForwardPass.cs:2011):

  • A1. int layerHd = _hp.LayerHeadDim![layer] (:2013) NPEs on non-Gemma → _hp.LayerHeadDim?[layer] ?? _headDim
  • A2. _gpu.GeluTanhMul(...) (:2103) hardcoded → dispatch on _hp.FfnActivation (SiLuMul for Silu)
  • A3. attnScale: 1f on all 4 attention calls (:2077-2090) → cached field (1f if _isGemma4Like else -1f)
  • A4. Generalize the gate IsGemma4BatchedPrefillSupported (:1779) — keep real guards (!_isMoE, !_tqEnabled, !_hasAttnBias, IsNeoxRope, batchable dtypes), drop the implicit Gemma assumption
  • A5. Open the Prefill gate (:1732) from _isGemma4Like to "dense + batchable"
  • A6. Bit-parity oracle test: batched prefill vs per-token Forward on Qwen3-8B, argmax-stable (flash/GEMM not byte-exact, matches Gemma 4 GPU prefill is ~70x slower than llama.cpp — adopt cuBLAS GEMM for prefill #141)
  • A7. Measure prefill t/s before/after on Qwen3-8B Q4_K_M; update README row

What Q4_K gains: flash-attention prefill (biggest lever, quant-agnostic, head_dim=128 ✓) + launch collapse. NOT the compute-bound trunk GEMM (Q4_K stays on memory-bound GEMM-N; that's Item C).

Work Item B — Decode CUDA graphs for non-Gemma (launch-overhead win)

ForwardGemma4 is split prefix(embed/PLE) → capturable device region → download. The non-Gemma Forward (:890) inlines everything with no capture.

  • B1. Refactor Forward into token-varying prefix (embed) + RunDeviceRegion(position) (layer loop + final norm/output) + download, keeping TQ ring-advance (:1056) and SnapKV capture (:955) OUTSIDE the region
  • B2. Wrap with existing generic capture helpers (TryBeginGraphCapture / TryEndGraphCaptureAndInstantiate / LaunchGraphForPosition); reuse the _tqEnabled || _kvEvictedCount > 0 bail from :1315
  • B3. Verify bit-identical (graph replay == direct launch); measure decode t/s on Qwen3-8B; update README

Work Item C — Compute-bound Q4_K prefill (optional, larger; gate on A's profiler)

  • C1. (cheaper) Extend MatMulBatchedGemm to dequant Q4_K->fp16->cuBLAS, OR
  • C2. (maximal) Q4_K int8 MMQ kernel (llama.cpp mul_mat_q Q4_K reference)

Only pursue if A's prefill profiling shows trunk matmuls (not attention) dominating after flash lands.

Verification

  • dotnet build clean (TreatWarningsAsErrors), dotnet test --filter ...ForwardPass
  • Argmax-stable parity oracle for A; bit-identical check for B
  • Before/after t/s on Qwen3-8B Q4_K_M (model at C:\p\sharpi\models\ or E:\models\)

Sequencing: A first (biggest win + clean oracle), B second, C gated on A's profiler.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions