Skip to content

Instantly share code, notes, and snippets.

@zachrattner
Created September 2, 2026 12:03
Show Gist options
  • Select an option

  • Save zachrattner/1505d0387046a18f0ba9297e9ddf702e to your computer and use it in GitHub Desktop.

Select an option

Save zachrattner/1505d0387046a18f0ba9297e9ddf702e to your computer and use it in GitHub Desktop.
Rapid-MLX vs vanilla MLX vs Ollama on an M1 Ultra: 54 controlled runs, and the prompt-cache artifact reproducing on a second engine

Rapid-MLX vs MLX vs Ollama on Apple silicon

A three-way follow-up to the MLX vs Ollama harness, run because someone asked a good question in a LinkedIn comment: was I using Rapid-MLX, rather than vanilla MLX?

Rapid-MLX is an OpenAI-compatible server that wraps the same Apple MLX stack mlx_lm exposes as a library. Its README headline claims "4.2x faster than Ollama". Nobody had published it against the thing it wraps, on an invalidated cache, so that is what this measures.

Everything here ran on one Mac Studio, M1 Ultra, 20 cores, 128GB, macOS 26.6.2, Ollama 0.33.2, mlx 0.32.2, mlx-lm 0.31.3, rapid-mlx 0.13.3. Fifty-four runs, zero errors, zero token-parity warnings.

Write-up: https://zachrattner.com/projects/ai-mac-cluster/mlx-vs-ollama

The headline

A well-built wrapper buys a few percent over the library it wraps, and it does not change which engine you should pick.

Decode tokens per second, median of three runs:

Model Prompt Ollama vanilla MLX Rapid-MLX Rapid vs MLX Rapid vs Ollama
Qwen3.8-27B dense 4-bit 512 20.86 30.72 31.86 +3.7% +52.7%
Qwen3.8-27B dense 4-bit 4096 22.07 29.61 30.92 +4.4% +40.1%
Qwen3.8-27B dense 4-bit 16384 19.00 27.41 27.95 +1.9% +47.1%
Qwen3-30B-A3B MoE 4-bit 512 91.23 68.06 72.50 +6.5% -20.5%
Qwen3-30B-A3B MoE 4-bit 4096 80.50 60.01 64.00 +6.6% -20.5%
Qwen3-30B-A3B MoE 4-bit 16384 57.76 45.62 45.01 -1.3% -22.1%

Time to first token, seconds, median of three:

Model Prompt Ollama vanilla MLX Rapid-MLX
Qwen3.8-27B dense 4-bit 512 2.315 3.638 3.240
Qwen3.8-27B dense 4-bit 4096 19.627 18.549 18.273
Qwen3.8-27B dense 4-bit 16384 85.107 77.941 77.206
Qwen3-30B-A3B MoE 4-bit 512 0.442 0.979 0.856
Qwen3-30B-A3B MoE 4-bit 4096 2.920 3.614 3.655
Qwen3-30B-A3B MoE 4-bit 16384 20.433 20.874 21.650

Four things fall out of that.

The 4.2x tagline measures 1.40x to 1.53x on the dense model, and 0.79x on the mixture-of-experts one. On MoE, Rapid-MLX is slower than Ollama. Worth being precise about where the inflation is: Rapid-MLX's own published benchmark measures 1.46x and 1.50x on decode, which is almost exactly what I get. Their measurement is credible. It is the repository headline that is not, and their blog even says "up to 3x" rather than 4.2x.

Ollama still wins on mixture-of-experts, and by more than before. It beats both MLX-based engines at every prompt length. This is robust to the one baseline discrepancy below: even using the previously published Ollama MoE figure of 83.32 rather than this session's 91.23, Ollama beats Rapid-MLX by 14.9 percent.

Prefill depends on the architecture rather than being uniformly weak. I expected Rapid-MLX to lose on prefill, since its own blog volunteers that llama.cpp beats it by 1.7 to 2x on long documents. On the dense model it had the best TTFT of all three at 4096 and 16384 tokens. On the MoE it was last.

Speculative decoding never fired, 0 attempts before and after the MoE run, so it is measured out rather than assumed. rapid-mlx info reports it as supported for the MoE checkpoint and disabled for the dense hybrid one.

The prompt cache trap reproduces on Rapid-MLX

This is the part worth taking away, and it generalises past any one engine.

The original harness exists because Ollama caches the KV state for a repeated prompt prefix and then reports a prompt_eval_duration covering the cache hit rather than the work. Rapid-MLX does the same thing. At 16,384 prompt tokens, sending an identical prompt twice and then a fresh one:

Engine First Identical repeat Fresh nonce Verdict
Ollama 90.625s 0.191s 85.433s nonce defeats it
vanilla MLX 84.652s 77.831s 78.328s no cache to defeat
Rapid-MLX 85.714s 1.159s 77.350s nonce defeats it

A 74x fiction on Rapid-MLX, against Ollama's 474x, on the same check.

So Rapid-MLX's other headline number, "0.08s cached TTFT", is a cache lookup and not compute. It says cached right in the tagline, so it is disclosed rather than concealed, which is more than most vendor numbers manage. The point is not that either project is cheating. It is that prompt caching is now normal, and any benchmark that sends the same prompt several times and averages the result is measuring a lookup on every engine that has one. Vanilla MLX rebuilds its cache every call, which is why it is the only column here with nothing to defeat, and why a naive comparison makes it look slow against both servers.

bench-3way.py --verify-cache-defeat runs that check, and it is worth running before trusting any number a caching server reports.

How the fairness problem was handled

Rapid-MLX exposes no native timing. Its /v1/completions usage block carries prompt_tokens and completion_tokens and no durations, and its Prometheus /metrics endpoint has no per-request prefill or decode timing either.

The original harness deliberately reads engine-native counters: Ollama returns nanosecond durations alongside its token counts, and mlx_lm reports prompt_tps and generation_tps. That excludes HTTP and process overhead, which is right when both sides expose such counters.

Timing Rapid-MLX client-side while reading native counters for the other two would charge it for overhead vanilla MLX never pays and Ollama's counters exclude. That is the mirror image of the cache error above: a number that looks like a like-for-like comparison and is not one.

So every engine is timed client-side, identically, and that is the comparison basis. Native counters are still recorded where an engine provides them, which is what lets the Ollama and MLX rows be checked against the original run. Client-side tracked native within about 3 percent throughout.

Definitions, applied the same way to all three:

  • client_ttft_s — request dispatch to first content token
  • client_decode_tps — (gen_tokens - 1) / (last token time - first token time)
  • client_total_s — dispatch to stream close

The first token is excluded from decode because its arrival is gated by prefill. There is deliberately no client-side prefill throughput column: dividing prompt tokens by TTFT folds in queueing and first-token sampling and is not prefill throughput. TTFT is reported directly instead.

What this does not control for

Vanilla MLX runs in-process, so its client-side timing has no socket in it while both servers cross localhost. That asymmetry favours vanilla MLX by whatever a localhost round trip costs, which is small next to a multi-second generation but is not zero. It is a floor on vanilla MLX's apparent advantage and a ceiling on the two servers', and removing it would mean wrapping mlx_lm in a server of its own, which would then be measuring a server.

Three copies of the same model were resident at once, one per engine, which put the machine at 56.5GB wired with 32.4GB in the compressor. Vanilla MLX still landed within 0.6 percent of its previously published figures on all six cells, which is what licenses the comparison. Ollama was within 0.8 percent on four of six cells, off by -4.4 percent on the dense 512 cell, which traces to the very first run of the session, and +9.9 percent on the MoE 512 cell.

One MoE checkpoint and one dense checkpoint, on 2022 silicon. Rapid-MLX's own MoE benchmark uses a different model on an M2 Pro and reports the opposite direction against Ollama. Both can be true; they are different checkpoints on different hardware, and this one says what happens on an M1 Ultra with Qwen3-30B-A3B.

Controls inherited from the original harness

Same weights per cell, the chat template applied once and sent raw to both servers so nothing is templated twice, a fresh nonce per repetition to defeat prefix caching on every engine, one discarded warm-up per cell per engine, thinking disabled, medians with the full range rather than a best-of, and engine order rotated across repetitions so thermal drift is shared rather than charged to whichever engine always ran last.

Token parity across engines is asserted on every repetition. If the three engines did not receive the same token count, the cell is meaningless, and the run reports it rather than quietly publishing it.

Files

File What it holds
bench-3way.py The harness. Written as a companion to bench.py rather than a change to it, so the original 54 runs stay reproducible
run-3way-moe.sh Restarts the server on the MoE checkpoint and runs that half, since Rapid-MLX serves one model per process
results-3way-4bit.json Qwen3.8-27B dense 4-bit, all three engines, full runs plus medians
results-3way-moe.json Qwen3-30B-A3B MoE 4-bit, all three engines
cache-defeat-27b-4bit.json The same-prompt-twice-then-nonce check, all three engines

Each results file carries the full environment block, every individual run under runs, and a summary with the median, min and max per cell, so the medians can be recomputed rather than taken on trust.

Setup

Two virtual environments, deliberately. Rapid-MLX pins its own mlx and mlx-lm, and installing it alongside the benchmark would risk moving the mlx-lm version that vanilla MLX is being measured with. It happens to pin the same versions today, which is a useful accident rather than something to rely on.

# The benchmark itself, and the vanilla MLX column
uv venv --python 3.13 .venv
uv pip install --python .venv/bin/python -r requirements.txt

# Rapid-MLX, isolated, talking over HTTP
uv venv --python 3.13 .venv-rapidmlx
uv pip install --python .venv-rapidmlx/bin/python rapid-mlx==0.13.3

Running it

Rapid-MLX holds one model per process, so the server is started outside the harness and the harness refuses to run if the served repo is not the one the cell needs. That guard exists because the original MoE run was discarded once for pairing an Ollama tag with an MLX build of a different checkpoint.

