mlxturbo 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. mlxturbo-0.1.0/.gitignore +10 -0
  2. mlxturbo-0.1.0/LICENSE +21 -0
  3. mlxturbo-0.1.0/PKG-INFO +133 -0
  4. mlxturbo-0.1.0/README.en.md +107 -0
  5. mlxturbo-0.1.0/README.md +111 -0
  6. mlxturbo-0.1.0/mlxturbo/__init__.py +10 -0
  7. mlxturbo-0.1.0/mlxturbo/_arch_registry.py +99 -0
  8. mlxturbo-0.1.0/mlxturbo/_mlx_compat.py +183 -0
  9. mlxturbo-0.1.0/mlxturbo/_vendor/__init__.py +7 -0
  10. mlxturbo-0.1.0/mlxturbo/_vendor/qwen4_exp.py +900 -0
  11. mlxturbo-0.1.0/mlxturbo/batch.py +993 -0
  12. mlxturbo-0.1.0/mlxturbo/cli.py +207 -0
  13. mlxturbo-0.1.0/mlxturbo/convert.py +423 -0
  14. mlxturbo-0.1.0/mlxturbo/convert_flash.py +505 -0
  15. mlxturbo-0.1.0/mlxturbo/fast_qmm.py +459 -0
  16. mlxturbo-0.1.0/mlxturbo/fused.py +309 -0
  17. mlxturbo-0.1.0/mlxturbo/kernels/__init__.py +0 -0
  18. mlxturbo-0.1.0/mlxturbo/kernels/_qmm_e120_source.py +433 -0
  19. mlxturbo-0.1.0/mlxturbo/kernels/_qmm_skinny_mma_source.py +220 -0
  20. mlxturbo-0.1.0/mlxturbo/kernels/dispatch.py +233 -0
  21. mlxturbo-0.1.0/mlxturbo/kernels/gated_delta_states.py +327 -0
  22. mlxturbo-0.1.0/mlxturbo/kernels/hyper_connection.py +481 -0
  23. mlxturbo-0.1.0/mlxturbo/kernels/moe_route.py +172 -0
  24. mlxturbo-0.1.0/mlxturbo/kernels/qmm_direct.py +231 -0
  25. mlxturbo-0.1.0/mlxturbo/kernels/qmm_skinny_mma.py +257 -0
  26. mlxturbo-0.1.0/mlxturbo/kernels/qmv_wide_nocap.py +279 -0
  27. mlxturbo-0.1.0/mlxturbo/kernels/rms_norm_gated.py +180 -0
  28. mlxturbo-0.1.0/mlxturbo/lookup_spec.py +269 -0
  29. mlxturbo-0.1.0/mlxturbo/mtp.py +153 -0
  30. mlxturbo-0.1.0/mlxturbo/mtp_flash.py +205 -0
  31. mlxturbo-0.1.0/mlxturbo/ngram_stream.py +434 -0
  32. mlxturbo-0.1.0/mlxturbo/rebit.py +139 -0
  33. mlxturbo-0.1.0/mlxturbo/runner.py +1372 -0
  34. mlxturbo-0.1.0/mlxturbo/sam.py +184 -0
  35. mlxturbo-0.1.0/mlxturbo/server.py +6129 -0
  36. mlxturbo-0.1.0/mlxturbo/spec.py +1137 -0
  37. mlxturbo-0.1.0/mlxturbo/spec_flash.py +493 -0
  38. mlxturbo-0.1.0/mlxturbo/train_mtp.py +263 -0
  39. mlxturbo-0.1.0/pyproject.toml +71 -0
@@ -0,0 +1,10 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .DS_Store
5
+ tools/isa/applegpu/
6
+ data/
7
+ models/
8
+ tools/bridge/*.dylib
9
+ tools/isa/build/
10
+ bench/results/
mlxturbo-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 akakeishin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.5
2
+ Name: mlxturbo
3
+ Version: 0.1.0
4
+ Summary: A fast MLX inference engine and OpenAI/Anthropic-compatible server for Apple Silicon
5
+ Project-URL: Homepage, https://github.com/akakeishin/mlxturbo
6
+ Project-URL: Repository, https://github.com/akakeishin/mlxturbo
7
+ Author-email: akakeishin <mizururi_22@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Environment :: GPU
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: MacOS :: MacOS X
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Requires-Python: >=3.13
20
+ Requires-Dist: fastapi>=0.141.1
21
+ Requires-Dist: huggingface-hub>=1.28.0
22
+ Requires-Dist: mlx-lm<0.32,>=0.31.3
23
+ Requires-Dist: mlx<0.33,>=0.32.2
24
+ Requires-Dist: uvicorn>=0.52.4
25
+ Description-Content-Type: text/markdown
26
+
27
+ # mlxturbo
28
+
29
+ English | [日本語](README.md)
30
+
31
+ A local inference engine for Apple Silicon (MLX), plus an HTTP server compatible with the OpenAI, Anthropic, and Responses APIs.
32
+
33
+ **Only two specific architectures are fast.** Every other model runs at the same speed as plain [mlx-lm](https://github.com/ml-explore/mlx-lm) (see the compatibility table below). This is not a "general-purpose acceleration runtime."
34
+
35
+ If you're coming from Ollama or LM Studio, the first difference you'll hit: **one process holds exactly one model.** Switching models means restarting the process (hosting multiple models at once from a single server is not supported). See "Constraints" below for details.
36
+
37
+ ## What this is
38
+
39
+ - An engine that loads a model into one long-lived process and speeds up decode with speculative decoding (self-speculation: the model's own MTP head plus context-based suffix lookup)
40
+ - An HTTP server (`mlxturbo-serve`) that exposes that engine over both the OpenAI-compatible API (`/v1/chat/completions`, `/v1/completions`, `/v1/responses`, `/v1/models`) and the Anthropic-compatible API (`/v1/messages`)
41
+ - Unsupported models still work (fallback), but in that case you get plain, unspeculated mlx-lm speed
42
+
43
+ ## Compatibility table (the honest version)
44
+
45
+ | Route | Target | Speculation | Measured speedup (vs. mlx-lm, see reproduction commands below) |
46
+ |---|---|---|---|
47
+ | `flash_spec` | Qwen3.8-Flash-Next (`qwen4_exp` architecture) + MTP sidecar | Yes (MTP depth 1 + fused hyper-connections kernel) | ~1.25x-1.39x (depends on task type, see below) |
48
+ | `spec` | Models satisfying the `qwen3_5` contract (e.g. the Qwen3.8-27B family) | Yes (MTP chain + suffix-lookup hybrid) | 1.3x-2.2x (depends on prompt content, see below) |
49
+ | `fallback` | Everything else | No | 1.0x (same speed as plain mlx-lm) |
50
+
51
+ You can see which route was selected in the startup log and in `GET /health`'s `runner` / `fallback_reason` fields. If you want to guard against silently falling back when speculation should be working, start with `--require-runner flash_spec` (or `spec`): the server refuses to start at all if the conditions aren't met. Details in [`docs/SERVER.md`](docs/SERVER.md).
52
+
53
+ ## Conditions for speculation to kick in
54
+
55
+ - `flash_spec` requires the model's `model_type` to be `qwen4_exp`, and MTP weights to be found (explicitly via `--mtp`, bundled in the main shards, or auto-discovered as a sidecar). If MTP isn't found, `flash_spec` does not degrade to a non-speculative mode — the route simply doesn't come up at all. Measurements are in [`docs/MTP-FLASH.md`](docs/MTP-FLASH.md)
56
+ - `spec` requires the model to match the `qwen3_5` contract (the layer structure and attention shape `SpecEngine` expects). If it doesn't match, the server falls back to `fallback`
57
+ - On both routes, `temp=0` (greedy) runs under exact-match verification against the true output distribution, and `temp>0` uses rejection sampling that keeps the distribution exactly identical too. However, sampling parameters that alter the distribution — `top_p<1.0` or `repetition_penalty!=1.0` — get a 400. The speculative block-verification assumes exact sampling from the target distribution, so distorting that distribution with sampling parameters breaks the verification's premise. This restriction does not apply to the `fallback` route
58
+
59
+ ## Installation
60
+
61
+ Requires an Apple Silicon Mac (macOS, with MLX/Metal available).
62
+
63
+ ```
64
+ git clone <this repository>
65
+ cd mlxturbo
66
+ uv sync
67
+ ```
68
+
69
+ ## Getting a model
70
+
71
+ - `fallback` / `spec` routes: point directly at a normal mlx-lm-compatible checkpoint (a Hugging Face repo ID or a local path)
72
+ - `flash_spec` route (Qwen3.8-Flash-Next): requires converting from the original checkpoint and extracting MTP. See [`docs/MTP-FLASH.md`](docs/MTP-FLASH.md) and `mlxturbo/convert_flash.py --help` (subcommands `estimate` / `extract-mtp` / `convert`). The `qwen4_exp` architecture isn't in mlx-lm upstream, but importing `mlxturbo` resolves it automatically — nothing is written to your mlx-lm package
73
+
74
+ ## Running it
75
+
76
+ Interactive CLI (`spec` route, aimed at the 27B family):
77
+
78
+ ```
79
+ uv run mlxturbo --model <path-or-repo-id> --prompt "hello"
80
+ ```
81
+
82
+ HTTP server:
83
+
84
+ ```
85
+ uv run mlxturbo-serve --model <path-or-repo-id> --served-model-name mymodel --port 8000
86
+ ```
87
+
88
+ Connection methods, the full option list, API key auth, and connection examples for opencode / Codex CLI / Claude Code / Chatbox are all in [`docs/SERVER.md`](docs/SERVER.md).
89
+
90
+ ## Constraints
91
+
92
+ - **One process, one model.** A model is loaded exactly once at startup and stays resident; switching models means starting a different process
93
+ - **Requests are processed serially.** Continuous batching is not implemented; each request serializes to one generation at a time. If multiple clients connect at once, later requests wait for the earlier one to finish (once the queue exceeds `--max-queue`, new requests get a 503)
94
+ - **`spec` / `flash_spec` routes return 400 for non-identity sampling parameters.** As noted above, this is because the speculative block-verification assumes exact distribution matching
95
+ - **Token logprobs are not supported.** The response's `logprobs` field is always `null` or an empty array
96
+ - For the full constraint list (the scope of determinism, context-length guards, etc.), see the "Constraints" section of [`docs/SERVER.md`](docs/SERVER.md)
97
+
98
+ ## Measured numbers and how to reproduce them
99
+
100
+ Raw result JSON files and the `docs/` write-ups linked below live in the GitHub repository, not in the PyPI package (the wheel/sdist ship only the `mlxturbo` library) — follow the links from [github.com/akakeishin/mlxturbo](https://github.com/akakeishin/mlxturbo) if you installed via `pip`.
101
+
102
+ All of the following were measured on an M3 Max 128GB / macOS 26.4 / mlx 0.32.2. Numbers shift across hardware generations (see "judgments that expire when the hardware generation changes" in [`docs/research/ROOFLINE-2026-08-26.md`](docs/research/ROOFLINE-2026-08-26.md)).
103
+
104
+ ### `spec` route (Qwen3.8-27B-4bit, greedy, 512 tok)
105
+
106
+ | Condition | decode tok/s | vs. mlx-lm |
107
+ |---|---|---|
108
+ | Plain mlx-lm (fallback-equivalent) | 21-23 | 1.0x |
109
+ | Self-speculation, sustained hard content (code) | 31.9 | 1.49x |
110
+ | Self-speculation, sustained hard content (prose) | 28.3 | 1.32x |
111
+
112
+ Reproduce: `uv run mlxturbo --model <model> --prompt "<prompt>"` (it prints `decode tok/s` automatically after generation). Comparison against plain mlx-lm: `uv run python bench/baseline.py <model-id>`.
113
+
114
+ ### `flash_spec` route (Qwen3.8-Flash-Next, v-l recipe + MTP 4bit, greedy, 48 tok)
115
+
116
+ | Prompt | Acceptance rate | Greedy tok/s | Speculative tok/s | Speedup |
117
+ |---|---|---|---|---|
118
+ | Prose (English 0.720 ± 0.046 / Japanese 0.682 ± 0.047) | ~0.70 | ~1.39x |
119
+ | Code | 0.564 ± 0.068 | ~1.25x |
120
+
121
+ Acceptance rate also varies by task type (code / prose / step-by-step instructions, etc). Details and reproduction commands are in the "tools" section of [`docs/MTP-FLASH.md`](docs/MTP-FLASH.md) (`tools/spec_flash_bench.py` and others). See also [`docs/BENCHMARKS.md`](docs/BENCHMARKS.md) for exactly which result file each number in this README traces back to, including one row whose source file we could not track down and one table whose own source document later flags it as not statistically significant.
122
+
123
+ ## Other documentation
124
+
125
+ - [`docs/README.md`](docs/README.md) — index of all documentation
126
+ - [`docs/SERVER.md`](docs/SERVER.md) — server startup, options, connection methods, constraints
127
+ - [`docs/OPERATIONS.md`](docs/OPERATIONS.md) — running a public instance: reverse proxy, `/health` monitoring, log reading, launchd
128
+ - [`docs/BENCHMARKS.md`](docs/BENCHMARKS.md) — which reproduction command and result file backs each published number
129
+ - [`docs/MTP-FLASH.md`](docs/MTP-FLASH.md) — design of Flash-Next's MTP speculative decoding
130
+ - [`docs/BACKLOG.md`](docs/BACKLOG.md) — things worth doing that haven't been started yet
131
+ - [`docs/RELEASE.md`](docs/RELEASE.md) — what to do before publishing
132
+
133
+ Everything under `docs/` besides this README's translation is Japanese-only for now.
@@ -0,0 +1,107 @@
1
+ # mlxturbo
2
+
3
+ English | [日本語](README.md)
4
+
5
+ A local inference engine for Apple Silicon (MLX), plus an HTTP server compatible with the OpenAI, Anthropic, and Responses APIs.
6
+
7
+ **Only two specific architectures are fast.** Every other model runs at the same speed as plain [mlx-lm](https://github.com/ml-explore/mlx-lm) (see the compatibility table below). This is not a "general-purpose acceleration runtime."
8
+
9
+ If you're coming from Ollama or LM Studio, the first difference you'll hit: **one process holds exactly one model.** Switching models means restarting the process (hosting multiple models at once from a single server is not supported). See "Constraints" below for details.
10
+
11
+ ## What this is
12
+
13
+ - An engine that loads a model into one long-lived process and speeds up decode with speculative decoding (self-speculation: the model's own MTP head plus context-based suffix lookup)
14
+ - An HTTP server (`mlxturbo-serve`) that exposes that engine over both the OpenAI-compatible API (`/v1/chat/completions`, `/v1/completions`, `/v1/responses`, `/v1/models`) and the Anthropic-compatible API (`/v1/messages`)
15
+ - Unsupported models still work (fallback), but in that case you get plain, unspeculated mlx-lm speed
16
+
17
+ ## Compatibility table (the honest version)
18
+
19
+ | Route | Target | Speculation | Measured speedup (vs. mlx-lm, see reproduction commands below) |
20
+ |---|---|---|---|
21
+ | `flash_spec` | Qwen3.8-Flash-Next (`qwen4_exp` architecture) + MTP sidecar | Yes (MTP depth 1 + fused hyper-connections kernel) | ~1.25x-1.39x (depends on task type, see below) |
22
+ | `spec` | Models satisfying the `qwen3_5` contract (e.g. the Qwen3.8-27B family) | Yes (MTP chain + suffix-lookup hybrid) | 1.3x-2.2x (depends on prompt content, see below) |
23
+ | `fallback` | Everything else | No | 1.0x (same speed as plain mlx-lm) |
24
+
25
+ You can see which route was selected in the startup log and in `GET /health`'s `runner` / `fallback_reason` fields. If you want to guard against silently falling back when speculation should be working, start with `--require-runner flash_spec` (or `spec`): the server refuses to start at all if the conditions aren't met. Details in [`docs/SERVER.md`](docs/SERVER.md).
26
+
27
+ ## Conditions for speculation to kick in
28
+
29
+ - `flash_spec` requires the model's `model_type` to be `qwen4_exp`, and MTP weights to be found (explicitly via `--mtp`, bundled in the main shards, or auto-discovered as a sidecar). If MTP isn't found, `flash_spec` does not degrade to a non-speculative mode — the route simply doesn't come up at all. Measurements are in [`docs/MTP-FLASH.md`](docs/MTP-FLASH.md)
30
+ - `spec` requires the model to match the `qwen3_5` contract (the layer structure and attention shape `SpecEngine` expects). If it doesn't match, the server falls back to `fallback`
31
+ - On both routes, `temp=0` (greedy) runs under exact-match verification against the true output distribution, and `temp>0` uses rejection sampling that keeps the distribution exactly identical too. However, sampling parameters that alter the distribution — `top_p<1.0` or `repetition_penalty!=1.0` — get a 400. The speculative block-verification assumes exact sampling from the target distribution, so distorting that distribution with sampling parameters breaks the verification's premise. This restriction does not apply to the `fallback` route
32
+
33
+ ## Installation
34
+
35
+ Requires an Apple Silicon Mac (macOS, with MLX/Metal available).
36
+
37
+ ```
38
+ git clone <this repository>
39
+ cd mlxturbo
40
+ uv sync
41
+ ```
42
+
43
+ ## Getting a model
44
+
45
+ - `fallback` / `spec` routes: point directly at a normal mlx-lm-compatible checkpoint (a Hugging Face repo ID or a local path)
46
+ - `flash_spec` route (Qwen3.8-Flash-Next): requires converting from the original checkpoint and extracting MTP. See [`docs/MTP-FLASH.md`](docs/MTP-FLASH.md) and `mlxturbo/convert_flash.py --help` (subcommands `estimate` / `extract-mtp` / `convert`). The `qwen4_exp` architecture isn't in mlx-lm upstream, but importing `mlxturbo` resolves it automatically — nothing is written to your mlx-lm package
47
+
48
+ ## Running it
49
+
50
+ Interactive CLI (`spec` route, aimed at the 27B family):
51
+
52
+ ```
53
+ uv run mlxturbo --model <path-or-repo-id> --prompt "hello"
54
+ ```
55
+
56
+ HTTP server:
57
+
58
+ ```
59
+ uv run mlxturbo-serve --model <path-or-repo-id> --served-model-name mymodel --port 8000
60
+ ```
61
+
62
+ Connection methods, the full option list, API key auth, and connection examples for opencode / Codex CLI / Claude Code / Chatbox are all in [`docs/SERVER.md`](docs/SERVER.md).
63
+
64
+ ## Constraints
65
+
66
+ - **One process, one model.** A model is loaded exactly once at startup and stays resident; switching models means starting a different process
67
+ - **Requests are processed serially.** Continuous batching is not implemented; each request serializes to one generation at a time. If multiple clients connect at once, later requests wait for the earlier one to finish (once the queue exceeds `--max-queue`, new requests get a 503)
68
+ - **`spec` / `flash_spec` routes return 400 for non-identity sampling parameters.** As noted above, this is because the speculative block-verification assumes exact distribution matching
69
+ - **Token logprobs are not supported.** The response's `logprobs` field is always `null` or an empty array
70
+ - For the full constraint list (the scope of determinism, context-length guards, etc.), see the "Constraints" section of [`docs/SERVER.md`](docs/SERVER.md)
71
+
72
+ ## Measured numbers and how to reproduce them
73
+
74
+ Raw result JSON files and the `docs/` write-ups linked below live in the GitHub repository, not in the PyPI package (the wheel/sdist ship only the `mlxturbo` library) — follow the links from [github.com/akakeishin/mlxturbo](https://github.com/akakeishin/mlxturbo) if you installed via `pip`.
75
+
76
+ All of the following were measured on an M3 Max 128GB / macOS 26.4 / mlx 0.32.2. Numbers shift across hardware generations (see "judgments that expire when the hardware generation changes" in [`docs/research/ROOFLINE-2026-08-26.md`](docs/research/ROOFLINE-2026-08-26.md)).
77
+
78
+ ### `spec` route (Qwen3.8-27B-4bit, greedy, 512 tok)
79
+
80
+ | Condition | decode tok/s | vs. mlx-lm |
81
+ |---|---|---|
82
+ | Plain mlx-lm (fallback-equivalent) | 21-23 | 1.0x |
83
+ | Self-speculation, sustained hard content (code) | 31.9 | 1.49x |
84
+ | Self-speculation, sustained hard content (prose) | 28.3 | 1.32x |
85
+
86
+ Reproduce: `uv run mlxturbo --model <model> --prompt "<prompt>"` (it prints `decode tok/s` automatically after generation). Comparison against plain mlx-lm: `uv run python bench/baseline.py <model-id>`.
87
+
88
+ ### `flash_spec` route (Qwen3.8-Flash-Next, v-l recipe + MTP 4bit, greedy, 48 tok)
89
+
90
+ | Prompt | Acceptance rate | Greedy tok/s | Speculative tok/s | Speedup |
91
+ |---|---|---|---|---|
92
+ | Prose (English 0.720 ± 0.046 / Japanese 0.682 ± 0.047) | ~0.70 | ~1.39x |
93
+ | Code | 0.564 ± 0.068 | ~1.25x |
94
+
95
+ Acceptance rate also varies by task type (code / prose / step-by-step instructions, etc). Details and reproduction commands are in the "tools" section of [`docs/MTP-FLASH.md`](docs/MTP-FLASH.md) (`tools/spec_flash_bench.py` and others). See also [`docs/BENCHMARKS.md`](docs/BENCHMARKS.md) for exactly which result file each number in this README traces back to, including one row whose source file we could not track down and one table whose own source document later flags it as not statistically significant.
96
+
97
+ ## Other documentation
98
+
99
+ - [`docs/README.md`](docs/README.md) — index of all documentation
100
+ - [`docs/SERVER.md`](docs/SERVER.md) — server startup, options, connection methods, constraints
101
+ - [`docs/OPERATIONS.md`](docs/OPERATIONS.md) — running a public instance: reverse proxy, `/health` monitoring, log reading, launchd
102
+ - [`docs/BENCHMARKS.md`](docs/BENCHMARKS.md) — which reproduction command and result file backs each published number
103
+ - [`docs/MTP-FLASH.md`](docs/MTP-FLASH.md) — design of Flash-Next's MTP speculative decoding
104
+ - [`docs/BACKLOG.md`](docs/BACKLOG.md) — things worth doing that haven't been started yet
105
+ - [`docs/RELEASE.md`](docs/RELEASE.md) — what to do before publishing
106
+
107
+ Everything under `docs/` besides this README's translation is Japanese-only for now.
@@ -0,0 +1,111 @@
1
+ # mlxturbo
2
+
3
+ [English](README.en.md) | 日本語
4
+
5
+ Apple Silicon (MLX) 上のローカル推論エンジンと、OpenAI / Anthropic / Responses 互換の HTTP サーバー。
6
+
7
+ **速いのは特定の2アーキテクチャだけ**で、それ以外のモデルは素の [mlx-lm](https://github.com/ml-explore/mlx-lm) と同速で動く汎用ランタイムです(後述の対応表を参照)。「汎用高速化ランタイム」ではありません。
8
+
9
+ Ollama や LM Studio から来た人が最初に踏む差分: **1 プロセスにつき 1 モデルしかロードしない。** モデルの切り替えはプロセスの再起動が要る(複数モデルの同時ホストは非対応)。詳しくは後述の「制約」を参照。
10
+
11
+ ## これは何か
12
+
13
+ - モデルを 1 プロセスにロードして常駐させ、投機デコード(自己投機: 本体のモデル自身が出す MTP ヘッド + 文脈 suffix-lookup)で decode を高速化するエンジン
14
+ - 上記エンジンを OpenAI 互換 (`/v1/chat/completions`, `/v1/completions`, `/v1/responses`, `/v1/models`) と Anthropic 互換 (`/v1/messages`) の両方で叩ける HTTP サーバー (`mlxturbo-serve`)
15
+ - 対応していないモデルでも動く(フォールバック)が、その場合は投機なしの素の mlx-lm と同速
16
+
17
+ ## 対応表(正直に)
18
+
19
+ | 経路 | 対象 | 投機 | 実測倍率(mlx-lm 比、後述の再現コマンド参照) |
20
+ |---|---|---|---|
21
+ | `flash_spec` | Qwen3.8-Flash-Next (`qwen4_exp` アーキテクチャ) + MTP サイドカー | あり(MTP 深さ1 + hyper-connections 融合カーネル) | 約 1.25x〜1.39x(課題の種類に依存、後述) |
22
+ | `spec` | `qwen3_5` 契約を満たすモデル(例: Qwen3.8-27B 系) | あり(MTP チェイン + suffix-lookup 混成) | 1.3x〜2.2x(プロンプト内容依存、後述) |
23
+ | `fallback` | 上記以外の全モデル | なし | 1.0x(素の mlx-lm と同速) |
24
+
25
+ どの経路が選ばれたかは起動ログと `GET /health` の `runner` / `fallback_reason` で確認できる。「投機が効くはずなのに黙って fallback に落ちている」を防ぎたい場合は `--require-runner flash_spec`(または `spec`)を付けて起動すると、条件を満たさないときに起動自体が失敗する(詳細は [`docs/SERVER.md`](docs/SERVER.md))。
26
+
27
+ ## 投機が効く条件
28
+
29
+ - `flash_spec` が動くには、モデルの `model_type` が `qwen4_exp` で、かつ MTP 重みが見つかっている必要がある(`--mtp` で明示指定するか、本体シャードに同梱されているか、サイドカーとして自動発見される)。MTP が見つからないときは投機無しの `flash_spec` にはならず、経路自体が成立しない。実測は [`docs/MTP-FLASH.md`](docs/MTP-FLASH.md) にある
30
+ - `spec` が動くには、モデルが `qwen3_5` の contract(`SpecEngine` が要求する層構成や attention の形)に合っている必要がある。合っていなければ `fallback` に落ちる
31
+ - 両経路とも、`temp=0`(greedy)は出力分布と厳密に一致する完全一致検証で動き、`temp>0` も棄却サンプリングで分布は厳密に同一のまま。ただし `top_p<1.0` や `repetition_penalty≠1.0` のように分布を変えるサンプリングパラメータを渡すと 400 になる。投機側のブロック検証が「対象分布からの厳密なサンプリング一致」を前提にしているため、サンプリングパラメータで分布そのものを歪められると検証が成り立たなくなる。この制限は `fallback` 経路には無い
32
+
33
+ ## インストール
34
+
35
+ Apple Silicon Mac(macOS、MLX/Metal が使える環境)が前提。
36
+
37
+ ```
38
+ git clone <このリポジトリ>
39
+ cd mlxturbo
40
+ uv sync
41
+ ```
42
+
43
+ ## モデルの入手
44
+
45
+ - `fallback` / `spec` 経路: 通常の mlx-lm 互換チェックポイント(Hugging Face repo ID かローカルパス)をそのまま指定できる
46
+ - `flash_spec` 経路(Qwen3.8-Flash-Next): 元 checkpoint からの変換とMTP抽出が要る。手順は [`docs/MTP-FLASH.md`](docs/MTP-FLASH.md) と `mlxturbo/convert_flash.py --help`(`estimate` / `extract-mtp` / `convert` の各サブコマンド)を参照。`qwen4_exp` アーキテクチャは mlx-lm 本体に無いが、`mlxturbo` を import した時点で自動的に解決される(利用者の mlx-lm パッケージへは何も書き込まない)
47
+
48
+ ## 起動
49
+
50
+ 対話 CLI(`spec` 経路、27B 系向け):
51
+
52
+ ```
53
+ uv run mlxturbo --model <path-or-repo-id> --prompt "こんにちは"
54
+ ```
55
+
56
+ HTTP サーバー:
57
+
58
+ ```
59
+ uv run mlxturbo-serve --model <path-or-repo-id> --served-model-name mymodel --port 8000
60
+ ```
61
+
62
+ 接続方法・オプション一覧・API キー認証・opencode / Codex CLI / Claude Code / Chatbox からの接続例は [`docs/SERVER.md`](docs/SERVER.md) に詳しくまとめてある。
63
+
64
+ ## 制約
65
+
66
+ - **1 プロセス 1 モデル。** モデルは起動時に 1 回だけロードして常駐する設計で、複数モデルの切り替えは別プロセスを立てる必要がある
67
+ - **リクエストは直列処理。** 継続バッチング(continuous batching)は未実装で、1 リクエスト = 1 生成に直列化される。複数クライアントを同時に繋ぐと、後着のリクエストは先着の生成が終わるまで待たされる(待ち行列上限 `--max-queue` を超えると 503)
68
+ - **spec / flash_spec 経路は非恒等値のサンプリングパラメータを 400 にする。** 理由は上述のとおり、投機のブロック検証が厳密な分布一致を前提にしているため
69
+ - **token logprobs は未対応。** レスポンスの `logprobs` は常に `null` または空配列
70
+ - 詳しい制約一覧(決定性の範囲、文脈長ガードなど)は [`docs/SERVER.md`](docs/SERVER.md) の「制約」節を参照
71
+
72
+ ## 実測値と再現コマンド
73
+
74
+ 以下でリンクしている結果 JSON や `docs/` の文書は GitHub リポジトリ側にあり、PyPI パッケージ(wheel/sdist)には含まれない(配布物に入るのは `mlxturbo` ライブラリ本体のみ)。`pip` で入れた場合は [github.com/akakeishin/mlxturbo](https://github.com/akakeishin/mlxturbo) 側からリンクを辿ること。
75
+
76
+ 以下はすべて M3 Max 128GB / macOS 26.4 / mlx 0.32.2 での実測。ハードウェア世代が変わると数字は変わる(詳しくは [`docs/research/ROOFLINE-2026-08-26.md`](docs/research/ROOFLINE-2026-08-26.md) の「ハード世代が変わると失効する判定」を参照)。
77
+
78
+ ### `spec` 経路(Qwen3.8-27B-4bit、greedy、512 tok)
79
+
80
+ | 条件 | decode tok/s | 対 mlx-lm |
81
+ |---|---|---|
82
+ | mlx-lm 素(フォールバック相当) | 21〜23 | 1.0x |
83
+ | 自己投機・難しい内容持続(code) | 31.9 | 1.49x |
84
+ | 自己投機・難しい内容持続(prose) | 28.3 | 1.32x |
85
+
86
+ 再現: `uv run mlxturbo --model <model> --prompt "<prompt>"`(生成の後に `decode tok/s` が自動で出力される)。mlx-lm 素との対照は `uv run python bench/baseline.py <model-id>`。
87
+
88
+ ### `flash_spec` 経路(Qwen3.8-Flash-Next、v-l レシピ + MTP 4bit、greedy)
89
+
90
+ 受理率は 160 トークン × 10 課題で測った(反復 200 以上)。
91
+
92
+ | 課題 | 受理率 | 倍率 |
93
+ |---|---|---|
94
+ | 散文(英語 0.720 ± 0.046 / 日本語 0.682 ± 0.047) | 約 0.70 | 約 1.39x |
95
+ | コード | 0.564 ± 0.068 | 約 1.25x |
96
+
97
+ 投機なしは 31.14 ms/token。**効くかどうかは言語ではなく課題の種類で決まり、コードが最も低い**(信頼区間が重ならない)。どの課題でも投機は効く。
98
+
99
+ 短い試行(48 トークン、反復 30 前後)では言語差があるように見えるが、標準誤差が ±0.09 あり有意ではない。**受理率は反復 300 以上で見ること。**経緯は [`docs/MTP-FLASH.md`](docs/MTP-FLASH.md) を参照。
100
+
101
+ 再現: `tools/spec_flash_accept.py`(受理率)と `tools/spec_flash_bench.py`(速度)。コマンドは [`docs/MTP-FLASH.md`](docs/MTP-FLASH.md) の「道具」節。
102
+
103
+ ## その他のドキュメント
104
+
105
+ - [`docs/README.md`](docs/README.md) — ドキュメント全体の索引
106
+ - [`docs/SERVER.md`](docs/SERVER.md) — サーバーの起動・オプション・接続方法・制約
107
+ - [`docs/OPERATIONS.md`](docs/OPERATIONS.md) — 公開インスタンスの運用(リバースプロキシ・`/health` 監視・ログの読み方・launchd 常駐)
108
+ - [`docs/BENCHMARKS.md`](docs/BENCHMARKS.md) — この README の実測値がどのコマンド・どの結果 JSON から来ているか
109
+ - [`docs/MTP-FLASH.md`](docs/MTP-FLASH.md) — Flash-Next の MTP 投機デコード設計
110
+ - [`docs/BACKLOG.md`](docs/BACKLOG.md) — やりたいが未着手のもの
111
+ - [`docs/RELEASE.md`](docs/RELEASE.md) — 公開時にやること
@@ -0,0 +1,10 @@
1
+ from . import _arch_registry
2
+ from .kernels.dispatch import enable
3
+
4
+ # Make qwen4_exp (Flash-Next) resolvable without writing into the user's mlx_lm.
5
+ # Importing any mlxturbo submodule always goes through here (a package __init__
6
+ # runs before its submodules). See the module docstring of _arch_registry.py for
7
+ # the details.
8
+ _arch_registry.install()
9
+
10
+ __all__ = ["enable"]
@@ -0,0 +1,99 @@
1
+ """Resolve qwen4_exp (Qwen3.8-Flash-Next) without writing into the user's mlx_lm.
2
+
3
+ ## Background
4
+
5
+ The Flash-Next model class is not part of mlx-lm proper (mlx-lm PR #1788 is
6
+ still unmerged). This package carries a vendored copy as
7
+ `mlxturbo/_vendor/qwen4_exp.py`, so that it ships inside the wheel and
8
+ resolves from an installed environment, not just a repo checkout.
9
+
10
+ mlx-lm has exactly one path for resolving a model class,
11
+
12
+ importlib.import_module(f"mlx_lm.models.{model_type}")
13
+
14
+ in `mlx_lm/utils.py:_get_classes()`, and mlx-lm itself has no plugin-style
15
+ registration mechanism. Previously we resolved this by having
16
+ `convert_flash.py install-arch` physically copy the vendored file into the
17
+ user's site-packages (`<mlx_lm install dir>/models/qwen4_exp.py`), but that
18
+ rewrites the user's mlx_lm package, with the side effects that it disappears
19
+ on `uv sync` or on an mlx-lm update, and that any other project sharing the
20
+ same mlx_lm gets dragged along with it (review comment: merely trying out
21
+ mlxturbo breaks your environment).
22
+
23
+ ## What this does instead
24
+
25
+ It inserts into `sys.meta_path` a minimal finder that redirects only the fully
26
+ qualified name `mlx_lm.models.qwen4_exp` to the vendored file. It takes no part
27
+ in any other import (find_spec returns None and defers to the default finders
28
+ as-is), so it affects nothing else, mlx_lm included.
29
+
30
+ Nothing is written into site-packages. The `mlx_lm.models` package itself stays
31
+ as it is (the normal installation), and relative imports inside the vendored
32
+ file such as `from .base import ...` ride naturally on the usual
33
+ `mlx_lm.models.base` resolution, because the finder gives the spec the correct
34
+ `__name__` / `__package__` (= "mlx_lm.models").
35
+
36
+ ## Why a meta_path finder rather than registering directly in `sys.modules`
37
+
38
+ Simply placing `sys.modules["mlx_lm.models.qwen4_exp"] = <already-executed
39
+ module>` ahead of time when mlxturbo is imported would also work for every
40
+ resolution path — `import mlx_lm.models.qwen4_exp as Q` /
41
+ `from mlx_lm.models import qwen4_exp` / `importlib.import_module(...)` — because
42
+ CPython's import-as / from-import fall back to `sys.modules[fullname]` when the
43
+ attribute lookup fails. But
44
+ `NGRAM_ON_DISK = os.environ.get("FASTMLX_NGRAM_DISK") == "1"` at the top of the
45
+ vendored file is evaluated exactly once when the module executes, and the
46
+ convention on the calling side (cli.py / server.py / convert_flash.py / things
47
+ under tools) is to set that environment variable immediately before qwen4_exp
48
+ is actually loaded (it is not yet set at the time mlxturbo is imported).
49
+ Registering into `sys.modules` up front would execute the module body before
50
+ that convention gets its chance, freezing it in a state that ignored
51
+ `FASTMLX_NGRAM_DISK`. A meta_path finder lets us defer execution until the
52
+ moment the import of `mlx_lm.models.qwen4_exp` is actually requested (= after
53
+ the caller has set the environment variable), so the convention stays intact.
54
+ """
55
+
56
+ from __future__ import annotations
57
+
58
+ import importlib.abc
59
+ import importlib.util
60
+ import sys
61
+ from pathlib import Path
62
+
63
+ MODULE_NAME = "mlx_lm.models.qwen4_exp"
64
+ # Resolved relative to this module's own installed location (not the repo
65
+ # root), so this works the same from a `pip install`-ed wheel as it does from
66
+ # a repo checkout — nothing here depends on `tools/` existing on disk.
67
+ VENDOR_PATH = Path(__file__).resolve().parent / "_vendor" / "qwen4_exp.py"
68
+
69
+
70
+ class _FlashNextArchFinder(importlib.abc.MetaPathFinder):
71
+ """Redirect only `mlx_lm.models.qwen4_exp` to the vendored file."""
72
+
73
+ def find_spec(self, fullname, path, target=None):
74
+ if fullname != MODULE_NAME:
75
+ return None
76
+ if not VENDOR_PATH.exists():
77
+ # Should not happen for a normal install (the vendor file ships
78
+ # inside the mlxturbo package itself), but silently defer to the
79
+ # default finders rather than raise — that would also break
80
+ # imports of models that never use qwen4_exp.
81
+ return None
82
+ return importlib.util.spec_from_file_location(fullname, VENDOR_PATH)
83
+
84
+
85
+ def install() -> None:
86
+ """Make qwen4_exp resolvable within the `mlx_lm.models` namespace.
87
+
88
+ Idempotent (calling it twice still installs only one finder). It is called
89
+ from `mlxturbo`'s `__init__.py`, so it is always in effect as soon as
90
+ `mlxturbo` is imported (regardless of whether qwen4_exp actually gets used).
91
+ """
92
+
93
+ if any(isinstance(f, _FlashNextArchFinder) for f in sys.meta_path):
94
+ return
95
+ # Even if an old install-arch copy is still sitting in site-packages, this
96
+ # one takes precedence (the vendored file is the source of truth). Since it
97
+ # takes no part in any other name, inserting it at the front has no effect
98
+ # on other imports.
99
+ sys.meta_path.insert(0, _FlashNextArchFinder())