Transcript
[00:00] This report from large IT data caught my eye the other day and in it they summarize or they declare
OMLX the best long context inference engine on Apple Silicon and therefore name it the best for
enterprise production use cases. They ran the benchmarks for OMLX, DFlash-MLX, Rapid MLX,
MLX-VLM and MTPLX and they ran all these on a M5 with 64 gigabytes of RAM and I wanted to see if I
got the same or similar results on my M3 Pro with 36 gigabytes of RAM.
[00:39] So if you want to follow along
I’m going to use two projects to get this done. The first one is the exact same benchmark lab that
they use for this report. Now I’m not going to include D-Flash MLX and I’m not going to include
MTP LX. Both of those require a really narrow set of models and I don’t think it’s a super fair
comparison. I am going to include the LLM MLX which Rapid MLX is a fork of. I took all of their charts
and data and log and I threw it into this backup folder and then on the plot results which we’ll
need later. I just updated this list of frameworks for the frameworks that I’m going to be working
with which again are Rapid-MLX, OMLX, MLX-VLM and VLLM-MLX. All of that’s really hard to say.
[01:32] Next I’m going to use my own library this insurance lab. I will note that when you do set it up you want
to go through the list of engines that I support here and you kind of want to turn off any that you
already have installed in your system as a safety precaution. I’m going to say false on OMLX because
I already have that and run that on my machine daily. So the way that works is I just run setup sh in my
inference lab directory and what that’s going to do is it’s going to pull down all the repositories of the
ones that I said yes to or that were true and it’s going to create a Python virtual
environments for each and every one so I can run them in isolation if they have dependencies on each
other like VLLM LMLX and Rapid MLX. I don’t want those to pull each other into their own virtual
environment. So that’s all set up and now I can start up one of these engines and then I can run the
benchmark against it. So let’s do that now.
[02:30] So from the inference lab repository I can run engine sh pass
it the engine in this case that’s Rapid MLX and tell it to use the model. Once that’s up and running we
can see that we’re getting some logs. I can switch over to the benchmark repository and run the inline
bench pass it the URL and the model that we’re going to be running and let that guy get started.
And there’s not a lot to see in this interface because it’s just running but if we switch back
over to the engine we can see all of these calls are happening and we can see that in the logs.
Now each of these runs takes about 15 minutes so obviously I’m not going to have you sit through
all of them but I wanted you to get an idea of what it’s like to run each of these benchmarks.
And when that finishes up we get a line delimited JSON for our engine and we also get a log for the same
engine. So MLX VLM we’ll run the engine we’ll run our benchmark and we get our report.
[03:31] So now that we’ve got all of our line delimited JSON and all of our logs for each of our inference
engines we are going to run the chart generation tool this plot results pi.
And I’m going to run that in this virtual environment cool so it generated all those let’s take a quick
look if I go into charts we can see we have each of our charts and before we dig into them I’m going
to have open code create an HTML report for us.
So in here I’m going to say create an HTML report about all of our line delimited JSON
you can use the chart images you can use the logs but I want to be very clear about not being swayed
by the data in the readme there’s a lot of terminology and phrasing in the readme that might be useful
So I’m saying it’s okay to use the same terms or phrases if they apply but do not start with any
assumptions based on the readme.
So we’re going to run that guy.
[04:30] So here is our report based on all the data that we generated from the benchmarks and I’m going to try to do
and I’m going to try to do this at the same time as so this is the first time I’m looking at it and the first time you’re looking at it so we got an overview.
I’m going to skip past so the headline result OMLX separates from the field as context grows.
So that’s in line with the report that we previously read.
OMLX streams in larger chunks that’s fine.
So we get to decode throughput and they all degrade as the context gets bigger with rapid MLX taking a steep
dive at 8k but then rejoining.
So and OMLX does have a little bit of a lead the entire time.
So pre-fill massive jump but honestly, I don’t find that completely surprising.
I have actually seen that.
I don’t know if I’ve seen these numbers on my machine to be honest.
I will look into exactly how this is measured, but this is where the benchmark separate OMLX is pre-fill throughput scales with context.
Again, very much in line with the original report.
So time to first token.
Oh, OMLX is winning big time there.
So again, as the context grows, the time to first tokens stays lower in OMLX.
So the key findings OMLX wins the moment context grows past 2k, which jives with everything that we just went through.
Decode is crowded in the middle.
That makes sense.
Stability favors OMLX at scale.
Again, I think the report that I initially started with and the results that I have here completely jive.
[06:24] I’m sold. OMLX is a great, great inference engine. I use it every single day. I hope you guys are too. If you’re on Apple and you’re running an M series processor, you need to be using OMLX.
About the benchmark process.
This report and process was inspired by the reporting originally done at https://www.largitdata.com/blog/mlx-inference-benchmark-apple-m5-max/
I used the exact same benchmarking code made available at https://github.com/ywchiu/mlx_benchmark_lab
Where this data differs from the original is that I ran the benchmark on an Apple M3 Pro Macbook Pro with 36GB of RAM, instead of a M5 Mac with 64GB of RAM. I also did not test dflash-mlx or mtplx, as the range of models available for those engines is narrow and specialized and does not seem like a fair comparison entrant for this test. I did include vllm-mlx. Rapid-MLX is a fork of vllm-mlx.
Each engine was run locally using https://github.com/joemaddalone/inference-lab
Contents
- Overview
- Setup & method
- Decode throughput
- Prefill throughput
- Time to first token
- Stability
- End-to-end latency
- Key findings
1. Overview
Four MLX-based inference engines were benchmarked serving the same Qwen3.6-35B-A3B-4bit model over the OpenAI-compatible chat API, streaming. Prompt context was swept from 64 to 32,768 tokens (7 sizes × 5 runs each), and three primary metrics were recorded per run:
- Prefill tps — prompt tokens processed per second before the first output token.
- Decode tps — output tokens generated per second (streaming).
- TTFT — time to first token, in milliseconds.
| KPI | Value |
|---|---|
| ~10× | omlx TTFT advantage at 32k context (vs. all other engines, log-scale gap) |
| ~9.5× | omlx prefill advantage at 32k context (5,354 vs ~561 tps) |
| 5.0× | omlx end-to-end win at 32k context (10.6 s vs ~53 s total) |
| 58.4 | Peak decode tps (omlx, 64 tok) (omlx leads at every context size) |
The headline result: omlx separates from the field as context grows. Below ~2,000 tokens the four engines are within noise of each other. Beyond that, omlx’s prefill throughput scales to roughly 5,400 tps while the other three plateau around 550–680 tps, producing a TTFT and end-to-end latency gap of roughly one order of magnitude at 32k context.
2. Setup & method
Test configuration (from run logs)
| Engine | Endpoint | Model id in log | Data source |
|---|---|---|---|
| omlx | http://127.0.0.1:8000 | Qwen3.6-35B-A3B-4bit | data/omlx.jsonl · logs/omlx.log |
| rapid-mlx | http://127.0.0.1:8000 | …/models/Qwen3.6-35B-A3B-4bit | data/rapid-mlx.jsonl · logs/rapid-mlx.log |
| mlx-vlm | http://127.0.0.1:8080 | …/models/Qwen3.6-35B-A3B-4bit | data/mlx-vlm.jsonl · logs/mlx-vlm.log |
| vllm-mlx | http://127.0.0.1:8000 | …/models/Qwen3.6-35B-A3B-4bit | data/vllm-mlx.jsonl · logs/vllm-mlx.log |
- Same prompt template for every run:
/no_thinkprefix + a fixed text to summarize (the/no_thinkprefix is the Qwen3 convention to disable thinking; no reasoning tokens were observed). - Context sizes: 64, 512, 2,048, 4,096, 8,192, 16,384, 32,768 prompt tokens; 5 timed runs each after a same-size warm-up.
max_tokens=256,temperature=0, streaming withstream_options.include_usage.- A best-effort prefix-cache clear was attempted before each run (
/v1/cache/clear).
Data notes & caveats
vllm-mlx reports
prompt_tokens=0on every run. The harness computesprefill_tps = prompt_tokens / prefill_time, so vllm-mlx shows a flat 0 and its prefill throughput cannot be compared directly. Its TTFT is still measured, however, and tracks rapid-mlx / mlx-vlm almost exactly at every context size — strong evidence that its real prefill cost is comparable to those two, not to omlx.
omlx streams in larger chunks. For a 256-token completion, omlx emitted ~37–43 content deltas while the other three emitted ~256 (one token each). Completion token counts are identical (~256), so decode-tps figures remain directly comparable, but this is an implementation difference worth knowing when parsing the raw JSONL.
All figures below are medians of 5 runs per context size, recomputed from the JSONL in data/. Best value per row is highlighted in green.
3. Decode throughput
Legend: rapid-mlx · omlx · mlx-vlm · vllm-mlx