.venv-rapidmlx/bin/rapid-mlx --no-telemetry serve mlx-community/Qwen3.8-27B-4bit \
  --host 127.0.0.1 --port 8080 --no-thinking

# Prove the nonce defeats every engine's cache before trusting anything
.venv/bin/python bench-3way.py --models qwen3.8-4bit --prompt-tokens 16384 \
  --gen-tokens 16 --verify-cache-defeat --output cache-defeat-27b-4bit.json

# Then the matrix
.venv/bin/python bench-3way.py --models qwen3.8-4bit --output results-3way-4bit.json

# And the MoE half, which restarts the server on the right checkpoint
./run-3way-moe.sh

Check two things before trusting any row. Do the prompt token counts match across engines? If not, they were not asked the same question. Is decode plausible against memory bandwidth? Token generation reads the active weights once per token, so decode cannot meaningfully exceed memory bandwidth divided by model size. An M1 Ultra at 800GB/s on a 15GB model tops out near 53 tokens per second in theory and lands well below it in practice. A number implying otherwise means the harness is measuring the wrong thing, not that you have found free performance.

#!/usr/bin/env python3
"""
Three-way benchmark: Ollama, vanilla MLX, and Rapid-MLX, on the same weights.
Why this is a separate file rather than a change to bench.py
------------------------------------------------------------
`bench.py` and the three `results-*.json` files it produced are the evidence
behind a published article. Mutating it would make those 54 runs
unreproducible. This script imports its controls instead, so the published
harness stays byte-identical and the third engine is additive.
What it adds beyond bench.py
----------------------------
**A third engine.** Rapid-MLX (`rapid-mlx`, distributed as the `vllm_mlx`
package) is an OpenAI-compatible server that wraps the same Apple MLX stack
`mlx_lm` exposes as a library. It is served the *same* MLX repo the vanilla MLX
path loads, so this is wrapper-against-library rather than a different model.
**Client-side timing for every engine, as the comparison basis.** This is the
methodological crux and the reason the file exists.
bench.py deliberately reads engine-native counters: Ollama returns
`prompt_eval_count`/`eval_count` with nanosecond durations, and `mlx_lm`
reports `prompt_tps`/`generation_tps`. That excludes HTTP and process overhead,
which is right when both sides expose such counters.
Rapid-MLX exposes none. Its `/v1/completions` usage block carries
`prompt_tokens` and `completion_tokens` and no durations, and its Prometheus
`/metrics` endpoint has no per-request prefill or decode timing either. So
there is no native number to read.
Timing Rapid-MLX client-side while reading native counters for the other two
would charge Rapid-MLX for HTTP, serialization, and scheduler overhead that
vanilla MLX never pays and Ollama's counters exclude. That is the mirror image
of the prompt-cache error the article documents: a number that looks like a
like-for-like comparison and is not one.
So every engine is timed client-side, identically, and those columns are what
the three-way comparison uses. Native counters are still recorded where an
engine provides them, so the Ollama and MLX rows stay checkable against the
published 54 runs.
Client-side definitions, applied the same way to all three:
client_ttft_s request dispatch to first content token
client_decode_tps (gen_tokens - 1) / (last token time - first token time)
client_total_s dispatch to stream close
`client_decode_tps` drops the first token because it is prefill-bound, which is
the standard client-side convention and the one Rapid-MLX's own published
benchmark uses. There is no client-side prefill throughput column: dividing
prompt tokens by TTFT folds in queueing and first-token sampling and is not
prefill throughput. TTFT is reported directly instead, which is the honest
client-observable prefill signal.
**In-process MLX is not charged HTTP it does not pay, and this is disclosed.**
Vanilla MLX runs in this process, so its client-side timing has no socket in
it, while Ollama and Rapid-MLX both cross localhost. That asymmetry favours
vanilla MLX by however much a localhost round trip costs, which is small next
to a multi-second generation but is not zero. It is a floor on vanilla MLX's
apparent advantage and a ceiling on the two servers', and it cannot be removed
without wrapping mlx_lm in a server of its own, which would then be measuring
a server this article is not about.
Controls inherited from bench.py
--------------------------------
Same weights per cell, the chat template applied once here and sent raw to both
servers, a fresh nonce per repetition to defeat prefix caching on every engine,
one discarded warm-up per cell per engine, thinking disabled, medians with the
full range, and engine order rotated across repetitions so thermal drift is
shared rather than charged to whichever ran last.
Running it
----------
Rapid-MLX serves one model per process, so the server is managed outside this
script and the script verifies it is serving the model it is about to measure:
.venv-rapidmlx/bin/rapid-mlx --no-telemetry serve <mlx repo> \
--host 127.0.0.1 --port 8080 --no-thinking
.venv/bin/python bench-3way.py --models qwen3.8-4bit --output results-3way-4bit.json
`--verify-cache-defeat` runs the check the README describes for Ollama against
all three engines: the same prompt twice, then a fresh nonce. A repeat that is
much faster than the first is a cache hit, and the nonce must restore the
original cost. If it does not, the nonce is not defeating that engine's cache
and every number in the run is measuring a lookup.
"""
from __future__ import annotations
import argparse
import json
import statistics
import time
import urllib.error
import urllib.request
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any, Iterable
from uuid import uuid4
import bench
from bench import (
MIN_TRUSTWORTHY_GEN_TOKENS,
MODELS,
build_prompt,
environment,
ollama_has,
ollama_pull,
ollama_resident_gb,
)
RAPIDMLX_HOST = "http://127.0.0.1:8080"
ENGINES = ["ollama", "mlx", "rapidmlx"]
DEFAULT_MODELS = ["qwen3.8-4bit", "qwen3-30b-a3b-4bit"]
DEFAULT_PROMPT_TOKENS = [512, 4096, 16384]
DEFAULT_GEN_TOKENS = 256
DEFAULT_REPS = 3
@dataclass
class Run:
model_key: str
engine: str
prompt_target: int
rep: int
prompt_tokens: int = 0
gen_tokens: int = 0
# Engine-native counters. Absent for rapidmlx, which exposes none.
native_prefill_tps: float = 0.0
native_decode_tps: float = 0.0
has_native: bool = False
# Client-side, measured identically for every engine. The comparison basis.
client_ttft_s: float = 0.0
client_decode_tps: float = 0.0
client_total_s: float = 0.0
peak_memory_gb: float = 0.0
finish_reason: str = ""
error: str = ""
warnings: list[str] = field(default_factory=list)
# ---------------------------------------------------------------------------
# Engine runners. Each returns the same contract, so measure() stays uniform.
# ---------------------------------------------------------------------------
def run_ollama(model_tag: str, prompt: str, gen_tokens: int, num_ctx: int) -> dict[str, Any]:
payload = {
"model": model_tag,
"prompt": prompt,
"stream": True,
"raw": True, # Template already applied. Do not apply it twice.
"think": False,
"options": {
"num_predict": gen_tokens,
"num_ctx": num_ctx,
"temperature": 0,
"seed": 42,
},
"keep_alive": "10m",
}
request = urllib.request.Request(
f"{bench.OLLAMA_HOST}/api/generate",
data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json"},
)
started = time.perf_counter()
first = last = 0.0
final: dict[str, Any] = {}
with urllib.request.urlopen(request, timeout=3600) as response:
for raw_line in response:
line = raw_line.decode().strip()
if not line:
continue
message = json.loads(line)
if message.get("response"):
now = time.perf_counter()
if not first:
first = now
last = now
if message.get("done"):
final = message
total = time.perf_counter() - started
prompt_ns = final.get("prompt_eval_duration") or 0
eval_ns = final.get("eval_duration") or 0
prompt_count = final.get("prompt_eval_count") or 0
eval_count = final.get("eval_count") or 0
return {
"prompt_tokens": prompt_count,
"gen_tokens": eval_count,
"native_prefill_tps": (prompt_count / (prompt_ns / 1e9)) if prompt_ns else 0.0,
"native_decode_tps": (eval_count / (eval_ns / 1e9)) if eval_ns else 0.0,
"has_native": True,
"client_ttft_s": (first - started) if first else 0.0,
"client_decode_tps": _client_decode(eval_count, first, last),
"client_total_s": total,
"peak_memory_gb": ollama_resident_gb(model_tag),
"finish_reason": final.get("done_reason", ""),
}
class MLX:
"""Vanilla mlx_lm, loaded once so load cost is not charged per run."""
def __init__(self, repo: str):
import mlx.core as mx
from mlx_lm import load
mx.reset_peak_memory()
self.model, self.tokenizer = load(repo)
self.repo = repo
def run(self, prompt: str, gen_tokens: int) -> dict[str, Any]:
import mlx.core as mx
from mlx_lm import stream_generate
from mlx_lm.sample_utils import make_sampler
mx.random.seed(42)
sampler = make_sampler(temp=0.0)
started = time.perf_counter()
first = last = 0.0
final = None
for response in stream_generate(
self.model, self.tokenizer, prompt, max_tokens=gen_tokens, sampler=sampler
):
now = time.perf_counter()
if not first:
first = now
last = now
final = response
total = time.perf_counter() - started
if final is None:
return {"error": "no tokens generated"}
return {
"prompt_tokens": final.prompt_tokens,
"gen_tokens": final.generation_tokens,
"native_prefill_tps": final.prompt_tps,
"native_decode_tps": final.generation_tps,
"has_native": True,
"client_ttft_s": (first - started) if first else 0.0,
"client_decode_tps": _client_decode(final.generation_tokens, first, last),
"client_total_s": total,
"peak_memory_gb": round(final.peak_memory, 2),
"finish_reason": final.finish_reason or "",
}
def run_rapidmlx(model_id: str, prompt: str, gen_tokens: int) -> dict[str, Any]:
"""
One Rapid-MLX generation over /v1/completions.
/v1/completions rather than /v1/chat/completions because the chat endpoint
would apply its own template to an already-templated string, changing the
token sequence and invalidating the cell. The token-parity assertion in
measure() is what proves this worked.
"""
payload = {
"model": model_id,
"prompt": prompt,
"max_tokens": gen_tokens,
"temperature": 0,
"stream": True,
"stream_options": {"include_usage": True},
}
request = urllib.request.Request(
f"{RAPIDMLX_HOST}/v1/completions",
data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json"},
)
started = time.perf_counter()
first = last = 0.0
usage: dict[str, Any] = {}
finish = ""
with urllib.request.urlopen(request, timeout=3600) as response:
for raw_line in response:
line = raw_line.decode().strip()
if not line.startswith("data:"):
continue
body = line[5:].strip()
if body == "[DONE]":
break
message = json.loads(body)
for choice in message.get("choices") or []:
if choice.get("text"):
now = time.perf_counter()
if not first:
first = now
last = now
if choice.get("finish_reason"):
finish = choice["finish_reason"]
if message.get("usage"):
usage = message["usage"]
total = time.perf_counter() - started
gen = usage.get("completion_tokens") or 0
return {
"prompt_tokens": usage.get("prompt_tokens") or 0,
"gen_tokens": gen,
"native_prefill_tps": 0.0,
"native_decode_tps": 0.0,
"has_native": False,
"client_ttft_s": (first - started) if first else 0.0,
"client_decode_tps": _client_decode(gen, first, last),
"client_total_s": total,
"peak_memory_gb": 0.0, # Not exposed per request; recorded once in environment.
"finish_reason": finish,
}
def _client_decode(gen_tokens: int, first: float, last: float) -> float:
"""
Tokens after the first, over the time between the first and last.
The first token is excluded because its arrival is gated by prefill, so
including it reports a decode rate depressed by prompt processing. This is
the standard client-side convention.
"""
span = last - first
if gen_tokens < 2 or span <= 0:
return 0.0
return (gen_tokens - 1) / span
# ---------------------------------------------------------------------------
# Rapid-MLX server checks
# ---------------------------------------------------------------------------
def rapidmlx_served_model() -> str | None:
try:
with urllib.request.urlopen(f"{RAPIDMLX_HOST}/v1/models", timeout=10) as response:
data = json.loads(response.read())
entries = data.get("data") or []
return entries[0].get("id") if entries else None
except Exception:
return None
def rapidmlx_metric(name: str) -> float:
"""Read one Prometheus counter, for the cache-defeat check."""
try:
with urllib.request.urlopen(f"{RAPIDMLX_HOST}/metrics", timeout=10) as response:
for line in response.read().decode().splitlines():
if line.startswith("#") or not line.strip():
continue
key, _, value = line.rpartition(" ")
if key.split("{")[0].strip() == name:
return float(value)
except Exception:
pass
return 0.0
# ---------------------------------------------------------------------------
# Cache-defeat verification
# ---------------------------------------------------------------------------
def verify_cache_defeat(model_key: str, target: int, gen_tokens: int) -> dict[str, Any]:
"""
The README's Ollama check, applied to all three engines.
Same prompt twice, then a fresh nonce. A second run much faster than the
first is a cache hit. The nonce run must return to roughly the first run's
cost; if it stays fast, the nonce is not defeating that engine's cache and
every prefill number in the run is a lookup rather than work.
"""
spec = MODELS[model_key]
print(f"\n cache-defeat check on {spec['label']} at {target} prompt tokens", flush=True)
mlx = MLX(spec["mlx"])
fixed = build_prompt(mlx.tokenizer, target, nonce="[fixed-prefix] ")
report: dict[str, Any] = {}
for engine in ENGINES:
def once(prompt: str) -> float:
if engine == "ollama":
data = run_ollama(spec["ollama"], prompt, gen_tokens, target + gen_tokens + 512)
elif engine == "mlx":
data = mlx.run(prompt, gen_tokens)
else:
data = run_rapidmlx(rapidmlx_served_model() or "", prompt, gen_tokens)
return data["client_ttft_s"]
first = once(fixed)
repeat = once(fixed)
fresh = once(build_prompt(mlx.tokenizer, target, nonce=f"[{uuid4().hex}] "))
cached = repeat < first * 0.6
restored = fresh > repeat * 1.4 if cached else True
report[engine] = {
"ttft_first_s": round(first, 4),
"ttft_repeat_s": round(repeat, 4),
"ttft_fresh_nonce_s": round(fresh, 4),
"repeat_was_cache_hit": cached,
"nonce_restored_cost": restored,
}
verdict = "no cache to defeat" if not cached else (
"nonce defeats it" if restored else "NONCE FAILED"
)
print(
f" {engine:<9} first {first:6.3f}s repeat {repeat:6.3f}s "
f"fresh {fresh:6.3f}s {verdict}",
flush=True,
)
del mlx
return report
# ---------------------------------------------------------------------------
# Orchestration
# ---------------------------------------------------------------------------
def measure(
model_key: str,
prompt_targets: Iterable[int],
reps: int,
gen_tokens: int,
) -> list[Run]:
spec = MODELS[model_key]
results: list[Run] = []
served = rapidmlx_served_model()
if served is None:
raise SystemExit(
f"Rapid-MLX is not answering at {RAPIDMLX_HOST}. Start it with:\n"
f" .venv-rapidmlx/bin/rapid-mlx --no-telemetry serve {spec['mlx']} "
f"--host 127.0.0.1 --port 8080 --no-thinking"
)
if served != spec["mlx"]:
raise SystemExit(
f"Rapid-MLX is serving '{served}' but this cell needs '{spec['mlx']}'.\n"
"Restart the server on the right repo. Measuring a different checkpoint "
"would be the exact defect the article's MoE re-run exists to avoid."
)
print(f" loading vanilla MLX {spec['mlx']} ...", flush=True)
mlx = MLX(spec["mlx"])
tokenizer = mlx.tokenizer
if not ollama_has(spec["ollama"]):
ollama_pull(spec["ollama"])
for target in prompt_targets:
num_ctx = max(2048, target + gen_tokens + 512)
# One discarded warm-up per engine per cell, each with its own nonce so
# it cannot prime a cache for the measured runs.
for engine in ENGINES:
warm = build_prompt(tokenizer, target, nonce=f"[{uuid4().hex}] ")
try:
if engine == "ollama":
run_ollama(spec["ollama"], warm, min(8, gen_tokens), num_ctx)
elif engine == "mlx":
mlx.run(warm, min(8, gen_tokens))
else:
run_rapidmlx(served, warm, min(8, gen_tokens))
except Exception as exc: # noqa: BLE001 - a failed warm-up is not fatal
print(f" warm-up failed for {engine}: {exc}", flush=True)
for rep in range(1, reps + 1):
# One fresh prompt per repetition, shared by all three engines. Sharing
# within a repetition is what makes the comparison fair; it is reuse
# across repetitions that has to be defeated.
prompt = build_prompt(tokenizer, target, nonce=f"[{uuid4().hex}] ")
# Rotate the order so each engine takes each position equally across
# three repetitions, sharing thermal drift rather than charging it to
# whichever engine always ran last.
shift = (rep - 1) % len(ENGINES)
order = ENGINES[shift:] + ENGINES[:shift]
seen_prompt_tokens: dict[str, int] = {}
for engine in order:
result = Run(
model_key=model_key, engine=engine, prompt_target=target, rep=rep
)
try:
if engine == "ollama":
data = run_ollama(spec["ollama"], prompt, gen_tokens, num_ctx)
elif engine == "mlx":
data = mlx.run(prompt, gen_tokens)
else:
data = run_rapidmlx(served, prompt, gen_tokens)
for key, value in data.items():
setattr(result, key, value)
if result.gen_tokens < MIN_TRUSTWORTHY_GEN_TOKENS:
result.warnings.append(
f"only {result.gen_tokens} tokens generated; decode rate is noisy"
)
seen_prompt_tokens[engine] = result.prompt_tokens
except Exception as exc: # noqa: BLE001 - record and continue
result.error = f"{type(exc).__name__}: {exc}"
status = result.error or (
f"ttft {result.client_ttft_s:6.3f}s "
f"decode {result.client_decode_tps:6.2f} tok/s "
f"native {result.native_decode_tps:6.2f} "
f"({result.prompt_tokens} in, {result.gen_tokens} out)"
)
print(f" {engine:<9} {target:>6} rep{rep} {status}", flush=True)
results.append(result)
# Token parity is the harness's own validity test. If the engines were
# not asked the same question, the cell means nothing.
distinct = set(seen_prompt_tokens.values())
if len(distinct) > 1:
message = f"prompt token mismatch across engines: {seen_prompt_tokens}"
print(f" WARNING {message}", flush=True)
for result in results[-len(order):]:
result.warnings.append(message)
return results
def summarize(runs: list[Run]) -> list[dict[str, Any]]:
grouped: dict[tuple[str, str, int], list[Run]] = {}
for run in runs:
if run.error:
continue
grouped.setdefault((run.model_key, run.engine, run.prompt_target), []).append(run)
rows = []
for (model_key, engine, target), items in sorted(grouped.items()):
def stat(attr: str) -> dict[str, float]:
values = [getattr(item, attr) for item in items]
return {
"median": round(statistics.median(values), 3),
"min": round(min(values), 3),
"max": round(max(values), 3),
}
rows.append(
{
"model_key": model_key,
"label": MODELS[model_key]["label"],
"engine": engine,
"prompt_target": target,
"prompt_tokens": statistics.median([i.prompt_tokens for i in items]),
"gen_tokens": statistics.median([i.gen_tokens for i in items]),
"reps": len(items),
"client_ttft_s": stat("client_ttft_s"),
"client_decode_tps": stat("client_decode_tps"),
"client_total_s": stat("client_total_s"),
"native_prefill_tps": stat("native_prefill_tps"),
"native_decode_tps": stat("native_decode_tps"),
"has_native": items[0].has_native,
"peak_memory_gb": max(i.peak_memory_gb for i in items),
}
)
return rows
def markdown_table(rows: list[dict[str, Any]]) -> str:
out = [
"| Model | Prompt | Engine | TTFT s (client) | Decode tok/s (client) | Decode tok/s (native) |",
"| --- | --- | --- | --- | --- | --- |",
]
for row in rows:
native = (
f"{row['native_decode_tps']['median']:.2f}" if row["has_native"] else "not exposed"
)
out.append(
f"| {row['label']} | {row['prompt_target']} | {row['engine']} "
f"| {row['client_ttft_s']['median']:.3f} "
f"| {row['client_decode_tps']['median']:.2f} | {native} |"
)
return "\n".join(out)
def main() -> int:
global RAPIDMLX_HOST
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--models", nargs="+", default=DEFAULT_MODELS, choices=list(MODELS))
parser.add_argument("--prompt-tokens", nargs="+", type=int, default=DEFAULT_PROMPT_TOKENS)
parser.add_argument("--gen-tokens", type=int, default=DEFAULT_GEN_TOKENS)
parser.add_argument("--reps", type=int, default=DEFAULT_REPS)
parser.add_argument("--output", default="results-3way.json")
parser.add_argument("--rapidmlx-host", default=RAPIDMLX_HOST)
parser.add_argument(
"--verify-cache-defeat",
action="store_true",
help="Run the same-prompt-twice-then-nonce check on all three engines and exit.",
)
args = parser.parse_args()
RAPIDMLX_HOST = args.rapidmlx_host
if not bench.ollama_available():
print("Ollama is not answering. Start it and retry.")
return 1
env = environment()
env["rapidmlx"] = _rapidmlx_version()
env["rapidmlx_served_model"] = rapidmlx_served_model()
env["timing_basis"] = (
"client-side for all engines; native counters recorded where exposed. "
"rapidmlx exposes no native prefill or decode timing."
)
print(json.dumps(env, indent=2), flush=True)
if args.verify_cache_defeat:
checks = {
key: verify_cache_defeat(key, max(args.prompt_tokens), args.gen_tokens)
for key in args.models
}
with open(args.output, "w") as handle:
json.dump({"environment": env, "cache_defeat": checks}, handle, indent=2)
print(f"\nwrote {args.output}")
return 0
runs: list[Run] = []
for key in args.models:
print(f"\n{MODELS[key]['label']}", flush=True)
runs.extend(measure(key, args.prompt_tokens, args.reps, args.gen_tokens))
rows = summarize(runs)
payload = {
"environment": env,
"config": {
"engines": ENGINES,
"models": args.models,
"prompt_tokens": args.prompt_tokens,
"gen_tokens": args.gen_tokens,
"reps": args.reps,
},
"summary": rows,
"runs": [asdict(run) for run in runs],
"generated_at": datetime.now(timezone.utc).isoformat(),
}
with open(args.output, "w") as handle:
json.dump(payload, handle, indent=2)
print("\n" + markdown_table(rows))
print(f"\nwrote {args.output}")
return 0
def _rapidmlx_version() -> str:
"""
Read the version from the sibling venv's dist-info rather than this one.
Rapid-MLX is deliberately installed in .venv-rapidmlx so that its own
mlx/mlx-lm pins cannot disturb the pinned mlx-lm==0.31.3 this benchmark
measures vanilla MLX with. So importlib.metadata in *this* interpreter
would never find it.
"""
import glob
import os
here = os.path.dirname(os.path.abspath(__file__))
pattern = os.path.join(here, ".venv-rapidmlx", "lib", "python*", "site-packages", "rapid_mlx-*.dist-info")
for path in sorted(glob.glob(pattern)):
name = os.path.basename(path)
return name[len("rapid_mlx-") : -len(".dist-info")]
return "unknown"
if __name__ == "__main__":
raise SystemExit(main())
{
"environment": {
"captured_at": "2026-09-02T11:08:53.860000+00:00",
"chip": "Apple M1 Ultra",
"cores": "20",
"memory_bytes": "137438953472",
"macos": "26.6.2",
"macos_build": "25G83",
"python": "3.13.15",
"ollama": "ollama version is 0.33.2",
"mlx": "0.32.2",
"mlx_lm": "0.31.3",
"rapidmlx": "0.13.3",
"rapidmlx_served_model": "mlx-community/Qwen3.8-27B-4bit",
"timing_basis": "client-side for all engines; native counters recorded where exposed. rapidmlx exposes no native prefill or decode timing."
},
"cache_defeat": {
"qwen3.8-4bit": {
"ollama": {
"ttft_first_s": 90.6247,
"ttft_repeat_s": 0.1906,
"ttft_fresh_nonce_s": 85.4329,
"repeat_was_cache_hit": true,
"nonce_restored_cost": true
},
"mlx": {
"ttft_first_s": 84.6525,
"ttft_repeat_s": 77.8307,
"ttft_fresh_nonce_s": 78.3277,
"repeat_was_cache_hit": false,
"nonce_restored_cost": true
},
"rapidmlx": {
"ttft_first_s": 85.7141,
"ttft_repeat_s": 1.1595,
"ttft_fresh_nonce_s": 77.3501,
"repeat_was_cache_hit": true,
"nonce_restored_cost": true
}
}
}
}
{
"environment": {
"captured_at": "2026-09-02T11:19:34.467809+00:00",
"chip": "Apple M1 Ultra",
"cores": "20",
"memory_bytes": "137438953472",
"macos": "26.6.2",
"macos_build": "25G83",
"python": "3.13.15",
"ollama": "ollama version is 0.33.2",
"mlx": "0.32.2",
"mlx_lm": "0.31.3",
"rapidmlx": "0.13.3",
"rapidmlx_served_model": "mlx-community/Qwen3.8-27B-4bit",
"timing_basis": "client-side for all engines; native counters recorded where exposed. rapidmlx exposes no native prefill or decode timing."
},
"config": {
"engines": [
"ollama",
"mlx",
"rapidmlx"
],
"models": [
"qwen3.8-4bit"
],
"prompt_tokens": [
512,
4096,
16384
],
"gen_tokens": 256,
"reps": 3
},
"summary": [
{
"model_key": "qwen3.8-4bit",
"label": "Qwen3.8-27B 4-bit",
"engine": "mlx",
"prompt_target": 512,
"prompt_tokens": 501,
"gen_tokens": 256,
"reps": 3,
"client_ttft_s": {
"median": 3.638,
"min": 3.052,
"max": 10.444
},
"client_decode_tps": {
"median": 30.724,
"min": 30.711,
"max": 30.748
},
"client_total_s": {
"median": 11.953,
"min": 11.361,
"max": 18.763
},
"native_prefill_tps": {
"median": 144.827,
"min": 48.77,
"max": 174.137
},
"native_decode_tps": {
"median": 30.844,
"min": 30.832,
"max": 30.869
},
"has_native": true,
"peak_memory_gb": 16.42
},
{
"model_key": "qwen3.8-4bit",
"label": "Qwen3.8-27B 4-bit",
"engine": "mlx",
"prompt_target": 4096,
"prompt_tokens": 3961,
"gen_tokens": 256,
"reps": 3,
"client_ttft_s": {
"median": 18.549,
"min": 18.496,
"max": 28.944
},
"client_decode_tps": {
"median": 29.614,
"min": 28.164,
"max": 29.747
},
"client_total_s": {
"median": 27.136,
"min": 27.121,
"max": 38.2
},
"native_prefill_tps": {
"median": 215.758,
"min": 138.04,
"max": 215.99
},
"native_decode_tps": {
"median": 29.73,
"min": 28.275,
"max": 29.864
},
"has_native": true,
"peak_memory_gb": 18.34
},
{
"model_key": "qwen3.8-4bit",
"label": "Qwen3.8-27B 4-bit",
"engine": "mlx",
"prompt_target": 16384,
"prompt_tokens": 16455,
"gen_tokens": 256,
"reps": 3,
"client_ttft_s": {
"median": 77.941,
"min": 77.9,
"max": 77.955
},
"client_decode_tps": {
"median": 27.412,
"min": 27.386,
"max": 27.466
},
"client_total_s": {
"median": 87.239,
"min": 87.217,
"max": 87.281
},
"native_prefill_tps": {
"median": 211.711,
"min": 211.601,
"max": 211.803
},
"native_decode_tps": {
"median": 27.519,
"min": 27.494,
"max": 27.574
},
"has_native": true,
"peak_memory_gb": 20.56
},
{
"model_key": "qwen3.8-4bit",
"label": "Qwen3.8-27B 4-bit",
"engine": "ollama",
"prompt_target": 512,
"prompt_tokens": 501,
"gen_tokens": 256,
"reps": 3,
"client_ttft_s": {
"median": 2.315,
"min": 2.312,
"max": 2.383
},
"client_decode_tps": {
"median": 20.861,
"min": 17.215,
"max": 20.963
},
"client_total_s": {
"median": 14.538,
"min": 14.477,
"max": 17.196
},
"native_prefill_tps": {
"median": 223.543,
"min": 222.204,
"max": 224.649
},
"native_decode_tps": {
"median": 20.944,
"min": 17.282,
"max": 21.045
},
"has_native": true,
"peak_memory_gb": 17.99
},
{
"model_key": "qwen3.8-4bit",
"label": "Qwen3.8-27B 4-bit",
"engine": "ollama",
"prompt_target": 4096,
"prompt_tokens": 3961,
"gen_tokens": 256,
"reps": 3,
"client_ttft_s": {
"median": 19.627,
"min": 19.611,
"max": 20.264
},
"client_decode_tps": {
"median": 22.074,
"min": 21.302,
"max": 22.401
},
"client_total_s": {
"median": 31.163,
"min": 31.01,
"max": 32.236
},
"native_prefill_tps": {
"median": 203.047,
"min": 201.073,
"max": 203.314
},
"native_decode_tps": {
"median": 22.161,
"min": 21.385,
"max": 22.489
},
"has_native": true,
"peak_memory_gb": 18.15
},
{
"model_key": "qwen3.8-4bit",
"label": "Qwen3.8-27B 4-bit",
"engine": "ollama",
"prompt_target": 16384,
"prompt_tokens": 16455,
"gen_tokens": 256,
"reps": 3,
"client_ttft_s": {
"median": 85.107,
"min": 85.062,
"max": 85.498
},
"client_decode_tps": {
"median": 18.995,
"min": 18.459,
"max": 19.02
},
"client_total_s": {
"median": 98.923,
"min": 98.47,
"max": 98.924
},
"native_prefill_tps": {
"median": 193.889,
"min": 193.346,
"max": 194.054
},
"native_decode_tps": {
"median": 19.069,
"min": 18.531,
"max": 19.094
},
"has_native": true,
"peak_memory_gb": 18.2
},
{
"model_key": "qwen3.8-4bit",
"label": "Qwen3.8-27B 4-bit",
"engine": "rapidmlx",
"prompt_target": 512,
"prompt_tokens": 501,
"gen_tokens": 256,
"reps": 3,
"client_ttft_s": {
"median": 3.24,
"min": 3.027,
"max": 3.838
},
"client_decode_tps": {
"median": 31.856,
"min": 31.845,
"max": 31.873
},
"client_total_s": {
"median": 11.245,
"min": 11.027,
"max": 11.846
},
"native_prefill_tps": {
"median": 0.0,
"min": 0.0,
"max": 0.0
},
"native_decode_tps": {
"median": 0.0,
"min": 0.0,
"max": 0.0
},
"has_native": false,
"peak_memory_gb": 0.0
},
{
"model_key": "qwen3.8-4bit",
"label": "Qwen3.8-27B 4-bit",
"engine": "rapidmlx",
"prompt_target": 4096,
"prompt_tokens": 3961,
"gen_tokens": 256,
"reps": 3,
"client_ttft_s": {
"median": 18.273,
"min": 18.226,
"max": 111.797
},
"client_decode_tps": {
"median": 30.916,
"min": 30.878,
"max": 30.945
},
"client_total_s": {
"median": 26.522,
"min": 26.466,
"max": 120.056
},
"native_prefill_tps": {
"median": 0.0,
"min": 0.0,
"max": 0.0
},
"native_decode_tps": {
"median": 0.0,
"min": 0.0,
"max": 0.0
},
"has_native": false,
"peak_memory_gb": 0.0
},
{
"model_key": "qwen3.8-4bit",
"label": "Qwen3.8-27B 4-bit",
"engine": "rapidmlx",
"prompt_target": 16384,
"prompt_tokens": 16455,
"gen_tokens": 256,
"reps": 3,
"client_ttft_s": {
"median": 77.206,
"min": 77.121,
"max": 77.328
},
"client_decode_tps": {
"median": 27.946,
"min": 27.885,
"max": 28.005
},
"client_total_s": {
"median": 86.351,
"min": 86.227,
"max": 86.453
},
"native_prefill_tps": {
"median": 0.0,
"min": 0.0,
"max": 0.0
},
"native_decode_tps": {
"median": 0.0,
"min": 0.0,
"max": 0.0
},
"has_native": false,
"peak_memory_gb": 0.0
}
],
"runs": [
{
"model_key": "qwen3.8-4bit",
"engine": "ollama",
"prompt_target": 512,
"rep": 1,
"prompt_tokens": 500,
"gen_tokens": 256,
"native_prefill_tps": 222.20424836746537,
"native_decode_tps": 17.282171893747318,
"has_native": true,
"client_ttft_s": 2.3832638328894973,
"client_decode_tps": 17.21474015989175,
"client_total_s": 17.19635762507096,
"peak_memory_gb": 17.99,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3.8-4bit",
"engine": "mlx",
"prompt_target": 512,
"rep": 1,
"prompt_tokens": 500,
"gen_tokens": 256,
"native_prefill_tps": 48.76965995118381,
"native_decode_tps": 30.831510479377304,
"has_native": true,
"client_ttft_s": 10.443592041032389,
"client_decode_tps": 30.711169982493978,
"client_total_s": 18.763320666039363,
"peak_memory_gb": 16.41,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3.8-4bit",
"engine": "rapidmlx",
"prompt_target": 512,
"rep": 1,
"prompt_tokens": 500,
"gen_tokens": 256,
"native_prefill_tps": 0.0,
"native_decode_tps": 0.0,
"has_native": false,
"client_ttft_s": 3.8379713750910014,
"client_decode_tps": 31.84481244403732,
"client_total_s": 11.845949542010203,
"peak_memory_gb": 0.0,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3.8-4bit",
"engine": "mlx",
"prompt_target": 512,
"rep": 2,
"prompt_tokens": 503,
"gen_tokens": 256,
"native_prefill_tps": 144.82713915239265,
"native_decode_tps": 30.84404642580264,
"has_native": true,
"client_ttft_s": 3.638048707973212,
"client_decode_tps": 30.723688498023005,
"client_total_s": 11.953302166890353,
"peak_memory_gb": 16.42,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3.8-4bit",
"engine": "rapidmlx",
"prompt_target": 512,
"rep": 2,
"prompt_tokens": 503,
"gen_tokens": 256,
"native_prefill_tps": 0.0,
"native_decode_tps": 0.0,
"has_native": false,
"client_ttft_s": 3.2404604160692543,
"client_decode_tps": 31.856356895880268,
"client_total_s": 11.245345999952406,
"peak_memory_gb": 0.0,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3.8-4bit",
"engine": "ollama",
"prompt_target": 512,
"rep": 2,
"prompt_tokens": 503,
"gen_tokens": 256,
"native_prefill_tps": 223.54313649241706,
"native_decode_tps": 20.944397614989448,
"has_native": true,
"client_ttft_s": 2.314633083064109,
"client_decode_tps": 20.861432636912483,
"client_total_s": 14.53840187494643,
"peak_memory_gb": 17.99,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3.8-4bit",
"engine": "rapidmlx",
"prompt_target": 512,
"rep": 3,
"prompt_tokens": 501,
"gen_tokens": 256,
"native_prefill_tps": 0.0,
"native_decode_tps": 0.0,
"has_native": false,
"client_ttft_s": 3.026504416950047,
"client_decode_tps": 31.872923446528567,
"client_total_s": 11.02723737503402,
"peak_memory_gb": 0.0,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3.8-4bit",
"engine": "ollama",
"prompt_target": 512,
"rep": 3,
"prompt_tokens": 501,
"gen_tokens": 256,
"native_prefill_tps": 224.64937210276298,
"native_decode_tps": 21.044777861682867,
"has_native": true,
"client_ttft_s": 2.3123584580607712,
"client_decode_tps": 20.962662457060688,
"client_total_s": 14.477186749922112,
"peak_memory_gb": 17.99,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3.8-4bit",
"engine": "mlx",
"prompt_target": 512,
"rep": 3,
"prompt_tokens": 501,
"gen_tokens": 256,
"native_prefill_tps": 174.13701519180785,
"native_decode_tps": 30.8688995295947,
"has_native": true,
"client_ttft_s": 3.0517174159176648,
"client_decode_tps": 30.748417383814683,
"client_total_s": 11.360678249970078,
"peak_memory_gb": 16.42,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3.8-4bit",
"engine": "ollama",
"prompt_target": 4096,
"rep": 1,
"prompt_tokens": 3965,
"gen_tokens": 256,
"native_prefill_tps": 201.07257639845088,
"native_decode_tps": 21.384972696151365,
"has_native": true,
"client_ttft_s": 20.2644503749907,
"client_decode_tps": 21.301703525195027,
"client_total_s": 32.23560479097068,
"peak_memory_gb": 18.15,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3.8-4bit",
"engine": "mlx",
"prompt_target": 4096,
"rep": 1,
"prompt_tokens": 3965,
"gen_tokens": 256,
"native_prefill_tps": 138.0395579338572,
"native_decode_tps": 28.274621959479145,
"has_native": true,
"client_ttft_s": 28.944371917052194,
"client_decode_tps": 28.16424550501194,
"client_total_s": 38.19980175001547,
"peak_memory_gb": 18.34,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3.8-4bit",
"engine": "rapidmlx",
"prompt_target": 4096,
"rep": 1,
"prompt_tokens": 3965,
"gen_tokens": 256,
"native_prefill_tps": 0.0,
"native_decode_tps": 0.0,
"has_native": false,
"client_ttft_s": 111.7972793749068,
"client_decode_tps": 30.878154897710566,
"client_total_s": 120.05579937482253,
"peak_memory_gb": 0.0,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3.8-4bit",
"engine": "mlx",
"prompt_target": 4096,
"rep": 2,
"prompt_tokens": 3961,
"gen_tokens": 256,
"native_prefill_tps": 215.9902054182192,
"native_decode_tps": 29.73049523875912,
"has_native": true,
"client_ttft_s": 18.495740375015885,
"client_decode_tps": 29.614434721163185,
"client_total_s": 27.120802792022005,
"peak_memory_gb": 18.34,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3.8-4bit",
"engine": "rapidmlx",
"prompt_target": 4096,
"rep": 2,
"prompt_tokens": 3961,
"gen_tokens": 256,
"native_prefill_tps": 0.0,
"native_decode_tps": 0.0,
"has_native": false,
"client_ttft_s": 18.225518333958462,
"client_decode_tps": 30.944688399553282,
"client_total_s": 26.466252458980307,
"peak_memory_gb": 0.0,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3.8-4bit",
"engine": "ollama",
"prompt_target": 4096,
"rep": 2,
"prompt_tokens": 3961,
"gen_tokens": 256,
"native_prefill_tps": 203.04704878470625,
"native_decode_tps": 22.48877669488071,
"has_native": true,
"client_ttft_s": 19.626832666806877,
"client_decode_tps": 22.40120426597093,
"client_total_s": 31.010439041769132,
"peak_memory_gb": 18.15,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3.8-4bit",
"engine": "rapidmlx",
"prompt_target": 4096,
"rep": 3,
"prompt_tokens": 3961,
"gen_tokens": 256,
"native_prefill_tps": 0.0,
"native_decode_tps": 0.0,
"has_native": false,
"client_ttft_s": 18.27329820813611,
"client_decode_tps": 30.91571296105082,
"client_total_s": 26.521620167186484,
"peak_memory_gb": 0.0,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3.8-4bit",
"engine": "ollama",
"prompt_target": 4096,
"rep": 3,
"prompt_tokens": 3961,
"gen_tokens": 256,
"native_prefill_tps": 203.3140967708373,
"native_decode_tps": 22.160919962115216,
"has_native": true,
"client_ttft_s": 19.611111833015457,
"client_decode_tps": 22.074218354689766,
"client_total_s": 31.163346708053723,
"peak_memory_gb": 18.15,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3.8-4bit",
"engine": "mlx",
"prompt_target": 4096,
"rep": 3,
"prompt_tokens": 3961,
"gen_tokens": 256,
"native_prefill_tps": 215.7580463349865,
"native_decode_tps": 29.86384681328496,
"has_native": true,
"client_ttft_s": 18.54939870815724,
"client_decode_tps": 29.747276760830538,
"client_total_s": 27.135943250032142,
"peak_memory_gb": 18.34,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3.8-4bit",
"engine": "ollama",
"prompt_target": 16384,
"rep": 1,
"prompt_tokens": 16453,
"gen_tokens": 256,
"native_prefill_tps": 193.8888766789321,
"native_decode_tps": 18.531332538175086,
"has_native": true,
"client_ttft_s": 85.10729862493463,
"client_decode_tps": 18.459049024538064,
"client_total_s": 98.92292237491347,
"peak_memory_gb": 18.2,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3.8-4bit",
"engine": "mlx",
"prompt_target": 16384,
"rep": 1,
"prompt_tokens": 16453,
"gen_tokens": 256,
"native_prefill_tps": 211.60072747878186,
"native_decode_tps": 27.493766882537177,
"has_native": true,
"client_ttft_s": 77.95521029108204,
"client_decode_tps": 27.38643933089104,
"client_total_s": 87.28072312497534,
"peak_memory_gb": 20.56,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3.8-4bit",
"engine": "rapidmlx",
"prompt_target": 16384,
"rep": 1,
"prompt_tokens": 16453,
"gen_tokens": 256,
"native_prefill_tps": 0.0,
"native_decode_tps": 0.0,
"has_native": false,
"client_ttft_s": 77.20578062511049,
"client_decode_tps": 27.88464766428701,
"client_total_s": 86.35095670819283,
"peak_memory_gb": 0.0,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3.8-4bit",
"engine": "mlx",
"prompt_target": 16384,
"rep": 2,
"prompt_tokens": 16457,
"gen_tokens": 256,
"native_prefill_tps": 211.71148973160632,
"native_decode_tps": 27.574006836529197,
"has_native": true,
"client_ttft_s": 77.94090129178949,
"client_decode_tps": 27.466369214372115,
"client_total_s": 87.23916537477635,
"peak_memory_gb": 20.56,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3.8-4bit",
"engine": "rapidmlx",
"prompt_target": 16384,
"rep": 2,
"prompt_tokens": 16457,
"gen_tokens": 256,
"native_prefill_tps": 0.0,
"native_decode_tps": 0.0,
"has_native": false,
"client_ttft_s": 77.32802433311008,
"client_decode_tps": 27.94636799660207,
"client_total_s": 86.45289149996825,
"peak_memory_gb": 0.0,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3.8-4bit",
"engine": "ollama",
"prompt_target": 16384,
"rep": 2,
"prompt_tokens": 16457,
"gen_tokens": 256,
"native_prefill_tps": 193.34585501415285,
"native_decode_tps": 19.06902914060504,
"has_native": true,
"client_ttft_s": 85.49750766693614,
"client_decode_tps": 18.995216311996682,
"client_total_s": 98.92446141690016,
"peak_memory_gb": 18.2,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3.8-4bit",
"engine": "rapidmlx",
"prompt_target": 16384,
"rep": 3,
"prompt_tokens": 16455,
"gen_tokens": 256,
"native_prefill_tps": 0.0,
"native_decode_tps": 0.0,
"has_native": false,
"client_ttft_s": 77.12089179106988,
"client_decode_tps": 28.00458260928931,
"client_total_s": 86.22676750016399,
"peak_memory_gb": 0.0,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3.8-4bit",
"engine": "ollama",
"prompt_target": 16384,
"rep": 3,
"prompt_tokens": 16455,
"gen_tokens": 256,
"native_prefill_tps": 194.0542371391837,
"native_decode_tps": 19.09409555371609,
"has_native": true,
"client_ttft_s": 85.0622882079333,
"client_decode_tps": 19.01990261616298,
"client_total_s": 98.46994087495841,
"peak_memory_gb": 18.2,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3.8-4bit",
"engine": "mlx",
"prompt_target": 16384,
"rep": 3,
"prompt_tokens": 16455,
"gen_tokens": 256,
"native_prefill_tps": 211.80255572150233,
"native_decode_tps": 27.519212834730585,
"has_native": true,
"client_ttft_s": 77.90023241681047,
"client_decode_tps": 27.411776808604877,
"client_total_s": 87.21705420780927,
"peak_memory_gb": 20.56,
"finish_reason": "length",
"error": "",
"warnings": []
}
],
"generated_at": "2026-09-02T11:47:03.728906+00:00"
}
{
"environment": {
"captured_at": "2026-09-02T11:47:50.078568+00:00",
"chip": "Apple M1 Ultra",
"cores": "20",
"memory_bytes": "137438953472",
"macos": "26.6.2",
"macos_build": "25G83",
"python": "3.13.15",
"ollama": "ollama version is 0.33.2",
"mlx": "0.32.2",
"mlx_lm": "0.31.3",
"rapidmlx": "0.13.3",
"rapidmlx_served_model": "mlx-community/Qwen3-30B-A3B-Instruct-2507-4bit",
"timing_basis": "client-side for all engines; native counters recorded where exposed. rapidmlx exposes no native prefill or decode timing."
},
"config": {
"engines": [
"ollama",
"mlx",
"rapidmlx"
],
"models": [
"qwen3-30b-a3b-4bit"
],
"prompt_tokens": [
512,
4096,
16384
],
"gen_tokens": 256,
"reps": 3
},
"summary": [
{
"model_key": "qwen3-30b-a3b-4bit",
"label": "Qwen3-30B-A3B 4-bit (MoE)",
"engine": "mlx",
"prompt_target": 512,
"prompt_tokens": 518,
"gen_tokens": 256,
"reps": 3,
"client_ttft_s": {
"median": 0.979,
"min": 0.813,
"max": 1.127
},
"client_decode_tps": {
"median": 68.062,
"min": 67.883,
"max": 68.115
},
"client_total_s": {
"median": 4.73,
"min": 4.568,
"max": 4.892
},
"native_prefill_tps": {
"median": 594.376,
"min": 500.315,
"max": 727.332
},
"native_decode_tps": {
"median": 68.329,
"min": 68.149,
"max": 68.382
},
"has_native": true,
"peak_memory_gb": 17.78
},
{
"model_key": "qwen3-30b-a3b-4bit",
"label": "Qwen3-30B-A3B 4-bit (MoE)",
"engine": "mlx",
"prompt_target": 4096,
"prompt_tokens": 3936,
"gen_tokens": 256,
"reps": 3,
"client_ttft_s": {
"median": 3.614,
"min": 3.503,
"max": 4.147
},
"client_decode_tps": {
"median": 60.011,
"min": 59.493,
"max": 60.064
},
"client_total_s": {
"median": 7.868,
"min": 7.76,
"max": 8.441
},
"native_prefill_tps": {
"median": 1123.554,
"min": 1029.583,
"max": 1159.415
},
"native_decode_tps": {
"median": 60.246,
"min": 59.726,
"max": 60.299
},
"has_native": true,
"peak_memory_gb": 18.32
},
{
"model_key": "qwen3-30b-a3b-4bit",
"label": "Qwen3-30B-A3B 4-bit (MoE)",
"engine": "mlx",
"prompt_target": 16384,
"prompt_tokens": 16505,
"gen_tokens": 256,
"reps": 3,
"client_ttft_s": {
"median": 20.874,
"min": 20.719,
"max": 25.683
},
"client_decode_tps": {
"median": 45.615,
"min": 45.561,
"max": 45.893
},
"client_total_s": {
"median": 26.479,
"min": 26.317,
"max": 31.246
},
"native_prefill_tps": {
"median": 794.849,
"min": 645.448,
"max": 801.236
},
"native_decode_tps": {
"median": 45.794,
"min": 45.739,
"max": 46.073
},
"has_native": true,
"peak_memory_gb": 19.46
},
{
"model_key": "qwen3-30b-a3b-4bit",
"label": "Qwen3-30B-A3B 4-bit (MoE)",
"engine": "ollama",
"prompt_target": 512,
"prompt_tokens": 518,
"gen_tokens": 256,
"reps": 3,
"client_ttft_s": {
"median": 0.442,
"min": 0.408,
"max": 0.464
},
"client_decode_tps": {
"median": 91.227,
"min": 90.833,
"max": 91.481
},
"client_total_s": {
"median": 3.238,
"min": 3.195,
"max": 3.272
},
"native_prefill_tps": {
"median": 1245.848,
"min": 1182.917,
"max": 1344.516
},
"native_decode_tps": {
"median": 91.574,
"min": 91.188,
"max": 91.828
},
"has_native": true,
"peak_memory_gb": 18.66
},
{
"model_key": "qwen3-30b-a3b-4bit",
"label": "Qwen3-30B-A3B 4-bit (MoE)",
"engine": "ollama",
"prompt_target": 4096,
"prompt_tokens": 3936,
"gen_tokens": 256,
"reps": 3,
"client_ttft_s": {
"median": 2.92,
"min": 2.901,
"max": 3.197
},
"client_decode_tps": {
"median": 80.497,
"min": 79.336,
"max": 80.616
},
"client_total_s": {
"median": 6.084,
"min": 6.069,
"max": 6.412
},
"native_prefill_tps": {
"median": 1398.09,
"min": 1346.939,
"max": 1403.303
},
"native_decode_tps": {
"median": 80.807,
"min": 79.639,
"max": 80.936
},
"has_native": true,
"peak_memory_gb": 19.03
},
{
"model_key": "qwen3-30b-a3b-4bit",
"label": "Qwen3-30B-A3B 4-bit (MoE)",
"engine": "ollama",
"prompt_target": 16384,
"prompt_tokens": 16505,
"gen_tokens": 256,
"reps": 3,
"client_ttft_s": {
"median": 20.433,
"min": 20.42,
"max": 20.511
},
"client_decode_tps": {
"median": 57.764,
"min": 57.359,
"max": 57.829
},
"client_total_s": {
"median": 24.88,
"min": 24.83,
"max": 24.927
},
"native_prefill_tps": {
"median": 818.875,
"min": 818.579,
"max": 818.909
},
"native_decode_tps": {
"median": 57.983,
"min": 57.582,
"max": 58.052
},
"has_native": true,
"peak_memory_gb": 20.26
},
{
"model_key": "qwen3-30b-a3b-4bit",
"label": "Qwen3-30B-A3B 4-bit (MoE)",
"engine": "rapidmlx",
"prompt_target": 512,
"prompt_tokens": 518,
"gen_tokens": 256,
"reps": 3,
"client_ttft_s": {
"median": 0.856,
"min": 0.73,
"max": 1.035
},
"client_decode_tps": {
"median": 72.502,
"min": 72.357,
"max": 72.545
},
"client_total_s": {
"median": 4.373,
"min": 4.245,
"max": 4.56
},
"native_prefill_tps": {
"median": 0.0,
"min": 0.0,
"max": 0.0
},
"native_decode_tps": {
"median": 0.0,
"min": 0.0,
"max": 0.0
},
"has_native": false,
"peak_memory_gb": 0.0
},
{
"model_key": "qwen3-30b-a3b-4bit",
"label": "Qwen3-30B-A3B 4-bit (MoE)",
"engine": "rapidmlx",
"prompt_target": 4096,
"prompt_tokens": 3936,
"gen_tokens": 256,
"reps": 3,
"client_ttft_s": {
"median": 3.655,
"min": 3.488,
"max": 3.992
},
"client_decode_tps": {
"median": 63.999,
"min": 63.842,
"max": 64.258
},
"client_total_s": {
"median": 7.639,
"min": 7.457,
"max": 7.987
},
"native_prefill_tps": {
"median": 0.0,
"min": 0.0,
"max": 0.0
},
"native_decode_tps": {
"median": 0.0,
"min": 0.0,
"max": 0.0
},
"has_native": false,
"peak_memory_gb": 0.0
},
{
"model_key": "qwen3-30b-a3b-4bit",
"label": "Qwen3-30B-A3B 4-bit (MoE)",
"engine": "rapidmlx",
"prompt_target": 16384,
"prompt_tokens": 16505,
"gen_tokens": 256,
"reps": 3,
"client_ttft_s": {
"median": 21.65,
"min": 20.807,
"max": 33.875
},
"client_decode_tps": {
"median": 45.005,
"min": 44.515,
"max": 45.008
},
"client_total_s": {
"median": 27.316,
"min": 26.473,
"max": 39.604
},
"native_prefill_tps": {
"median": 0.0,
"min": 0.0,
"max": 0.0
},
"native_decode_tps": {
"median": 0.0,
"min": 0.0,
"max": 0.0
},
"has_native": false,
"peak_memory_gb": 0.0
}
],
"runs": [
{
"model_key": "qwen3-30b-a3b-4bit",
"engine": "ollama",
"prompt_target": 512,
"rep": 1,
"prompt_tokens": 517,
"gen_tokens": 256,
"native_prefill_tps": 1344.515961250894,
"native_decode_tps": 91.82762806367099,
"has_native": true,
"client_ttft_s": 0.4076850831042975,
"client_decode_tps": 91.48132206895387,
"client_total_s": 3.1952640409581363,
"peak_memory_gb": 18.66,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3-30b-a3b-4bit",
"engine": "mlx",
"prompt_target": 512,
"rep": 1,
"prompt_tokens": 517,
"gen_tokens": 256,
"native_prefill_tps": 727.3316862985679,
"native_decode_tps": 68.32854955349819,
"has_native": true,
"client_ttft_s": 0.8134123331401497,
"client_decode_tps": 68.06197193423584,
"client_total_s": 4.568039542064071,
"peak_memory_gb": 17.78,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3-30b-a3b-4bit",
"engine": "rapidmlx",
"prompt_target": 512,
"rep": 1,
"prompt_tokens": 517,
"gen_tokens": 256,
"native_prefill_tps": 0.0,
"native_decode_tps": 0.0,
"has_native": false,
"client_ttft_s": 0.7297023751307279,
"client_decode_tps": 72.54506778045742,
"client_total_s": 4.245020375121385,
"peak_memory_gb": 0.0,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3-30b-a3b-4bit",
"engine": "mlx",
"prompt_target": 512,
"rep": 2,
"prompt_tokens": 518,
"gen_tokens": 256,
"native_prefill_tps": 500.31493039244606,
"native_decode_tps": 68.1487345174092,
"has_native": true,
"client_ttft_s": 1.1273849999997765,
"client_decode_tps": 67.88307216843381,
"client_total_s": 4.892019666032866,
"peak_memory_gb": 17.78,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3-30b-a3b-4bit",
"engine": "rapidmlx",
"prompt_target": 512,
"rep": 2,
"prompt_tokens": 518,
"gen_tokens": 256,
"native_prefill_tps": 0.0,
"native_decode_tps": 0.0,
"has_native": false,
"client_ttft_s": 0.8555167911108583,
"client_decode_tps": 72.50173933525001,
"client_total_s": 4.372910999925807,
"peak_memory_gb": 0.0,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3-30b-a3b-4bit",
"engine": "ollama",
"prompt_target": 512,
"rep": 2,
"prompt_tokens": 518,
"gen_tokens": 256,
"native_prefill_tps": 1245.848174880526,
"native_decode_tps": 91.57361749771513,
"has_native": true,
"client_ttft_s": 0.44237529207021,
"client_decode_tps": 91.22651639135232,
"client_total_s": 3.2378364999312907,
"peak_memory_gb": 18.66,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3-30b-a3b-4bit",
"engine": "rapidmlx",
"prompt_target": 512,
"rep": 3,
"prompt_tokens": 519,
"gen_tokens": 256,
"native_prefill_tps": 0.0,
"native_decode_tps": 0.0,
"has_native": false,
"client_ttft_s": 1.0351598330307752,
"client_decode_tps": 72.35689997508364,
"client_total_s": 4.559540041955188,
"peak_memory_gb": 0.0,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3-30b-a3b-4bit",
"engine": "ollama",
"prompt_target": 512,
"rep": 3,
"prompt_tokens": 519,
"gen_tokens": 256,
"native_prefill_tps": 1182.9167673323516,
"native_decode_tps": 91.18819283880599,
"has_native": true,
"client_ttft_s": 0.4640209579374641,
"client_decode_tps": 90.83254844033874,
"client_total_s": 3.271545791067183,
"peak_memory_gb": 18.66,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3-30b-a3b-4bit",
"engine": "mlx",
"prompt_target": 512,
"rep": 3,
"prompt_tokens": 519,
"gen_tokens": 256,
"native_prefill_tps": 594.375847301507,
"native_decode_tps": 68.38201193023798,
"has_native": true,
"client_ttft_s": 0.9790196251124144,
"client_decode_tps": 68.11539656285134,
"client_total_s": 4.730285500176251,
"peak_memory_gb": 17.78,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3-30b-a3b-4bit",
"engine": "ollama",
"prompt_target": 4096,
"rep": 1,
"prompt_tokens": 3936,
"gen_tokens": 256,
"native_prefill_tps": 1398.08970365187,
"native_decode_tps": 80.80680544814633,
"has_native": true,
"client_ttft_s": 2.9007314580958337,
"client_decode_tps": 80.49652888180854,
"client_total_s": 6.06902145803906,
"peak_memory_gb": 19.03,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3-30b-a3b-4bit",
"engine": "mlx",
"prompt_target": 4096,
"rep": 1,
"prompt_tokens": 3936,
"gen_tokens": 256,
"native_prefill_tps": 1159.4150959415965,
"native_decode_tps": 60.24571248477783,
"has_native": true,
"client_ttft_s": 3.502859665779397,
"client_decode_tps": 60.010695429791745,
"client_total_s": 7.7601247078273445,
"peak_memory_gb": 18.3,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3-30b-a3b-4bit",
"engine": "rapidmlx",
"prompt_target": 4096,
"rep": 1,
"prompt_tokens": 3936,
"gen_tokens": 256,
"native_prefill_tps": 0.0,
"native_decode_tps": 0.0,
"has_native": false,
"client_ttft_s": 3.4879540409892797,
"client_decode_tps": 64.25837746759396,
"client_total_s": 7.456518291030079,
"peak_memory_gb": 0.0,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3-30b-a3b-4bit",
"engine": "mlx",
"prompt_target": 4096,
"rep": 2,
"prompt_tokens": 4177,
"gen_tokens": 256,
"native_prefill_tps": 1029.582704274771,
"native_decode_tps": 59.72580025570118,
"has_native": true,
"client_ttft_s": 4.146518917055801,
"client_decode_tps": 59.492816739208614,
"client_total_s": 8.440708250040188,
"peak_memory_gb": 18.32,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3-30b-a3b-4bit",
"engine": "rapidmlx",
"prompt_target": 4096,
"rep": 2,
"prompt_tokens": 4177,
"gen_tokens": 256,
"native_prefill_tps": 0.0,
"native_decode_tps": 0.0,
"has_native": false,
"client_ttft_s": 3.992349667008966,
"client_decode_tps": 63.84205225954886,
"client_total_s": 7.986790374852717,
"peak_memory_gb": 0.0,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3-30b-a3b-4bit",
"engine": "ollama",
"prompt_target": 4096,
"rep": 2,
"prompt_tokens": 4177,
"gen_tokens": 256,
"native_prefill_tps": 1346.9388018339264,
"native_decode_tps": 79.63911039380608,
"has_native": true,
"client_ttft_s": 3.1974479160271585,
"client_decode_tps": 79.33624188799058,
"client_total_s": 6.411813666112721,
"peak_memory_gb": 19.03,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3-30b-a3b-4bit",
"engine": "rapidmlx",
"prompt_target": 4096,
"rep": 3,
"prompt_tokens": 3932,
"gen_tokens": 256,
"native_prefill_tps": 0.0,
"native_decode_tps": 0.0,
"has_native": false,
"client_ttft_s": 3.654887333046645,
"client_decode_tps": 63.999023527196634,
"client_total_s": 7.639499332988635,
"peak_memory_gb": 0.0,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3-30b-a3b-4bit",
"engine": "ollama",
"prompt_target": 4096,
"rep": 3,
"prompt_tokens": 3932,
"gen_tokens": 256,
"native_prefill_tps": 1403.3034019043814,
"native_decode_tps": 80.93574365879515,
"has_native": true,
"client_ttft_s": 2.920405540848151,
"client_decode_tps": 80.61614549194059,
"client_total_s": 6.083747165976092,
"peak_memory_gb": 19.03,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3-30b-a3b-4bit",
"engine": "mlx",
"prompt_target": 4096,
"rep": 3,
"prompt_tokens": 3932,
"gen_tokens": 256,
"native_prefill_tps": 1123.5544229640936,
"native_decode_tps": 60.29940242647974,
"has_native": true,
"client_ttft_s": 3.6142416249495,
"client_decode_tps": 60.06409368589881,
"client_total_s": 7.86782025010325,
"peak_memory_gb": 18.32,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3-30b-a3b-4bit",
"engine": "ollama",
"prompt_target": 16384,
"rep": 1,
"prompt_tokens": 16501,
"gen_tokens": 256,
"native_prefill_tps": 818.8748646457068,
"native_decode_tps": 57.98300237345266,
"has_native": true,
"client_ttft_s": 20.511121707968414,
"client_decode_tps": 57.76442991387218,
"client_total_s": 24.92677566711791,
"peak_memory_gb": 20.26,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3-30b-a3b-4bit",
"engine": "mlx",
"prompt_target": 16384,
"rep": 1,
"prompt_tokens": 16501,
"gen_tokens": 256,
"native_prefill_tps": 794.8487337722258,
"native_decode_tps": 45.739378386469205,
"has_native": true,
"client_ttft_s": 20.874159917002544,
"client_decode_tps": 45.56083376339736,
"client_total_s": 26.47862754203379,
"peak_memory_gb": 19.46,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3-30b-a3b-4bit",
"engine": "rapidmlx",
"prompt_target": 16384,
"rep": 1,
"prompt_tokens": 16501,
"gen_tokens": 256,
"native_prefill_tps": 0.0,
"native_decode_tps": 0.0,
"has_native": false,
"client_ttft_s": 33.875450749881566,
"client_decode_tps": 44.51500055348899,
"client_total_s": 39.604070958914235,
"peak_memory_gb": 0.0,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3-30b-a3b-4bit",
"engine": "mlx",
"prompt_target": 16384,
"rep": 2,
"prompt_tokens": 16508,
"gen_tokens": 256,
"native_prefill_tps": 645.4482410696663,
"native_decode_tps": 46.072864922022504,
"has_native": true,
"client_ttft_s": 25.68261033319868,
"client_decode_tps": 45.89310306463817,
"client_total_s": 31.24646787508391,
"peak_memory_gb": 19.46,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3-30b-a3b-4bit",
"engine": "rapidmlx",
"prompt_target": 16384,
"rep": 2,
"prompt_tokens": 16508,
"gen_tokens": 256,
"native_prefill_tps": 0.0,
"native_decode_tps": 0.0,
"has_native": false,
"client_ttft_s": 20.806730249896646,
"client_decode_tps": 45.00841889332821,
"client_total_s": 26.472568708006293,
"peak_memory_gb": 0.0,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3-30b-a3b-4bit",
"engine": "ollama",
"prompt_target": 16384,
"rep": 2,
"prompt_tokens": 16508,
"gen_tokens": 256,
"native_prefill_tps": 818.9087965875295,
"native_decode_tps": 58.052203443946965,
"has_native": true,
"client_ttft_s": 20.419862957904115,
"client_decode_tps": 57.82912033294015,
"client_total_s": 24.830403625033796,
"peak_memory_gb": 20.26,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3-30b-a3b-4bit",
"engine": "rapidmlx",
"prompt_target": 16384,
"rep": 3,
"prompt_tokens": 16505,
"gen_tokens": 256,
"native_prefill_tps": 0.0,
"native_decode_tps": 0.0,
"has_native": false,
"client_ttft_s": 21.649653791915625,
"client_decode_tps": 45.005441362327765,
"client_total_s": 27.31586154201068,
"peak_memory_gb": 0.0,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3-30b-a3b-4bit",
"engine": "ollama",
"prompt_target": 16384,
"rep": 3,
"prompt_tokens": 16505,
"gen_tokens": 256,
"native_prefill_tps": 818.5787063302147,
"native_decode_tps": 57.58198403674451,
"has_native": true,
"client_ttft_s": 20.433122667018324,
"client_decode_tps": 57.35910472118408,
"client_total_s": 24.879754417110234,
"peak_memory_gb": 20.26,
"finish_reason": "length",
"error": "",
"warnings": []
},
{
"model_key": "qwen3-30b-a3b-4bit",
"engine": "mlx",
"prompt_target": 16384,
"rep": 3,
"prompt_tokens": 16505,
"gen_tokens": 256,
"native_prefill_tps": 801.2364025059729,
"native_decode_tps": 45.794000673551196,
"has_native": true,
"client_ttft_s": 20.718825374962762,
"client_decode_tps": 45.61529329243587,
"client_total_s": 26.31676191580482,
"peak_memory_gb": 19.46,
"finish_reason": "length",
"error": "",
"warnings": []
}
],
"generated_at": "2026-09-02T11:55:25.134362+00:00"
}
#!/bin/zsh
# Phase two: restart Rapid-MLX on the MoE checkpoint and run the three-way matrix.
#
# The server holds one model per process, so it has to be restarted between
# cells. bench-3way.py refuses to run if the served repo does not match the
# cell, which is the guard against the defect the article's own MoE re-run
# exists to avoid: the first attempt paired Ollama's qwen3:30b-a3b with an MLX
# build of the 2507 Instruct refresh, a different checkpoint, and was discarded.
#
# Spec decode is left at its default. `rapid-mlx info` reports it as supported
# for this checkpoint and disabled for the dense hybrid one, so the Prometheus
# counters are read before and after to record whether it actually fired rather
# than assuming either way.
set -e
cd "$(dirname "$0")"
REPO="mlx-community/Qwen3-30B-A3B-Instruct-2507-4bit"
PORT=8080
echo "stopping any running rapid-mlx ..."
pkill -f "rapid-mlx" 2>/dev/null || true
sleep 5
echo "starting rapid-mlx on ${REPO} ..."
nohup .venv-rapidmlx/bin/rapid-mlx --no-telemetry --no-banner serve "${REPO}" \
--host 127.0.0.1 --port "${PORT}" --no-thinking --log-level WARNING \
> rapidmlx-moe.serve.log 2>&1 &
for i in $(seq 1 60); do
sleep 3
ID=$(curl -s --max-time 5 "http://127.0.0.1:${PORT}/v1/models" 2>/dev/null \
| python3 -c "import json,sys; print(json.load(sys.stdin)['data'][0]['id'])" 2>/dev/null || true)
if [[ -n "${ID}" ]]; then
echo "ready: ${ID}"
break
fi
done
if [[ "${ID}" != "${REPO}" ]]; then
echo "server is serving '${ID}', expected '${REPO}'" >&2
exit 1
fi
echo "spec-decode counters before the run:"
curl -s --max-time 10 "http://127.0.0.1:${PORT}/metrics" \
| grep -E "^rapid_mlx_spec_decode_(attempts|accepts)_total" || echo " none reported"
.venv/bin/python bench-3way.py --models qwen3-30b-a3b-4bit --output results-3way-moe.json
echo "spec-decode counters after the run:"
curl -s --max-time 10 "http://127.0.0.1:${PORT}/metrics" \
| grep -E "^rapid_mlx_spec_decode_(attempts|accepts)_total" || echo " none reported"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment