Skip to content

murakumo vs. vLLM, SGLang and llama.cpp — a measured comparison, including the parts where we lose

For engineers evaluating whether murakumo is a serious alternative to vLLM / SGLang / llama.cpp — measured 2026-09-06, unflattering numbers included

The honest way to start a comparison like this is to say what is being compared, because the four things in the title are not the same layer, and a single ranking across layers is meaningless. vLLM, SGLang and llama.cpp are inference engines. Most of what we build is not.

What each of these actually is

Project What it is An inference engine?
kotoba-lang/murakumo A control plane over llama.cpp and MLX. An exo-style memory-weighted layer-shard planner that probes each node's live memory, cuts a contiguous layer partition, and emits llama.cpp --rpc/--tensor-split or mlx.launch ring commands. No — the layer above one.
kotoba-lang/num A GPU numerical compute library at the BLAS level: a portable .cljc core with injected backends (a CPU reference implementation, WGSL/WebGPU reaching Metal). No — a part.
kotoba-lang/torch A PyTorch-style neural-network module graph expressed as plain EDN. The numerics are delegated to num. No — a part.
kotoba-lang/inference The own-stack runtime built on num + torch: GGUF reading, a real 42-layer Gemma4 forward pass. Yes — the only one of the four that is.

Worth saying explicitly because the name invites the wrong guess: kotoba-lang/vllm in this workspace is a client for vLLM servers — inference requests rendered as EDN data — not an engine and not a reimplementation of one.

Scale, measured

Measured 2026-09-06 through the GitHub REST API. Stars and forks come from GET /repos/{owner}/{repo}; contributor and commit counts come from the Link: … rel="last" page number on /contributors and /commits at per_page=1, which is the count rather than an estimate of it.

Repository Stars Contributors Commits
ggml-org/llama.cpp 127,228 445 10,826
vllm-project/vllm 91,072 453 20,916
exo-explore/exo 47,276 98 2,354
sgl-project/sglang 35,524 450 17,840
kotoba-lang/murakumo 0 1 961
kotoba-lang/num 0 1 147
kotoba-lang/torch 0 1 146
kotoba-lang/inference 0 1 81

All four of ours are public repositories. Forks: zero. Watchers: zero. External contributors: zero. That is not a rhetorical flourish — it is the single most important number on this page for anyone deciding what to depend on. Four hundred and fifty people review a change to vLLM. One reviews a change to murakumo.

The decisive measurement: we ran distributed serving, and it lost

Verified 2026-07-05, recorded in kotoba-lang/murakumo's README. The same model, two ways:

Configuration Throughput
Spread across the 7-node CPU RPC ring 12.7 tok/s
Standalone on the head's GPU 61.5 tok/s

A real GPU beats network-distributed CPU by roughly five times on this hardware. We built the distributed path, we measured it against the single-node path, and the single-node path won. Publishing the ratio is the point: a distributed-inference project that only shows you the distributed number is not telling you what it costs.

The reason is physical, and it is also measured. ADR-260815 put a measurement plane in front of the parallelism chooser. Nine of eleven inventoried nodes answered; every one reports 1000baseT full-duplex, and a verified transfer between two minis moved 67,108,864 bytes in 575 ms — 933 Mbps, i.e. the 1 GbE wire saturated. At that link speed the planner correctly returns pipeline, not tensor. Tensor parallelism is not a feature we are missing here; it is a feature that would make things worse on this wire.

What would overturn that is one purchase away and has not been made. Each Mac mini exposes three Thunderbolt ports, already enrolled as members of bridge0 by macOS default, with bridge0 status: inactive on every node — 27 idle ports across the nine reachable nodes, and zero cables. Nothing on the node side has to change for that to become a fabric. Nothing on the node side has changed, either. Until the cables exist, the ports are a plan, not a capability.

The discipline that makes those numbers worth reading

The same ADR is the reason we are willing to publish the losing number. Its rule is that :link-gbps is zero unless every rank boundary carries a verified transfer, and the plan reports :evidence as one of :measured / :partial / :none / :unverified — so a zero cannot silently mean four different things. Before that, a fleet nobody measured and a fleet measured at 0.5 Gbps arrived at the chooser as the same value.

The probe's first implementation reported 9,587–14,913 Mbps across a 1 GbE fleet — fifteen times the physical ceiling. The measured cause: macOS nc abandons the connection when the send buffer fills, so 133,120 bytes of a 16,777,216-byte transfer arrived, the sender exited 0, and the 8 ms it took to fail became the denominator. Exit status, elapsed time and the absence of any error all reported success. That is the failure mode this whole page is written against: a measurement that could not run returning what a measurement that ran and found nothing wrong returns.

What the router actually has