Decode tps (median) vs prompt context length, log scale on x.
Median decode tps by context size
| Context | rapid-mlx | omlx | mlx-vlm | vllm-mlx |
|---|---|---|---|---|
| 64 | 56.7 | 58.4 | 55.6 | 57.0 |
| 512 | 55.8 | 58.5 | 54.5 | 54.8 |
| 2,048 | 55.2 | 56.9 | 53.8 | 54.6 |
| 4,096 | 54.3 | 56.1 | 53.7 | 54.8 |
| 8,192 | 45.6 | 53.9 | 51.8 | 52.7 |
| 16,384 | 49.1 | 50.4 | 48.4 | 48.5 |
| 32,768 | 44.0 | 45.4 | 42.9 | 42.5 |
omlx posts the highest median decode speed at every context size. The other three engines swap the 2nd/3rd/4th places depending on context.
Decay with context length

Decode tps as a percentage of each engine’s own baseline at its smallest tested context.
Every engine loses speed as the context grows, and the shape of the decay is remarkably similar across engines:
- % of own baseline retained at 32,768 tokens: omlx 77.7%, rapid-mlx 77.6%, mlx-vlm 77.2%, vllm-mlx 74.6%.
- The most expensive step is between 4,096 and 8,192 tokens, where all four take a visible step down — decode falls furthest for rapid-mlx (54.3 → 45.6) which then partially recovers at 16,384 (49.1).
- vllm-mlx degrades the most overall; omlx retains the most.
4. Prefill throughput

