Skip to content

qwen35 batched prefill: flash attention for the full-attention layers — 32k prefill 3.2k tok/s vs llama.cpp 10.1k (0.26× ollama) #4442

Open

Description

@noahgift

Child of #4220 (CRUX ollama, part 2). Found by measurement on lambda (RTX 4090), same GGUF (Qwen3.5-4B-Q4_K_M.gguf, sha256 00fe7986…), apr 0.69.5-rc.1 (5660b0877).

Observed. apr's batched prefill rate falls off with prompt length. llama.cpp's does not.

prompt tokens apr batched prefill (serve trace line) ollama v0.34.4, server prompt_eval llama-bench b11050 -fa 1 pp
856–863 5389 tok/s (159 ms) 5765 10267 ± 1319 (pp850)
4075–4082 7798 tok/s (523 ms) 10166 11490 ± 326 (pp4096)
32727–32734 3178 tok/s (10300 ms) 12365 10099 ± 55 (pp32768)

At 32k apr is 0.26× ollama. The trace line reads attention cuBLAS f32: the 8 full-attention layers materialise the score matrix per chunk, so attention is quadratic in memory traffic. llama.cpp runs these layers with flash attention (-fa 1; ollama's default is --flash-attn auto).

This is not #3787: 10300 ms of the 10428 ms TTFT is inside the prefill trace line.

Ask. A fused flash-attention prefill kernel (online softmax, no score matrix) for the qwen35 full-attention layers in the batched prefill path. Done when the serve trace line at 32k is at least 0.8× llama-bench pp32768 on the same box and GGUF, and the temp-0 output matches the current path's.

Receipts: /mnt/nvme-raid0/tmp/crux4220/p2/out-4b-r2/ (serve.log, llama-bench.json, ollama.jsonl).

Activity

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

    @noahgift
    MemberAuthor

    A/B: APR_QWEN35_PREFILL_ATTENTION=f32 vs flash: flash is slower at every length, and 1.5× slower at 32k.

    Binary apr 0.69.5-rc.1 (5660b0877) sha256 4bfdacd2…eb49014, RTX 4090 (driver 580.119.02), Qwen3.5-4B Q4_K_M, serve run --gpu-layers all --context-length 40960 --trace. Arms interleaved f32→flash, 2 replicates each.

    prompt f32 TTFT r1 / r2 (ms) flash TTFT r1 / r2 (ms) flash ÷ f32 (mean)
    863 tok 133 / 137 151 / 149 1.11× slower
    4,082 tok 496 / 501 619 / 618 1.24× slower
    32,734 tok 8,791 / 8,175 13,496 / 12,566 1.54× slower
    • Mechanism engaged: each flash log has 8 attention flash (f16 inputs, f32 accumulation) prefill lines and no cuBLAS lines. Each f32 log has 8 attention cuBLAS f32 lines and 0 flash lines.
    • Correctness: the generated text is byte-identical in all 4 runs (sha256 a78db9251af1… over the 3 non-warmup completions).
    • Caveat: a foreign compute process (pid 3650689, 936 MiB) held the GPU in every arm. It is the same in both arms, so it does not bias the ratio. f32-r1's warmup TTFT (3.9 s) is a cold start, and no prompt row is taken from warmup.

    Conclusion: the flash prefill path as shipped does not close the 32k gap (f32 32k ≈ 3.9k tok/s → flash ≈ 2.5k tok/s, vs llama.cpp 10.1k). f32 should stay the default. The flash kernel needs profiling (apr profile / nsys on the 32k chunk) before it is a candidate.

  3. modified the milestones: 0.70.1, 0.71.0 on Oct 4, 2026
  4. noahgift commented on Oct 14, 2026

    @noahgift
    MemberAuthor

    merged into #5305 (master ticket for epic #3598; triage rule 7b, 2026-10-14). It is a checklist row there; reopen this issue when work on it starts or its PR opens.

  5. modified the milestones: 0.71.0, 0.73.0, 0.74.0 on Oct 27, 2026
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