The public boundary is api.murakumo.cloud. Its router does implement capacity admission control, landed 2026-09-01 — this corrects an earlier statement of ours that it was contract-only:

  • owned-slot-admission returns :slot-available / :all-slots-busy / :capacity-unmeasured. The unmeasured case is admitted while reporting :enforced false — an unmeasured probe is explicitly not treated as an empty pool.
  • merge-pool-capacity asks the whole owned pool rather than one member. That exists because of a measured bug on 2026-09-01: the probe asked only one head, got connection_refused, read that as unmeasured, and admitted 8 concurrent requests that queued to 27.7 s.
  • demote-busy-endpoints moves a head measured full behind the others without re-sorting, because rotating the leader splits llama.cpp's prefix cache — an 18k-token transcript re-evaluates from zero at about 83 s instead of hitting the cache at 7–13 s.
  • The policy is to return an early 429 rather than queue, because a queued request and a fast one both answer 200 and nobody can tell them apart until the latency arrives.

Continuous batching is delegated to llama.cpp's own slots (--parallel), which is where it belongs. What murakumo does not have is a vLLM-style kernel-level scheduler, and at this layer it does not need one. "Delegated" is a design decision about which layer owns a concern; it is not the same claim as "implemented", and it is not the same claim as "missing" either.

Where the own stack really stands

This is kotoba-lang/inference, the one of the four that is an engine, as recorded in its own verify/maturity.edn (updated 2026-08-28).

Genuinely working. GGUF Q4_K / Q6_K dequantisation. A real 42-layer Gemma4-E4B forward pass. JDK-FFM native K-dot and Deno WebGPU→Metal K-dot both matching the official ggml Q8_K/Q4_K/Q6_K oracle. Token-level agreement with live Ollama: the prompt The capital of France is produces token 9079, " Paris".

Not working yet, in its own words. The gap :distributed-llm-vllm-like is marked :contract-only: "Distributed transformer specs now carry tensor/pipeline parallelism, shard strategy, and paged KV cache as EDN, but no vLLM-like scheduler, admission control, continuous batching, or remote worker execution has been implemented yet."

Speed. Warm decode is 8.4 s/token on Metal, and 6.9–15.5 s/token on a real prompt. Those are seconds per token, not tokens per second. An engine at that speed is a correctness result, not a serving result, and we do not serve production traffic from it.

Live capacity today, and what that number actually measures

Self-declared at GET /v1/models, measured 2026-09-05 for murakumo-main (Qwen3.8-27B, Q4_K_M, int4, Japan):

Condition Aggregate throughput
concurrency 1 1.04 tok/s
concurrency 2 0.80 tok/s, with 3 of 4 requests timing out
concurrency 4, measured 2026-08-31 35.56 tok/s

That was published as a thirty-five-fold degradation, with server-side queueing on the b70 head as the suspected cause, and flagged for the operator rather than papered over. Two corrections followed on this page, and both were wrong. They are kept below the current text, because the shape of the mistakes is the more useful part.

What is verified as of 2026-09-07. murakumo-main is served by gad's port 8090. The unit holding that port is named murakumo-ring.service and its unit file really does define a ten-participant llama.cpp RPC ring — --rpc against nine Mac minis, --split-mode layer --tensor-split 3,3,3,3,4,3,3,3,3,12. A systemd drop-in blanks that ExecStart= and substitutes something else. The effective command, read with systemctl show -p ExecStart, is a single-node Vulkan server: Qwen3.8-27B Q4_K_M with an mmproj, -ngl 999 -c 524288 --parallel 2 --flash-attn on --spec-type ngram-cache,ngram-simple. There is no --rpc in it. The nine RPC workers are still listening on :50052 and have zero connections.

So the distributed ring is not serving, and has not been while any of these numbers were taken. gad is one machine with a very large context window; b70 is another machine with a small one. They differ by about four times per token, and the reason is not distribution — it is different hardware and a sixteen-fold difference in context budget. The seven-day median of 10.38 tok/s quoted in the previous version of this paragraph is real but is a median across a mixture of configurations: that unit restarted twenty-three times on 09-07 alone. Since the current configuration started there are four decode samples, which is not a distribution, and this page will not print one.

What survives all of it: the published sum(completion_tokens) / wall_clock is a real defect, because prefill sits inside the wall clock and a 13-second uncached prefill in front of sixteen tokens publishes as 1.2 tok/s whatever the hardware is doing. And the pool's members genuinely differ by more than four times. If you are evaluating murakumo as a serving endpoint, ask for the decode rate and the prefill rate separately, and ask which endpoint answered.

Two things did get fixed this week, both on b70. Its slots went from one to two at unchanged total context — the same KV budget split in half, and of 3,795 logged requests none exceeded the resulting 16,384-token window. Measured three ways, the result disagreed with itself: a same-shape burst got 8% worse (4,636 → 5,052 ms median), sustained aggregate decode got 8% better (32.3 → 35.5 tok/s), and a short request queued behind a long one went from 16.54 s to 5.12 s with non-overlapping ranges — while the long generation itself paid 12.95 → 20.2 s. Nothing there makes the head faster; it stops one request monopolising it. And prefix reuse turns out to have been working the whole time: n_prompt_tokens_cache reports the current task and reads 0 on an idle slot, which is what was misread as a cache miss. The per-request figure is timings.cache_n, and a nonce-prefixed control shows 10,059 of 12,111 tokens reused on the second turn — prefill 12.1 s down to 2.7 s.

