Repository navigation
CRUX parity spike: apr serve vs ollama on Qwen3.5 (0.8B/2B/4B/9B/27B) — measure gap, name techniques, file fixes #4220
Copy link
Copy link
Closed as not planned
1 / 21 of 2 issues completedClosed as not planned
1 / 21 of 2 issues completed
Copy link
Labels
P1High priorityHigh priorityowner:la-0.71Owned by the 0.71 look-ahead slotOwned by the 0.71 look-ahead slotsize:MK̂ 3-4 hK̂ 3-4 h
Milestone
Description
Operator, verbatim, 2026-09-24: "lets dedicate a worker per CRUX on this 0.69.3 issue ie. ollama, llama.cpp, vllm, llamafile and figure out how to make parity work, or even beat them for these models."
One worker per CRUX engine. This ticket covers ollama.
Deliverables:
- A same-GGUF, same-host table for apr serve vs ollama. Columns: prefill tok/s at 850/4k/32k, decode tok/s, TTFT. Run correctness first (temp 0, token-identical, or name the divergence). Prove the GPU path with a trace line. Record the ollama version and SHA; it must be the latest release.
- The top 3–5 techniques ollama uses that apr lacks, each with the file or kernel it lives in and the estimated gain. Rank them by gain ÷ effort.
- One child ticket per technique, with a milestone: 0.69.3 only if it lands before the tag, otherwise backlog/0.70.
Coordinate with Time-to-first-token on Qwen3.5-4B-Q4_K_M is 14-16s vs llama.cpp's 1.8-2.9s on the same box (7.7x); apr's decode rate is unmeasurable with shipped tooling #3596 (batched prefill, aprender-f5), apr serve (0.69.1) has no prompt-prefix reuse on the qwen35 CUDA path: an identical ~850-token prompt costs 11.0 s on both runs (ollama: 8.2k tok/s uncached, 37k cached) #4214/apr serve (0.69.1) decodes Qwen3.5-4B Q4_K_M at ~87 tok/s on RTX 4090 vs ollama 0.34.2 at ~149 tok/s (0.58x) on the same box #4215/0.71: qwen35-0.8b-q4km golden thinking-ON never closes on apr (greedy near-tie path, no localized defect) — shipped as known RED in 0.69.1 #4030 (infra-c2/infra-76) and Release gate: apr serve throughput vs llama.cpp on the SAME GGUF (Qwen3.5 0.8B/2B/4B/9B/27B) — a regression ratchet that blocks the tag, plus a published parity target #4218 (speed gate, aprender-75). Don't duplicate them.
Collapsed rows (APR-EPIC-001 rule 20)
One issue per PR: tick a line when its PR merges; the PR closing the last line says Closes #4220, the others Refs #4220 row <id>.
- CRUX parity spike: apr serve vs llama.cpp on Qwen3.5 (0.8B/2B/4B/9B/27B) — measure gap, name techniques, file fixes #4221: CRUX parity spike: apr serve vs llama.cpp on Qwen3.5 (0.8B/2B/4B/9B/27B) — measure gap, name techniques, file fixes (CRUX parity spike: apr serve vs llama.cpp on Qwen3.5 (0.8B/2B/4B/9B/27B) — measure gap, name techniques, file fixes #4221, last comment CRUX parity spike: apr serve vs llama.cpp on Qwen3.5 (0.8B/2B/4B/9B/27B) — measure gap, name techniques, file fixes #4221 (comment))
- CRUX parity spike: apr serve vs vllm on Qwen3.5 (0.8B/2B/4B/9B/27B) — measure gap, name techniques, file fixes #4222: CRUX parity spike: apr serve vs vllm on Qwen3.5 (0.8B/2B/4B/9B/27B) — measure gap, name techniques, file fixes (CRUX parity spike: apr serve vs vllm on Qwen3.5 (0.8B/2B/4B/9B/27B) — measure gap, name techniques, file fixes #4222, last comment CRUX parity spike: apr serve vs vllm on Qwen3.5 (0.8B/2B/4B/9B/27B) — measure gap, name techniques, file fixes #4222 (comment))
- CRUX parity spike: apr serve vs llamafile on Qwen3.5 (0.8B/2B/4B/9B/27B) — measure gap, name techniques, file fixes #4223: CRUX parity spike: apr serve vs llamafile on Qwen3.5 (0.8B/2B/4B/9B/27B) — measure gap, name techniques, file fixes (CRUX parity spike: apr serve vs llamafile on Qwen3.5 (0.8B/2B/4B/9B/27B) — measure gap, name techniques, file fixes #4223, last comment CRUX parity spike: apr serve vs llamafile on Qwen3.5 (0.8B/2B/4B/9B/27B) — measure gap, name techniques, file fixes #4223 (comment))
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
P1High priorityHigh priorityowner:la-0.71Owned by the 0.71 look-ahead slotOwned by the 0.71 look-ahead slotsize:MK̂ 3-4 hK̂ 3-4 h
Activity
noahgift commentedon Sep 24, 2026
#4220 (ollama CRUX) part 1: source-level techniques, no GPU numbers yet
The GPU stays serialised until 0.69.3 G1/G2 and RC acceptance finish, so this part reads source only.
b2da9e468af2). lambda runs 0.34.2, which is not the latest; the upgrade goes through infra-8d/forjar-d3 before part 2.161755f29e41), perLLAMA_CPP_VERSION. ollama startsllama-serveras a subprocess (llm/llama_server.go:351-445) and patches only the loader and clip (llama/compat/001-llama-cpp-hooks.patch). So the kernel-side techniques below are llama.cpp's; the llama.cpp CRUX worker will find the same kernels. What is specific to ollama is its launch defaults.origin/chore/0.69.1-merge-back@eed4a959a.0. Baseline caveat: the 149 tok/s decode ran a different GGUF, with MTP speculative decoding on
The LQW-001 baseline ran ollama on the registry
qwen3.5:4b(blobsha256:81fb60c7…). I parsed that GGUF's header: it carries 15mtp.*tensors. The unsloth GGUF that apr ran (00fe7986…) carries 0.ollama switches MTP drafting on automatically for such a blob.
hasMTPDraft/hasLegacyQwenMTPDraftreturn true forqwen35whenmtp.*tensors exist (llm/llama_server.go:836-851). ThenEnableMTP=true(:900-907), and it adds--spec-type draft-mtp --spec-draft-n-max 4 --spec-draft-backend-sampling(:806-821). The defaultDraftNumPredict: 4is atapi/types.go:1147.So 149 vs 87 compares apr without speculation against ollama with MTP speculation on a different file. The same-GGUF row in part 2 will be ollama without MTP; the headline ratio for #4218 has to name which one it uses. ollama's defaults also include
-b/-ub 512(api/types.go:1144→llm/llama_server.go:593-595),--flash-attn auto(:600-624) andcache_prompt: true(:2262).Techniques ollama/llama.cpp use that apr lacks, ranked by gain ÷ effort
Not listed on purpose: fused conv1d+SiLU and in-place GDN state. apr already has both (
forward_qwen35_cuda.rs:1336,:1392).ggml-cuda/gated_delta_net.cu:63(token loop inside the kernel),:169-215(launch: grid H×seqs); ubatch 512 from ollamaapi/types.go:1144. llama.cpp has no chunked GDN either (:180TODO)forward_singleis the only entry point (forward_qwen35_cuda.rs:1480), so prefill is N decode steps. That fits the 77 tok/s prefill ≈ 87 decodeggml-cuda.cu:4420-4440(graph per key),:2595(update check),:4185-4400(capture and replay)aprender-gpu/src/driver/graph.rsexists. About 546 launches per token: 24 GDN layers × 16 (8 GEMV, gates, conv, delta, gated-norm, swiglu, 2 add, 2 norm) plus 8 attention layers × 20, plus norm and lm_head.GpuBuffer::from_hostallocates a new device buffer every token (:1506), which blocks capture and adds a cudaMalloc per tokenn_ctx_checkpoints = 32per slot (common/common.h:629),create_checkpoint(tools/server/server-context.cpp:2309-2365, snapshot viallama_state_seq_get_data_ext(..., PARTIAL_ONLY)at:2363); ollama always sendscache_prompt: true(llm/llama_server.go:2262)src/models/qwen35.cpp:36-101; the GDN kernel writes K rollback snapshots (gated_delta_net.cu:147-154), fused into the cache scatter (ggml-cuda.cu:2752-2800,:3454-3466); spec replay from checkpointsserver-context.cpp:56,257,1246mtp.*(the ollama blob does)ggml-cuda.cu:1868ggml_cuda_should_use_mmvq;mmvq.cu,quantize.cuforward_qwen35_cuda.rs:562-568) because its own dp4a path was "catastrophic through the recurrence". llama.cpp runs MMVQ through the same recurrence, so apr's divergence is an apr quantization defect, not something inherent to the modelggml-cuda.cu:2700,:3229-3249per_head_rmsnorm_into+gdn_partial_neox_rope_into(2+2 launches per attention layer)Not a gap vs ollama: apr downloads the full 248k-float logits vector every token (
forward_qwen35_cuda.rs:1549, about 1 MB). llama.cpp does the same by default (common/common.h:296backend_sampling=false; ollama turns backend sampling on only for the draft). A device-side argmax would be a "beat" item, not a parity item.Part 2 (after the GPU frees up)
Same-GGUF table in #4218's row format: engine, version, GGUF sha, GPU, n≥3 with spread. Rows: prefill at 850/4k/32k, decode, TTFT, on ollama v0.34.4 against the unsloth
00fe7986…via a ModelfileFROM, plus a second ollama row on the MTP blob with and withoutdraft_num_predict=0, to size technique 4. Correctness first: temp 0 token-identity, and a trace line proving the GPU path. Sources used here: ollama v0.34.4 and llama.cpp b11081 shallow clones under/mnt/nvme-raid0/tmp/crux4220/.noahgift commentedon Sep 25, 2026
#4220 (ollama CRUX) part 2: same-GGUF, same-host table
Setup. lambda, RTX 4090 (driver 580.119.02), under
flock /tmp/apr-gpu.lock. One foreign compute app (936 MiB, containerised) was resident the whole time. Measured 2026-09-25 15:59–16:20 UTC (17:59–18:20 Madrid).Qwen3.5-4B-Q4_K_M.gguf, sha25600fe7986ff5f6b46…, 0mtp.*tensors.0.69.5-rc.1 (5660b0877), the fleet-installed rc binary (sha2564bfdacd2…), runningserve run --gpu-layers all --context-length 40960.FROM, with/api/chat,think:falseandnum_ctx 40960.-ngl 99 -fa 1, 3 reps.GPU path proven:
Backend: GPU (CUDA, NVIDIA GeForce RTX 4090…) [qwen35 hybrid forward]andgpu-layers: requested=all resolved=32 total=32, 0fell_back, plus abatched prefill … attention cuBLAS f32trace line for every request.ollama psread 100% GPU.Prompt tokens are identical across engines: 30 / 863 / 4082 / 32734. That means the same chat template and the same tokenizer output.
load_duration)Caveats:
load_duration2.2–2.5 s even with the Modelfile and requestnum_ctxequal). Its TTFT column therefore uses the server's ownprompt_eval_duration, which excludes load. That is fair to ollama.The MTP blob does not explain the old 149 baseline. On ollama's registry
qwen3.5:4b, which auto-enables MTP drafting, decode was 152–161 tok/s. That is slower than ollama on the unsloth file without MTP (172–201). On this prompt MTP is not a win, so #4245 (MTP) drops in the gain ÷ effort ranking.Correctness at temp 0:
…standing where the stone met the sky, apr…standing on the very same granite planks.Ranking, updated by measurement (gain ÷ effort)
cuBLAS f32with a materialised score matrix. New child 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 (flash-attention prefill).Receipts:
/mnt/nvme-raid0/tmp/crux4220/p2/on lambda.bench.sh,serve_probe.pyandollama_probe.pyare the harness.out-4b-r2/holds the table run.out-4b-ident/holds the identity and reload-attribution run.noahgift commentedon Sep 25, 2026
#4220 part 3: the same-GGUF table at every size (0.8B, 2B, 4B, 9B)
Setup as in part 2: apr 0.69.5-rc.1 (
5660b0877, sha2564bfdacd2…), ollama 0.34.4, and llama-bench b11050 (-ngl 99 -fa 1). Each size runs from the same GGUF file (lqw/models/Qwen3.5-*-Q4_K_M.gguf) on one RTX 4090, underflock /tmp/apr-gpu.lock. Prompts are 863, 4,082 and 32,734 tokens, the same counts on both engines. apr prefill rates come from the[qwen35] batched prefilltrace line. ollama's come fromprompt_eval_count / prompt_eval_duration; wall clock minusload_durationagrees within 5%. Every apr run loggedgpu-layers … resolved=N total=N (backend=cuda)with no fallback.* The 9B decode ran after the fallback had stuck (see below), so it is not a clean decode number.
Readings
prompt_evaltime. That excludes ollama's model reload of 2.2–2.5 s per request in this harness.attention cuBLAS f32on every run here. 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 should first measure what flash does at 32k before writing a new kernel.--context-length 40960, the perf(prefill): MMQ int8 prefill matmul + LM head on the last token only (0.69.5) #4313 fp16 prefill weights (13.2 GB) leave about 400 MiB free. The 512-row prefill workspace misses and the session prefills one token at a time. It stays that way: afterwards, 23-token prompts also go one token at a time. The workspace is sized by the end position, not by the tokens being prefilled, and nothing tries a smaller chunk.Evidence:
/mnt/nvme-raid0/tmp/crux4220/p2/out-{0p8b,2b,4b-r2,4b-r3,9b}/: provenance.txt (sha256 of binary and GGUF, versions, foreign GPU processes), apr.jsonl, ollama.jsonl, llama-bench.json and serve.log.table.pybuilds this table from them.noahgift commentedon Sep 25, 2026
#4220 part 3 follow-up: 9B decode rerun. Part 3 flagged the 9B decode figure (0.41×) as contaminated. This rerun does not give a clean replacement. It shows the 9B decode figure depends on host load.
The rerun used a fresh
apr serve(0.69.5-rc.15660b0877, same GGUF,--context-length 8192, RTX 4090) and ran the decode probes before any long prompt, so the #4450 sticky state could not form. Decode = (completion−1)/(e2e−TTFT), 128 tokens.llama-bench tg128 on the same file: 140.6 tok/s. A foreign 936 MiB GPU process (pid 3650689, outside our namespace) was resident for the whole rerun.
Reading: apr's 9B decode swings 0.28–0.41× with host load, while the GPU-bound comparators do not. That is consistent with an eager, launch-bound decode step (#4233's opt-in CUDA-graph decode is on
batch/0.70.0, not in rc.1). A quiet-host run withQWEN35_CUDA_GRAPH=1on a 0.70.0 binary is what gives a citable 9B decode number. Until then treat 0.28–0.41 as a range, not a point.The TTFT side is unchanged from part 3: 850 → 685 ms and 4k → 992 ms (prefill 5138 tok/s from the serve log). Raw data:
/mnt/nvme-raid0/tmp/crux4220/p2/out-9b-dec/.noahgift commentedon Sep 25, 2026
#4220 p5 — 9B decode, eager vs
QWEN35_CUDA_GRAPH=1on the 0.70.0 car binary (apr-m0694)Binary
apr 0.70.0 (99c6c61b4), RTX 4090,Qwen3.5-9B-Q4_K_M.gguf, ctx 8192. Order was A/B/A (eager, graph, eager), with a fresh server per arm, under the GPU lock. Each arm got an 8-token warmup, then 3× 128-token decode on/v1/chat/completions. Measured 2026-09-25 21:45–21:52Z.[aprender#4233] qwen35 decode graph: captured 626 kernels at position 30and thenreplay launched (626 kernels, position 31).apr profile/ nsys on a quiet host, not the graph flag.Artifacts:
/mnt/nvme-raid0/tmp/crux4220/p2/out-9b-graph/(graph9b.sh,apr.jsonl,serve-*.log,provenance.txt).16 remaining items
noahgift commentedon Oct 14, 2026
CRUX parity sweep, apr serve vs ollama, Qwen3.5 0.8B–27B, GB10 (gx10, sm_121, 122 GB unified), 2026-10-14
Same harness and method as the 2026-10-12 RTX 4090 sweep, on the second GPU class.
Setup
98a370669= main @ 98a3706), built--features cudaon the host, binary sha2569310d5df….gh api repos/ollama/ollama/releases/latest→ v0.40.4).84b5f7f1…. Prompt tokens 869 / 4087 / 32740 on both.scripts/crux_ollama_parity.sh@ 9991ba6, unmodified. Rates are measured as in the 10-12 comment (ollama server-side, apr serve-log prefill lines body+suffix, decode = median of 3 × 128 client-stamped).Prefill / decode tok/s, ratio = apr ÷ ollama
(apr TTFT in the last column; ollama TTFT includes a model load because of keep_alive 0, so its server prefill rate is the fair figure.)
GPU path, from the serve trace on every size:
Backend: GPU (CUDA, NVIDIA GB10, 122502 MB VRAM);gpu-layers requested=all resolved=N total=N(24/24 … 64/64); every prefill isbatched prefill … attention cuBLAS f32; 0fell_back/per-token lines.Correctness, temp 0, 128-token decode prompt: no size is byte-identical. First differing byte: 0.8B 10, 2B 49, 4B 226, 9B 13, 27B 136. Every output is a coherent continuation. One prompt per size, so these are candidate near-ties, not a named defect.
What GB10 adds to the 4090 picture
fp16 prefill weights NOT prewarmed: 46445 MiB needed, 32361 MiB free; prefill uses f32. The result is 0.10–0.17× on prefill and a 397 s TTFT at 32k. The dense fp16 copy is 2.8× the 16.7 GB Q4_K_M file. Bigger memory does not fix cause Feature Request: Decision Tree & Random Forest for Classification Tasks #1; a quantized-weight prefill GEMM (or a streamed per-layer dequant) does. Side note: on unified memory the "free" figure leaves out reclaimable page cache, so the check also under-reports the room it has.attention cuBLAS f32signature as the 4090 (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: flash path exists, not chosen).The CPU (intel, 4 pinned threads, 0.8B–9B) table follows when its run finishes. Technique ranking and child tickets follow after that.
Agent: aprender-wmainred
noahgift commentedon Oct 14, 2026
Folded into the checklist of its epic #3598 (full original text kept there, verbatim). Owner owner:la-0.71: the work is unchanged and continues from the epic checklist; reopen if you want it tracked separately. Refs #3598.