Prefill tps (median) vs prompt context length, log scale on x. vllm-mlx reports 0 (prompt tokens unmeasured).
This is where the benchmark separates. omlx’s prefill throughput scales with context: roughly 137 tps at 64 tokens rising to ~5,400 tps at 8k–32k tokens. rapid-mlx and mlx-vlm stay roughly flat at ~560–680 tps regardless of context length.
Median prefill tps by context size
| Context | rapid-mlx | omlx | mlx-vlm | vllm-mlx |
|---|---|---|---|---|
| 64 | 131 | 137 | 233 | 0* |
| 512 | 374 | 430 | 426 | 0* |
| 2,048 | 612 | 671 | 608 | 0* |
| 4,096 | 658 | 1,433 | 660 | 0* |
| 8,192 | 627 | 5,314 | 675 | 0* |
| 16,384 | 629 | 5,435 | 636 | 0* |
| 32,768 | 561 | 5,354 | 553 | 0* |
* vllm-mlx reported prompt_tokens=0; prefill tps is therefore 0 / not meaningful (see Section 2).
| Context | omlx | rapid-mlx | omlx / rapid |
|---|---|---|---|
| 4,096 | 1,433 | 658 | 2.2× |
| 8,192 | 5,314 | 627 | 8.5× |
| 16,384 | 5,435 | 629 | 8.6× |
| 32,768 | 5,354 | 561 | 9.5× |
Interpretation: at small contexts all engines prefill at a few hundred tps. omlx is the only engine whose prefill scales up with batch/compute parallelism as the prompt grows, reaching its ceiling around 8k–16k tokens. The practical consequence is that omlx’s time-to-first-token stops growing with context in the mid range — see the next section.
5. Time to first token

TTFT (ms, log-y) vs prompt context length.
Median TTFT (ms) by context size
| Context | rapid-mlx | omlx | mlx-vlm | vllm-mlx |
|---|---|---|---|---|
| 64 | 604 | 575 | 339 | 686 |
| 512 | 1,175 | 1,021 | 1,030 | 1,261 |
| 2,048 | 2,744 | 2,501 | 2,763 | 2,757 |
| 4,096 | 5,069 | 2,327 | 5,051 | 5,080 |
| 8,192 | 10,586 | 1,249 | 9,839 | 10,022 |
| 16,384 | 21,058 | 2,438 | 20,830 | 21,052 |
| 32,768 | 47,193 | 4,945 | 47,840 | 47,668 |
At 32,768 tokens, omlx reaches the first token in ~4.9 s versus ~47–48 s for every other engine — a 9.6–9.7× gap. At 8,192 tokens the gap is ~8×. Only at the smallest context (64 tokens) does omlx trail anyone: mlx-vlm’s 339 ms is the fastest TTFT recorded in the whole benchmark.
Notably, omlx’s TTFT is non-monotonic: 2,501 ms at 2,048 tokens, 2,327 ms at 4,096, then 1,249 ms at 8,192 — it drops even though the prompt quadruples. This is the direct consequence of the prefill scaling shown in Section 4; once parallelism kicks in, longer prompts cost less wall time per token. rapid-mlx, mlx-vlm and vllm-mlx instead show the expected linear-ish growth in TTFT, which is why their curves climb steeply on the log-y chart.
6. Stability (run-to-run variance)

Decode tps stddev per context — lower is more consistent across the 5 runs.

