MLX
speculative-decoding
multi-token-prediction
mtp
apple-silicon
omlx
mtplx
qwen
qwen3.8
quantization
Instructions to use KaedeTai/mlx-mtp-graft with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use KaedeTai/mlx-mtp-graft with MLX:
# Download the model from the Hub pip install huggingface_hub[hf_xet] huggingface-cli download --local-dir mlx-mtp-graft KaedeTai/mlx-mtp-graft
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
- Atomic Chat
MTP head graft for MLX Qwen3.8-27B quantizations that dropped theirs
Browse files- README.md +168 -0
- graft_mtp.py +150 -0
- qwen3.8-27b-mtp-4bit.safetensors +3 -0
README.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
library_name: mlx
|
| 4 |
+
tags:
|
| 5 |
+
- mlx
|
| 6 |
+
- speculative-decoding
|
| 7 |
+
- multi-token-prediction
|
| 8 |
+
- apple-silicon
|
| 9 |
+
- qwen3.8
|
| 10 |
+
base_model:
|
| 11 |
+
- Qwen/Qwen3.8-27B
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
# MTP graft — restoring Multi-Token-Prediction heads to MLX quantizations that dropped them
|
| 15 |
+
|
| 16 |
+
Most MLX quantizations of Qwen3.5/3.6/3.8 keep `mtp_num_hidden_layers: 1` in
|
| 17 |
+
`config.json` **while silently dropping the MTP weights**. The checkpoint loads,
|
| 18 |
+
generates correctly, and gives you no reason to suspect anything — it just
|
| 19 |
+
cannot use Lightning MTP in oMLX, and nothing tells you why.
|
| 20 |
+
|
| 21 |
+
This repo contains the **MTP head only** (314 MB), plus a tool to graft it onto a
|
| 22 |
+
recipient checkpoint. No base weights are redistributed.
|
| 23 |
+
|
| 24 |
+
Measured on an Apple **M5 Max, 128 GB**, macOS 26.4.1, oMLX 0.6.3rc1.
|
| 25 |
+
|
| 26 |
+
## Result
|
| 27 |
+
|
| 28 |
+
Recipient: `Qwen3.8-27B` abliterated with [Heretic](https://github.com/p-e-w/heretic)
|
| 29 |
+
(`trohrbaugh/Qwen3.8-27B-heretic-ara`, MLX affine 4-bit g64), which ships with
|
| 30 |
+
**0 MTP tensors**. Served through oMLX with `mtp_enabled`, greedy, thinking off
|
| 31 |
+
via `chat_template_kwargs`, A/B/A interleaved:
|
| 32 |
+
|
| 33 |
+
| prompt | before (no MTP) | after graft | speedup |
|
| 34 |
+
|---|---:|---:|---:|
|
| 35 |
+
| Python LRU cache | 29.0 / 29.9 tok/s | **61.1 / 67.1** | **2.18×** |
|
| 36 |
+
| Chinese prose (200 chars) | 29.4 tok/s | **41.6 / 40.3** | **1.39×** |
|
| 37 |
+
|
| 38 |
+
For scale, the same machine and settings on a checkpoint that *shipped* with its
|
| 39 |
+
MTP heads intact (`tozp/…-OBLITERATED-V2`) gives 63.3 / 39–41 tok/s — i.e. the
|
| 40 |
+
graft lands the recipient in the same place, and the MTP multiplier itself is
|
| 41 |
+
the same (2.15× vs 2.18×).
|
| 42 |
+
|
| 43 |
+
**MTP is strongly content-dependent.** Code roughly doubles; Chinese prose gains
|
| 44 |
+
~40%. That matches what a separate speculative-decoding stack (DFlash2 block
|
| 45 |
+
diffusion) does on the same machine — two unrelated mechanisms hitting the same
|
| 46 |
+
wall on CJK, which suggests the cause is CJK token entropy rather than any one
|
| 47 |
+
draft head being undertrained.
|
| 48 |
+
|
| 49 |
+
## Output fidelity — read this
|
| 50 |
+
|
| 51 |
+
Speculative decoding is verify-then-accept: the head only *proposes*, the base
|
| 52 |
+
model verifies. It cannot make the model say something the base model would
|
| 53 |
+
reject. But batched K-token verification and single-token decoding take
|
| 54 |
+
different floating-point paths, so **near-ties can flip at temperature 0**.
|
| 55 |
+
|
| 56 |
+
Byte-for-byte comparison, fixed seed, `temperature=0, top_p=1, top_k=0`:
|
| 57 |
+
|
| 58 |
+
| content | tokens (before / after) | identical | similarity |
|
| 59 |
+
|---|---|---|---:|
|
| 60 |
+
| Python code | 165 / 165 | **yes** | 1.000 |
|
| 61 |
+
| Arithmetic | 3 / 3 | **yes** | 1.000 |
|
| 62 |
+
| Chinese explanation | 70 / 70 | no — 1 token | 0.992 |
|
| 63 |
+
| Chinese creative prose | 192 / 217 | no — diverges at ~50% | 0.663 |
|
| 64 |
+
|
| 65 |
+
The single-token flip looks like this:
|
| 66 |
+
|
| 67 |
+
```
|
| 68 |
+
before: …因為它假設近期被使用過的資料[[,未來被再次使用的機率較高。]]
|
| 69 |
+
after : …因為它假設近期被使用過的資料[[在未來被再次使用的機率較高。]]
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
Both are fluent; neither is better. Low-entropy text has no near-ties, so it
|
| 73 |
+
reproduces exactly. Creative text is full of them, and once one token flips
|
| 74 |
+
everything after it differs. **If you need bit-reproducible output, do not
|
| 75 |
+
enable MTP.**
|
| 76 |
+
|
| 77 |
+
## Known incompatibility: `mlx_lm` breaks on MTP tensors
|
| 78 |
+
|
| 79 |
+
A checkpoint containing MTP tensors produces **garbage** under `mlx_lm.load`,
|
| 80 |
+
silently — no error, no warning. Controlled test, same checkpoint, only the 46
|
| 81 |
+
MTP tensors differing:
|
| 82 |
+
|
| 83 |
+
```
|
| 84 |
+
BigBang-v1-heretic-MLX-4bit (MTP = 0) -> normal Chinese output
|
| 85 |
+
BigBang-v1-heretic-MLX-4bit-mtp (MTP = 46) -> ";av L WL two one lya"**开始中j位ode理ments谷雨谷…"
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
`mlx_vlm` and oMLX both handle the same checkpoints correctly. So:
|
| 89 |
+
|
| 90 |
+
* **oMLX** — works, and is the only stack that actually *uses* the head.
|
| 91 |
+
* **mlx_vlm** — works (ignores the head).
|
| 92 |
+
* **mlx_lm** — **broken**. Keep an ungrafted copy if you need this path.
|
| 93 |
+
|
| 94 |
+
This bit us: we first blamed vision tensors for the same garbage on a VL
|
| 95 |
+
checkpoint that happened to have both. It is the MTP tensors.
|
| 96 |
+
|
| 97 |
+
## Usage
|
| 98 |
+
|
| 99 |
+
```bash
|
| 100 |
+
python graft_mtp.py \
|
| 101 |
+
--recipient /path/to/model-without-mtp \
|
| 102 |
+
--donor /path/to/donor-or-standalone-head \
|
| 103 |
+
--out /path/to/model-with-mtp
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
The recipient's weights are **hard-linked**, not copied — the graft costs only
|
| 107 |
+
the MTP shard on disk. The original directory is never modified.
|
| 108 |
+
|
| 109 |
+
The tool refuses to graft across mismatched `hidden_size`, head counts,
|
| 110 |
+
`vocab_size`, `num_experts`, `moe_intermediate_size`, or a different base
|
| 111 |
+
quantization scheme (`--force` overrides). Two donor layouts are handled: heads
|
| 112 |
+
already namespaced under `language_model.mtp.`, and repos that *are* just the
|
| 113 |
+
head (`fc`/`layers.0`/`norm`/`pre_fc_norm_*`), such as
|
| 114 |
+
[`mlx-community/Qwen3.6-35B-A3B-MTP-4bit`](https://huggingface.co/mlx-community/Qwen3.6-35B-A3B-MTP-4bit).
|
| 115 |
+
|
| 116 |
+
Then in oMLX: set `mtp_enabled` for the model **and unload/reload it**. oMLX
|
| 117 |
+
reads model settings at load time only — editing `model_settings.json` while the
|
| 118 |
+
model is resident has no effect whatsoever (verified twice).
|
| 119 |
+
|
| 120 |
+
### Finding recipients
|
| 121 |
+
|
| 122 |
+
Check the weights, not the config — the config lies:
|
| 123 |
+
|
| 124 |
+
```python
|
| 125 |
+
import json
|
| 126 |
+
wm = json.load(open(f"{d}/model.safetensors.index.json"))["weight_map"]
|
| 127 |
+
print(sum(1 for k in wm if "mtp" in k.lower() or "nextn" in k.lower()))
|
| 128 |
+
```
|
| 129 |
+
|
| 130 |
+
Of eight Qwen3.5/3.6/3.8 MLX checkpoints on hand, **four** declared
|
| 131 |
+
`mtp_num_hidden_layers: 1` with zero MTP tensors present.
|
| 132 |
+
|
| 133 |
+
## Provenance
|
| 134 |
+
|
| 135 |
+
The head in this repo is **Qwen's own**, not something trained here:
|
| 136 |
+
|
| 137 |
+
1. `Qwen/Qwen3.8-27B` — original MTP heads
|
| 138 |
+
2. `tozp/Qwen3.8-27B-OBLITERATED-V2-mlx-4bit` — MLX affine 4-bit g64; its
|
| 139 |
+
`abliteration_metadata.json` records *"Restore MTP + vision tensors from
|
| 140 |
+
stock"*, so the heads it carries are unmodified
|
| 141 |
+
3. this repo — those 29 tensors, extracted unchanged
|
| 142 |
+
|
| 143 |
+
29 tensors, 314.3 MB, `sha256[:16] = 505827ce17922d2d`.
|
| 144 |
+
|
| 145 |
+
Applies to **`qwen3_5` dense, hidden 5120** (Qwen3.8-27B and derivatives).
|
| 146 |
+
For `qwen3_5_moe` (hidden 2048, 256 experts), use
|
| 147 |
+
`mlx-community/Qwen3.6-35B-A3B-MTP-4bit` as donor instead — same tool, 46
|
| 148 |
+
tensors.
|
| 149 |
+
|
| 150 |
+
## Limitations
|
| 151 |
+
|
| 152 |
+
* One machine (M5 Max), one serving stack (oMLX), the prompt set above. No
|
| 153 |
+
long-context, no batch >1, no acceptance-rate instrumentation — oMLX does not
|
| 154 |
+
expose one.
|
| 155 |
+
* Grafting the stock head onto an **abliterated** model is not obviously sound in
|
| 156 |
+
theory. The empirical argument: the recipient's KL to stock is 0.0535 (Heretic
|
| 157 |
+
reports it), while a checkpoint that ships this head natively sits at 0.9624 —
|
| 158 |
+
the recipient is ~18× closer to stock than a model where the head demonstrably
|
| 159 |
+
works. And the failure mode is bounded: a mismatched head lowers acceptance,
|
| 160 |
+
it cannot corrupt output.
|
| 161 |
+
* Not tested with vision inputs on VL recipients.
|
| 162 |
+
|
| 163 |
+
## Credits
|
| 164 |
+
|
| 165 |
+
- [Qwen](https://huggingface.co/Qwen/Qwen3.8-27B) — the MTP heads
|
| 166 |
+
- [tozp](https://huggingface.co/tozp/Qwen3.8-27B-OBLITERATED-V2-mlx-4bit) — the 4-bit quantization they were extracted from
|
| 167 |
+
- [Heretic](https://github.com/p-e-w/heretic) / [trohrbaugh](https://huggingface.co/trohrbaugh/Qwen3.8-27B-heretic-ara) — the recipient
|
| 168 |
+
- [oMLX](https://omlx.ai) — Lightning MTP
|
graft_mtp.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Graft Multi-Token-Prediction heads onto an MLX checkpoint that lost them.
|
| 3 |
+
|
| 4 |
+
Many MLX quantizations of Qwen3.5/3.6/3.8 keep `mtp_num_hidden_layers: 1` in
|
| 5 |
+
config.json while silently dropping the MTP weights, because the standard
|
| 6 |
+
conversion path only walks the main decoder. The result loads fine and runs
|
| 7 |
+
fine -- it just cannot use Lightning MTP in oMLX, and nothing tells you why.
|
| 8 |
+
|
| 9 |
+
This copies the MTP subtree from a donor that still has it, into a recipient
|
| 10 |
+
that does not. The recipient's own weights are never modified: they are
|
| 11 |
+
hard-linked into a new directory and the MTP tensors are added as one extra
|
| 12 |
+
shard.
|
| 13 |
+
|
| 14 |
+
Why this is safe: speculative decoding is verify-then-accept. The MTP head
|
| 15 |
+
only *proposes* tokens; the base model verifies every one of them. A poorly
|
| 16 |
+
matched head lowers the acceptance rate -- it costs throughput, it cannot
|
| 17 |
+
corrupt output. The worst outcome is no speedup.
|
| 18 |
+
|
| 19 |
+
Two caveats that are real and measured, see README.
|
| 20 |
+
"""
|
| 21 |
+
import argparse, json, os, shutil, sys
|
| 22 |
+
import mlx.core as mx
|
| 23 |
+
|
| 24 |
+
MTP_PREFIX = "language_model.mtp."
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _load_all(d):
|
| 28 |
+
idx = os.path.join(d, "model.safetensors.index.json")
|
| 29 |
+
if os.path.exists(idx):
|
| 30 |
+
wm = json.load(open(idx))["weight_map"]
|
| 31 |
+
return wm, sorted(set(wm.values()))
|
| 32 |
+
files = sorted(f for f in os.listdir(d) if f.endswith(".safetensors"))
|
| 33 |
+
wm = {}
|
| 34 |
+
for f in files:
|
| 35 |
+
for k in mx.load(os.path.join(d, f)).keys():
|
| 36 |
+
wm[k] = f
|
| 37 |
+
return wm, files
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def collect_mtp(donor):
|
| 41 |
+
"""Pull the MTP subtree out of `donor`, normalised to MTP_PREFIX.
|
| 42 |
+
|
| 43 |
+
Handles both layouts seen in the wild:
|
| 44 |
+
* in-model -- keys already start with `language_model.mtp.`
|
| 45 |
+
* standalone -- the repo IS just the head (fc/layers.0/norm/pre_fc_*),
|
| 46 |
+
e.g. mlx-community/Qwen3.6-35B-A3B-MTP-4bit
|
| 47 |
+
"""
|
| 48 |
+
wm, files = _load_all(donor)
|
| 49 |
+
inline = {k: k for k in wm if k.startswith(MTP_PREFIX)}
|
| 50 |
+
if inline:
|
| 51 |
+
keys = inline
|
| 52 |
+
else:
|
| 53 |
+
roots = ("fc.", "layers.", "norm.", "pre_fc_norm_embedding.",
|
| 54 |
+
"pre_fc_norm_hidden.")
|
| 55 |
+
keys = {k: MTP_PREFIX + k for k in wm if k.startswith(roots)}
|
| 56 |
+
if not keys:
|
| 57 |
+
sys.exit(f"no MTP tensors found in {donor}")
|
| 58 |
+
out = {}
|
| 59 |
+
for f in files:
|
| 60 |
+
w = mx.load(os.path.join(donor, f))
|
| 61 |
+
for src, dst in keys.items():
|
| 62 |
+
if wm[src] == f and src in w:
|
| 63 |
+
out[dst] = w[src]
|
| 64 |
+
if len(out) != len(keys):
|
| 65 |
+
sys.exit(f"expected {len(keys)} MTP tensors, recovered {len(out)}")
|
| 66 |
+
return out
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def check(recipient_cfg, donor_cfg):
|
| 70 |
+
"""Refuse mismatched architectures. These are the fields the MTP head's
|
| 71 |
+
shapes actually depend on; a mismatch means the graft cannot work."""
|
| 72 |
+
r = recipient_cfg.get("text_config", recipient_cfg)
|
| 73 |
+
d = donor_cfg.get("text_config", donor_cfg)
|
| 74 |
+
bad = []
|
| 75 |
+
for k in ("hidden_size", "num_attention_heads", "num_key_value_heads",
|
| 76 |
+
"head_dim", "vocab_size", "num_experts", "moe_intermediate_size"):
|
| 77 |
+
if k in r and k in d and r[k] != d[k]:
|
| 78 |
+
bad.append(f"{k}: recipient={r[k]} donor={d[k]}")
|
| 79 |
+
rq = (recipient_cfg.get("quantization") or {})
|
| 80 |
+
dq = (donor_cfg.get("quantization") or {})
|
| 81 |
+
for k in ("bits", "group_size", "mode"):
|
| 82 |
+
# Per-tensor overrides are fine; only the base scheme has to agree.
|
| 83 |
+
if rq.get(k) != dq.get(k):
|
| 84 |
+
bad.append(f"quantization.{k}: recipient={rq.get(k)} donor={dq.get(k)}")
|
| 85 |
+
return bad
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def main():
|
| 89 |
+
ap = argparse.ArgumentParser()
|
| 90 |
+
ap.add_argument("--recipient", required=True, help="model missing its MTP head")
|
| 91 |
+
ap.add_argument("--donor", required=True, help="model (or standalone head) that has one")
|
| 92 |
+
ap.add_argument("--out", required=True)
|
| 93 |
+
ap.add_argument("--force", action="store_true", help="graft despite arch mismatch")
|
| 94 |
+
a = ap.parse_args()
|
| 95 |
+
|
| 96 |
+
rc = json.load(open(os.path.join(a.recipient, "config.json")))
|
| 97 |
+
dc = json.load(open(os.path.join(a.donor, "config.json")))
|
| 98 |
+
bad = check(rc, dc)
|
| 99 |
+
if bad:
|
| 100 |
+
print("architecture mismatch:")
|
| 101 |
+
for b in bad:
|
| 102 |
+
print(" " + b)
|
| 103 |
+
if not a.force:
|
| 104 |
+
sys.exit("refusing to graft (use --force to override)")
|
| 105 |
+
|
| 106 |
+
if os.path.exists(a.out):
|
| 107 |
+
shutil.rmtree(a.out)
|
| 108 |
+
os.makedirs(a.out)
|
| 109 |
+
# Hard-link the weights: the graft costs only the MTP shard on disk, and
|
| 110 |
+
# the recipient's files are shared, not copied. Small files are copied so
|
| 111 |
+
# a later in-place config edit cannot corrupt the original -- a real bug
|
| 112 |
+
# we hit with a tool that hard-linked config.json then rewrote it.
|
| 113 |
+
for f in os.listdir(a.recipient):
|
| 114 |
+
s, d = os.path.join(a.recipient, f), os.path.join(a.out, f)
|
| 115 |
+
if not os.path.isfile(s):
|
| 116 |
+
continue
|
| 117 |
+
(os.link if f.endswith(".safetensors") else shutil.copy2)(s, d)
|
| 118 |
+
|
| 119 |
+
mtp = collect_mtp(a.donor)
|
| 120 |
+
shard = "model-mtp-00001-of-00001.safetensors"
|
| 121 |
+
mx.save_safetensors(os.path.join(a.out, shard), mtp, metadata={"format": "mlx"})
|
| 122 |
+
size = os.path.getsize(os.path.join(a.out, shard))
|
| 123 |
+
|
| 124 |
+
ridx = os.path.join(a.recipient, "model.safetensors.index.json")
|
| 125 |
+
if os.path.exists(ridx):
|
| 126 |
+
base = json.load(open(ridx))
|
| 127 |
+
else:
|
| 128 |
+
wm, _ = _load_all(a.recipient)
|
| 129 |
+
base = {"metadata": {"total_size": 0}, "weight_map": wm}
|
| 130 |
+
wm = dict(base["weight_map"])
|
| 131 |
+
for k in mtp:
|
| 132 |
+
wm[k] = shard
|
| 133 |
+
meta = dict(base.get("metadata", {}))
|
| 134 |
+
meta["total_size"] = int(meta.get("total_size", 0)) + size
|
| 135 |
+
json.dump({"metadata": meta, "weight_map": wm},
|
| 136 |
+
open(os.path.join(a.out, "model.safetensors.index.json"), "w"), indent=2)
|
| 137 |
+
|
| 138 |
+
cfg = json.load(open(os.path.join(a.out, "config.json")))
|
| 139 |
+
tgt = cfg.get("text_config", cfg)
|
| 140 |
+
if not (cfg.get("mtp_num_hidden_layers") or tgt.get("mtp_num_hidden_layers")):
|
| 141 |
+
cfg["mtp_num_hidden_layers"] = 1
|
| 142 |
+
json.dump(cfg, open(os.path.join(a.out, "config.json"), "w"), indent=1)
|
| 143 |
+
print(" set mtp_num_hidden_layers = 1")
|
| 144 |
+
|
| 145 |
+
print(f"grafted {len(mtp)} MTP tensors ({size/1e6:.1f} MB) -> {a.out}")
|
| 146 |
+
print(f" tensors: {len(base['weight_map'])} -> {len(wm)}")
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
if __name__ == "__main__":
|
| 150 |
+
main()
|
qwen3.8-27b-mtp-4bit.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:505827ce17922d2d0f274ca6be21d7a35404eab21600da7784612ba7cef27f9f
|
| 3 |
+
size 314300890
|