Corrections, 2026-09-07. The first version of this section said the fleet was degraded. The second said it was not, and that the number was an accounting artefact — measured on b70, which is not the endpoint the number came from. The third said the endpoint was a live RPC ring, read off a unit file whose name and first ExecStart both say so, without asking systemd for the effective command. Each correction was measured. Each measured the wrong thing: the wrong machine, then the wrong layer. They are left standing here rather than replaced, because a page whose argument is that operators should publish their own unflattering measurements does not get to quietly tidy up its own.

Feature comparison

Read the murakumo column as a control plane. "Delegated" there means the concern is owned by llama.cpp underneath, which is a placement decision, not a gap. The num/torch/inference column is the own stack, which is early.

Provenance. The vLLM, SGLang and llama.cpp columns were read off those projects' official documentation and repositories on 2026-09-06 — links are in the notes below the table — and were not benchmarked by us. The murakumo and num/torch/inference columns are our own measurements and our own source. Where we could not confirm a cell from a primary source we say so rather than guess.

Capability vLLM SGLang llama.cpp murakumo num/torch/inference
Continuous batching Yes Yes Yes (--cont-batching, default on) Delegated to llama.cpp slots No
Paged / prefix KV cache Yes (PagedAttention + automatic prefix caching) Yes (RadixAttention) Yes (server slots + --cache-prompt, default on) Delegated Spec only
Tensor parallelism Yes Yes Only under --split-mode tensor, documented experimental — see note The planner emits it; at 933 Mbps it is the wrong choice today Spec only
Pipeline parallelism Yes Yes Yes — the default --split-mode layer is documented as pipeline parallelism Yes — measured at 12.7 tok/s Spec only
Speculative decoding / MTP Yes (EAGLE, MTP, draft models, n-gram) Yes (EAGLE-2/3, MTP, DFLASH, n-gram, standalone draft) Yes (--spec-type, incl. eagle3 / mtp / n-gram) Qwen3.8 MTP partial Partial
Prefill/decode disaggregation Yes, documented experimental; production use relies on third-party connectors Yes (PD disaggregation; Mooncake / NIXL / ASCEND transfer backends) Not shipped — open feature request No (disaggregat matches 0 files in the repository) No
Capacity admission at the public boundary Yes — pool-wide, early 429, landed 2026-09-01 No

Notes on the upstream cells

  • llama.cpp tensor parallelism is the cell most often stated wrongly, including by us. Its multi-GPU documentation distinguishes three split modes: the default layer is "Pipeline parallelism. Each GPU holds a contiguous slice of layers"; row is an "older row-split tensor-parallel path with comparatively poor performance" that the docs say to avoid in new deployments; and tensor is "tensor parallelism that splits both weights and KV across the participating GPUs", marked experimental. --tensor-split itself is the per-GPU proportion knob ("how much of the model goes to each GPU"), applied under whichever split mode is active — it is not by itself tensor parallelism. The RPC backend distributes weights and KV in proportion to each device's available memory and does not use either term in its own README.
  • llama.cpp prefill/decode disaggregation is an open feature request (issue #21266), not a shipped flag. We found no server documentation describing it as available, so the cell reads "not shipped" on the strength of the tracking issue rather than an explicit statement of absence.
  • vLLM: parallelism and scaling, speculative decoding, disaggregated prefilling (the page's own title carries "(experimental)").
  • SGLang: the project README for continuous batching, RadixAttention and tensor/pipeline/expert/data parallelism; PD disaggregation.
  • The two dashes in the admission-control row are not a claim that these engines lack rate limiting. They are engines rather than hosted boundaries, so the row does not apply to them in the same sense; we did not find a comparable per-pool admission decision to cite, and we are not inventing one to fill the cell.

The honest reading

If you need to serve a large model fast today, use vLLM or SGLang on real GPUs, or llama.cpp if you are on Apple Silicon or CPU. That is not modesty; it is what our own measurements say. Four hundred and fifty contributors, twenty thousand commits and a saturated NVLink domain beat one contributor, a thousand commits and 933 Mbps of Ethernet, and no amount of architecture makes that arithmetic different.

What murakumo is actually for is the layer above: taking heterogeneous machines you already own, deciding what can run where from measured memory rather than from a configuration file, emitting the engine's own commands instead of replacing the engine, and refusing to answer when it has not measured enough to answer. The parts of that which are done are done and measured. The parts that are not are listed above with the same specificity.

The reason to publish the 12.7 against the 61.5, and to go and measure the head behind the 1.04 rather than publish it as a collapse, is that a comparison page which only contains the flattering half of the operator's own measurements is not evidence about the system — it is evidence about the operator. Neither is a page that leaves an unexamined bad number standing because it sounds suitably humble.

Every murakumo number on this page is our own measurement, taken on our own hardware, on the date stated next to it. Fleet numbers are specific to this fleet — eleven Apple Silicon nodes plus an AMD Strix Halo head on 1 GbE — and do not generalise to other hardware. The vLLM / SGLang / llama.cpp column was read off those projects' own documentation and repositories on 2026-09-06 (links inline); we did not benchmark them, and nothing on this page compares their throughput to ours. GitHub counts move hourly.