Per-context distribution of decode tps across the 5 runs (boxplots).
Decode tps stddev by context size
| Context | rapid-mlx | omlx | mlx-vlm | vllm-mlx |
|---|---|---|---|---|
| 64 | 0.4 | 4.4 | 10.7 | 0.3 |
| 512 | 0.6 | 0.1 | 9.1 | 3.2 |
| 2,048 | 0.4 | 0.0 | 0.4 | 0.6 |
| 4,096 | 3.2 | 0.1 | 1.0 | 0.5 |
| 8,192 | 5.6 | 0.1 | 0.8 | 0.5 |
| 16,384 | 1.0 | 0.2 | 0.7 | 0.8 |
| 32,768 | 0.3 | 0.1 | 0.5 | 0.9 |
Median decode tps used elsewhere; stddev computed over the 5 runs per cell.
- omlx is the most consistent engine at every context ≥ 2,048, with stddev ≤ 0.2 tps — essentially zero spread across the 5 runs at large contexts.
- rapid-mlx is the most volatile at 8,192 (stddev 5.6, range 36.9–52.4): one run dropped to 36.9 tps while the other four sat near 52 — a ~30% swing. It also shows a 48.0 tps outlier at 4,096.
- mlx-vlm has cold-start outliers at small contexts — stddev 10.7 at 64 (a 34.1 tps run vs ~56 median) and 9.1 at 512 (a 36.5 run). Its large-context behavior is stable.
- vllm-mlx is generally stable (≤ 0.9 everywhere except a 48.9 tps outlier at 512, stddev 3.2).
Practical takeaway: small-context medians for mlx-vlm and 8,192-token medians for rapid-mlx are inflated/depressed by single outlier runs; the 5-run medians in Sections 3–5 should be read with those outliers in mind.
7. End-to-end latency
Median total request time (seconds) — TTFT + decode
| Context | rapid-mlx | omlx | mlx-vlm | vllm-mlx |
|---|---|---|---|---|
| 64 | 5.1 | 5.0 | 5.0 | 5.2 |
| 512 | 5.7 | 5.4 | 5.7 | 5.9 |
| 2,048 | 7.4 | 6.8 | 7.5 | 7.5 |
| 4,096 | 9.8 | 6.9 | 9.8 | 9.8 |
| 8,192 | 16.3 | 6.0 | 14.9 | 14.9 |
| 16,384 | 26.3 | 7.5 | 26.1 | 26.3 |
| 32,768 | 53.0 | 10.6 | 53.8 | 53.7 |
At 32,768 tokens, a full request takes 10.6 s on omlx versus 53.0–53.8 s on the other three — a ~5× end-to-end difference. Because all engines decode at comparable rates (~43–45 tps at that size), the entire advantage traces back to prefill/TTFT. The three non-omlx engines are essentially indistinguishable end-to-end at large contexts (within ~0.8 s of each other at 32k).
8. Key findings
1. omlx wins the moment context grows past ~2k tokens
Prefill throughput scales to ~5,400 tps (vs ~560–680 tps plateau for rapid-mlx / mlx-vlm), making its TTFT up to ~10× lower and total latency ~5× lower at 32,768 tokens — while also delivering the highest decode tps at every size.
2. Decode is the crowded middle — everyone lands in a 44–58 tps band
Decode differences are small in absolute terms (omlx leads by 1–8 tps), and all four engines degrade by a comparable ~22–25% by 32k context. Decode throughput is not the differentiator between these engines.
3. The prefill gap is the story, and it compounds
At 32,768 tokens omlx’s advantage is 9.5× on prefill tps, 9.6–9.7× on TTFT, and ~5× on total time — because prefill time is the dominant term in a request once the prompt is large.
4. Small-context behavior flips the leaderboard
At 64 tokens, mlx-vlm has the fastest TTFT (339 ms) and fastest prefill (233 tps); omlx’s prefill starts slow (137 tps) and only wins from 512 tokens up. For short-prompt, latency-sensitive workloads mlx-vlm is competitive at the very smallest sizes.
5. Stability favors omlx at scale; watch the outliers elsewhere
omlx is near-deterministic at large contexts (stddev ≤ 0.2 tps). rapid-mlx showed a ~30% decode drop in one 8,192-token run, and mlx-vlm had two cold-start outliers at 64/512. For capacity planning, rely on medians over multiple runs.
6. Data caveats to carry forward
vllm-mlx’s prefill tps is not measurable in this harness (prompt_tokens=0 reported); its TTFT aligns with rapid-mlx / mlx-vlm, implying similar real prefill cost. omlx also streams ~37–43 (not ~256) content deltas for a 256-token completion — larger SSE chunks, identical token counts. Neither affects the decode comparisons.