Instructions to use pipenetwork/DeepSeek-V4-Flash-MLX-REAP25 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use pipenetwork/DeepSeek-V4-Flash-MLX-REAP25 with MLX:
# Make sure mlx-lm is installed # pip install --upgrade mlx-lm # if on a CUDA device, also pip install mlx[cuda] # Generate text with mlx-lm from mlx_lm import load, generate model, tokenizer = load("pipenetwork/DeepSeek-V4-Flash-MLX-REAP25") prompt = "Once upon a time in" text = generate(model, tokenizer, prompt=prompt, verbose=True) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
- MLX LM
How to use pipenetwork/DeepSeek-V4-Flash-MLX-REAP25 with MLX LM:
Generate or start a chat session
# Install MLX LM uv tool install mlx-lm # Generate some text mlx_lm.generate --model "pipenetwork/DeepSeek-V4-Flash-MLX-REAP25" --prompt "Once upon a time"
Add files using upload-large-folder tool
Browse files- README.md +136 -0
- config.json +122 -0
- deepseek_v4_mlx/attention.py +239 -0
- deepseek_v4_mlx/cache.py +142 -0
- deepseek_v4_mlx/compressor.py +154 -0
- deepseek_v4_mlx/config.py +152 -0
- deepseek_v4_mlx/convert.py +218 -0
- deepseek_v4_mlx/dequant.py +92 -0
- deepseek_v4_mlx/fakequant.py +95 -0
- deepseek_v4_mlx/generate.py +37 -0
- deepseek_v4_mlx/hyper_connections.py +118 -0
- deepseek_v4_mlx/indexer.py +113 -0
- deepseek_v4_mlx/layers.py +132 -0
- deepseek_v4_mlx/load.py +68 -0
- deepseek_v4_mlx/model.py +102 -0
- deepseek_v4_mlx/moe.py +140 -0
- deepseek_v4_mlx/sparse_attention.py +71 -0
- generation_config.json +9 -0
- model-00001-of-00025.safetensors +3 -0
- model-00002-of-00025.safetensors +3 -0
- model-00003-of-00025.safetensors +3 -0
- model-00004-of-00025.safetensors +3 -0
- model-00005-of-00025.safetensors +3 -0
- model-00006-of-00025.safetensors +3 -0
- model-00007-of-00025.safetensors +3 -0
- model-00008-of-00025.safetensors +3 -0
- model-00009-of-00025.safetensors +3 -0
- model-00010-of-00025.safetensors +3 -0
- model-00011-of-00025.safetensors +3 -0
- model-00012-of-00025.safetensors +3 -0
- model-00013-of-00025.safetensors +3 -0
- model-00014-of-00025.safetensors +3 -0
- model-00015-of-00025.safetensors +3 -0
- model-00016-of-00025.safetensors +3 -0
- model-00017-of-00025.safetensors +3 -0
- model-00018-of-00025.safetensors +3 -0
- model-00019-of-00025.safetensors +3 -0
- model-00020-of-00025.safetensors +3 -0
- model-00021-of-00025.safetensors +3 -0
- model-00022-of-00025.safetensors +3 -0
- model-00023-of-00025.safetensors +3 -0
- model-00024-of-00025.safetensors +3 -0
- model-00025-of-00025.safetensors +3 -0
- model.safetensors.index.json +0 -0
- tokenizer.json +0 -0
- tokenizer_config.json +34 -0
README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: mit
|
| 3 |
+
base_model: deepseek-ai/DeepSeek-V4-Flash-0731
|
| 4 |
+
base_model_relation: quantized
|
| 5 |
+
pipeline_tag: text-generation
|
| 6 |
+
library_name: mlx
|
| 7 |
+
tags:
|
| 8 |
+
- mlx
|
| 9 |
+
- moe
|
| 10 |
+
- deepseek
|
| 11 |
+
- deepseek-v4
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
# DeepSeek-V4-Flash-MLX-REAP25
|
| 15 |
+
|
| 16 |
+
MLX (Apple Silicon) conversion of [deepseek-ai/DeepSeek-V4-Flash-0731](https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash-0731),
|
| 17 |
+
built from `mixed-4_8bit` (4-bit routed experts, 8-bit elsewhere) with **25% of routed experts pruned** by REAP saliency.
|
| 18 |
+
|
| 19 |
+
**Code / loader:** [github.com/PipeNetwork/deepseek-v4-mlx](https://github.com/PipeNetwork/deepseek-v4-mlx)
|
| 20 |
+
|
| 21 |
+
DeepSeek-V4-Flash is a **304B-parameter** sparse-MoE model. Its `deepseek_v4`
|
| 22 |
+
architecture is **not** implemented in any released runtime — not `transformers`,
|
| 23 |
+
and not `mlx-lm` (which carries `deepseek`, `_v2`, `_v3`, `_v32`). This repo bundles
|
| 24 |
+
a from-scratch MLX port under `deepseek_v4_mlx/`, validated against DeepSeek's own
|
| 25 |
+
reference implementation.
|
| 26 |
+
|
| 27 |
+
## Builds
|
| 28 |
+
|
| 29 |
+
| Variant | Size | wikitext-2 ppl | Notes |
|
| 30 |
+
|---|---:|---:|---|
|
| 31 |
+
| [8bit](https://huggingface.co/pipenetwork/DeepSeek-V4-Flash-MLX-8bit) | ~304 GB | 5.9878 | highest fidelity |
|
| 32 |
+
| [6bit](https://huggingface.co/pipenetwork/DeepSeek-V4-Flash-MLX-6bit) | ~233 GB | 6.0260 | +0.6% vs 8-bit for 70 GB less |
|
| 33 |
+
| [mixed-4_8bit](https://huggingface.co/pipenetwork/DeepSeek-V4-Flash-MLX-mixed-4_8bit) | ~165 GB | 6.1262 | **recommended** — 4-bit experts, 8-bit everything else |
|
| 34 |
+
| [4bit](https://huggingface.co/pipenetwork/DeepSeek-V4-Flash-MLX-4bit) | ~162 GB | 6.3005 | superseded by `mixed-4_8bit` (3 GB more, 2.8% better) |
|
| 35 |
+
| [REAP25](https://huggingface.co/pipenetwork/DeepSeek-V4-Flash-MLX-REAP25) | ~129 GB | 6.4025 | 25% of experts pruned — **matches 4-bit quality 33 GB smaller** |
|
| 36 |
+
| [REAP37](https://huggingface.co/pipenetwork/DeepSeek-V4-Flash-MLX-REAP37) | ~111 GB | 6.9634 | 37% pruned; fits a 128 GB Mac |
|
| 37 |
+
| [REAP50](https://huggingface.co/pipenetwork/DeepSeek-V4-Flash-MLX-REAP50) | ~93 GB | 7.7520 | 50% pruned; smallest build |
|
| 38 |
+
|
| 39 |
+
Perplexity is teacher-forced over **wikitext-2-raw-v1 test**, 199,485 tokens in 195
|
| 40 |
+
windows of 1024 — identical windows for every build. Differences are established by
|
| 41 |
+
a **paired** bootstrap over shared windows, which removes window-difficulty variance:
|
| 42 |
+
independent intervals overlap almost entirely and settle nothing, while the paired
|
| 43 |
+
interval is ~10x tighter. Every gap quoted here is significant at 95%.
|
| 44 |
+
|
| 45 |
+
### Why `mixed-4_8bit` is the recommended build
|
| 46 |
+
|
| 47 |
+
The routed experts are **98.1%** of all quantized parameters, and they ship natively
|
| 48 |
+
as FP4. Keeping them at 4 bits while raising everything else — attention,
|
| 49 |
+
embeddings, shared experts, about 5B parameters — to 8 bits costs **3 GB** and
|
| 50 |
+
recovers **56%** of the gap between 4-bit and 8-bit. Per parameter, the non-expert
|
| 51 |
+
weights are roughly 50x more quantization-sensitive.
|
| 52 |
+
|
| 53 |
+
Two variants were built, measured, and **not** published:
|
| 54 |
+
|
| 55 |
+
* **group-size 32** (matching FP4's native 32-value scale blocks) is 0.5% *worse*
|
| 56 |
+
than group 64 and 18 GB larger. Finer scaling improves raw tensor reconstruction
|
| 57 |
+
but not prediction — the mismatch is that FP4's levels are non-uniform while
|
| 58 |
+
affine's are evenly spaced, which no group size fixes.
|
| 59 |
+
* **3-bit experts** collapse the model entirely (perplexity 1.4e7, degenerate token
|
| 60 |
+
loops). 4-bit sits just above a cliff for this checkpoint, not in the middle of a
|
| 61 |
+
gentle curve.
|
| 62 |
+
|
| 63 |
+
### REAP-pruned builds
|
| 64 |
+
|
| 65 |
+
[REAP (Cerebras, arXiv:2510.13999)](https://arxiv.org/abs/2510.13999) drops the
|
| 66 |
+
lowest-saliency routed experts, where saliency is the mean of
|
| 67 |
+
`router_weight x ||expert_output||` over the tokens routed to that expert — its
|
| 68 |
+
actual contribution, not how often it fires. The router renormalizes over survivors.
|
| 69 |
+
|
| 70 |
+
**Only 40 of the 43 layers are pruned.** The first three route through a fixed
|
| 71 |
+
`tid2eid` token-id table with no router to renormalize, so dropping an expert there
|
| 72 |
+
would leave table entries pointing at nothing. They keep all 256, and the config
|
| 73 |
+
records two expert counts which the loader resolves per layer.
|
| 74 |
+
|
| 75 |
+
Saliency was profiled over **150k tokens** of English prose, ten other languages and
|
| 76 |
+
real code — deliberately disjoint from the evaluation set, verified by 32-gram
|
| 77 |
+
overlap (0 of 4687), so pruning decisions are not fit to the text they are judged on.
|
| 78 |
+
|
| 79 |
+
| Build | Experts kept | Saliency retained | ppl | vs unpruned |
|
| 80 |
+
|---|---:|---:|---:|---:|
|
| 81 |
+
| `mixed-4_8bit` | 256 | 100% | 6.1262 | — |
|
| 82 |
+
| `REAP25` | 192 | 96.2% | 6.4025 | +4.5% |
|
| 83 |
+
| `REAP37` | 161 | 91.5% | 6.9634 | +13.7% |
|
| 84 |
+
| `REAP50` | 128 | 83.9% | 7.7520 | +26.5% |
|
| 85 |
+
|
| 86 |
+
**`REAP25` is the one to take** — its perplexity is statistically indistinguishable
|
| 87 |
+
from plain 4-bit (paired ratio 1.016, CI [0.998, 1.038], crossing 1.0) at 33 GB less.
|
| 88 |
+
|
| 89 |
+
Note how poorly saliency retention predicts damage: 96.2% retained still costs 4.5%,
|
| 90 |
+
and the last 12 points of retention cost 22 points of perplexity. Treat it as a
|
| 91 |
+
screen for whether pruning is viable, not as an estimate of what it will cost.
|
| 92 |
+
|
| 93 |
+
## ⚠️ Loading requires the bundled loader
|
| 94 |
+
|
| 95 |
+
```bash
|
| 96 |
+
pip install mlx mlx-lm transformers
|
| 97 |
+
```
|
| 98 |
+
```python
|
| 99 |
+
from deepseek_v4_mlx.load import load
|
| 100 |
+
from deepseek_v4_mlx.generate import greedy_generate, load_tokenizer
|
| 101 |
+
|
| 102 |
+
model, args = load("/path/to/this/repo")
|
| 103 |
+
tok = load_tokenizer() # DeepSeek-V3's tokenizer — see below
|
| 104 |
+
ids = tok("The capital of France is")["input_ids"]
|
| 105 |
+
print(tok.decode(greedy_generate(model, args, ids, max_new_tokens=32)))
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
**The base repo ships no tokenizer** — only `encoding/encoding_dsv4.py`, which renders
|
| 109 |
+
prompt *strings*. DeepSeek-V3's tokenizer is the correct one: same padded `vocab_size`
|
| 110 |
+
129280 over 128815 real tokens, `bos=0`/`eos=1` matching V4's `generation_config.json`,
|
| 111 |
+
and every V4 special token present.
|
| 112 |
+
|
| 113 |
+
## What this port implements
|
| 114 |
+
|
| 115 |
+
| Mechanism | Notes |
|
| 116 |
+
|---|---|
|
| 117 |
+
| **Hyper-Connections** | the residual stream carries **4 parallel copies**, mixed by a Sinkhorn-normalized combination matrix |
|
| 118 |
+
| **Hash-routed experts** | the first 3 MoE layers pick experts from a `tid2eid[vocab, 6]` table keyed on token id — routing independent of context |
|
| 119 |
+
| **Learned KV compression** | gated pooling over spans, alternating ratio 4/128, overlapping windows at ratio 4 |
|
| 120 |
+
| **Sparse-attention indexer** | scores compressed spans, keeps the top 512 for attention to read |
|
| 121 |
+
| **MLA + grouped output LoRA** | single shared KV vector; `o_groups: 8` block-diagonal output projection |
|
| 122 |
+
| **sqrt-softplus routing**, clamped SwiGLU, attention sinks | |
|
| 123 |
+
|
| 124 |
+
The source checkpoint is **doubly pre-quantized** — FP8 `e4m3` with `e8m0` 128x128
|
| 125 |
+
block scales for most matmuls, FP4 `e2m1` packed two-per-byte for routed experts — so
|
| 126 |
+
conversion dequantizes both formats before requantizing. Both decode paths are
|
| 127 |
+
bit-exact against an independent implementation.
|
| 128 |
+
|
| 129 |
+
Dropped: the multi-token-prediction head and **DSpark** (speculative decode over
|
| 130 |
+
5-token blocks in the last three layers). Both hang off the reference's `forward_spec`
|
| 131 |
+
path and are unused for ordinary generation.
|
| 132 |
+
|
| 133 |
+
`wo_a` is kept unquantized: it is a block-diagonal projection the model reshapes by
|
| 134 |
+
group, and a packed quantized weight cannot be reshaped that way. Costs ~2% of build size.
|
| 135 |
+
|
| 136 |
+
License: MIT (inherits the base model).
|
config.json
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"DeepseekV4ForCausalLM"
|
| 4 |
+
],
|
| 5 |
+
"attention_bias": false,
|
| 6 |
+
"attention_dropout": 0.0,
|
| 7 |
+
"bos_token_id": 0,
|
| 8 |
+
"eos_token_id": 1,
|
| 9 |
+
"hc_eps": 1e-06,
|
| 10 |
+
"hc_mult": 4,
|
| 11 |
+
"hc_sinkhorn_iters": 20,
|
| 12 |
+
"head_dim": 512,
|
| 13 |
+
"hidden_act": "silu",
|
| 14 |
+
"hidden_size": 4096,
|
| 15 |
+
"index_head_dim": 128,
|
| 16 |
+
"index_n_heads": 64,
|
| 17 |
+
"index_topk": 512,
|
| 18 |
+
"initializer_range": 0.02,
|
| 19 |
+
"max_position_embeddings": 1048576,
|
| 20 |
+
"model_type": "deepseek_v4",
|
| 21 |
+
"moe_intermediate_size": 2048,
|
| 22 |
+
"n_routed_experts": 256,
|
| 23 |
+
"n_shared_experts": 1,
|
| 24 |
+
"norm_topk_prob": true,
|
| 25 |
+
"num_attention_heads": 64,
|
| 26 |
+
"num_experts_per_tok": 6,
|
| 27 |
+
"num_hidden_layers": 43,
|
| 28 |
+
"num_hash_layers": 3,
|
| 29 |
+
"num_key_value_heads": 1,
|
| 30 |
+
"num_nextn_predict_layers": 1,
|
| 31 |
+
"o_groups": 8,
|
| 32 |
+
"o_lora_rank": 1024,
|
| 33 |
+
"q_lora_rank": 1024,
|
| 34 |
+
"qk_rope_head_dim": 64,
|
| 35 |
+
"rms_norm_eps": 1e-06,
|
| 36 |
+
"rope_scaling": {
|
| 37 |
+
"beta_fast": 32,
|
| 38 |
+
"beta_slow": 1,
|
| 39 |
+
"factor": 16,
|
| 40 |
+
"original_max_position_embeddings": 65536,
|
| 41 |
+
"type": "yarn"
|
| 42 |
+
},
|
| 43 |
+
"rope_theta": 10000,
|
| 44 |
+
"routed_scaling_factor": 1.5,
|
| 45 |
+
"scoring_func": "sqrtsoftplus",
|
| 46 |
+
"sliding_window": 128,
|
| 47 |
+
"swiglu_limit": 10.0,
|
| 48 |
+
"tie_word_embeddings": false,
|
| 49 |
+
"topk_method": "noaux_tc",
|
| 50 |
+
"torch_dtype": "bfloat16",
|
| 51 |
+
"transformers_version": "4.57.1",
|
| 52 |
+
"use_cache": true,
|
| 53 |
+
"vocab_size": 129280,
|
| 54 |
+
"compress_rope_theta": 160000,
|
| 55 |
+
"compress_ratios": [
|
| 56 |
+
0,
|
| 57 |
+
0,
|
| 58 |
+
4,
|
| 59 |
+
128,
|
| 60 |
+
4,
|
| 61 |
+
128,
|
| 62 |
+
4,
|
| 63 |
+
128,
|
| 64 |
+
4,
|
| 65 |
+
128,
|
| 66 |
+
4,
|
| 67 |
+
128,
|
| 68 |
+
4,
|
| 69 |
+
128,
|
| 70 |
+
4,
|
| 71 |
+
128,
|
| 72 |
+
4,
|
| 73 |
+
128,
|
| 74 |
+
4,
|
| 75 |
+
128,
|
| 76 |
+
4,
|
| 77 |
+
128,
|
| 78 |
+
4,
|
| 79 |
+
128,
|
| 80 |
+
4,
|
| 81 |
+
128,
|
| 82 |
+
4,
|
| 83 |
+
128,
|
| 84 |
+
4,
|
| 85 |
+
128,
|
| 86 |
+
4,
|
| 87 |
+
128,
|
| 88 |
+
4,
|
| 89 |
+
128,
|
| 90 |
+
4,
|
| 91 |
+
128,
|
| 92 |
+
4,
|
| 93 |
+
128,
|
| 94 |
+
4,
|
| 95 |
+
128,
|
| 96 |
+
4,
|
| 97 |
+
128,
|
| 98 |
+
4,
|
| 99 |
+
0,
|
| 100 |
+
0,
|
| 101 |
+
0
|
| 102 |
+
],
|
| 103 |
+
"dspark_block_size": 5,
|
| 104 |
+
"dspark_noise_token_id": 128799,
|
| 105 |
+
"dspark_target_layer_ids": [
|
| 106 |
+
40,
|
| 107 |
+
41,
|
| 108 |
+
42
|
| 109 |
+
],
|
| 110 |
+
"dspark_markov_rank": 256,
|
| 111 |
+
"quantization": {
|
| 112 |
+
"group_size": 64,
|
| 113 |
+
"bits": 8,
|
| 114 |
+
"expert_bits": 4
|
| 115 |
+
},
|
| 116 |
+
"reap": {
|
| 117 |
+
"kept_experts": 192,
|
| 118 |
+
"ratio_pct": 25,
|
| 119 |
+
"hash_layers_keep": 256,
|
| 120 |
+
"scored_layers": 40
|
| 121 |
+
}
|
| 122 |
+
}
|
deepseek_v4_mlx/attention.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""DeepSeek-V4 attention — MLA over a sliding window plus selected compressed spans.
|
| 2 |
+
|
| 3 |
+
Shape of one layer:
|
| 4 |
+
|
| 5 |
+
* queries go through a low-rank bottleneck (``wq_a`` -> ``q_norm`` -> ``wq_b``),
|
| 6 |
+
then get a **weightless** per-head RMS normalization;
|
| 7 |
+
* a single shared KV vector per position (MLA) — key and value are the same tensor;
|
| 8 |
+
* every layer attends to the last ``window_size`` tokens; compression layers also
|
| 9 |
+
attend to a set of pooled spans, chosen by :class:`Indexer` at ratio 4 or taken
|
| 10 |
+
wholesale at ratio 128;
|
| 11 |
+
* the output has the rotary embedding applied **in reverse** before a grouped
|
| 12 |
+
low-rank projection (``wo_a`` per group, then a shared ``wo_b``).
|
| 13 |
+
|
| 14 |
+
Both paths live here. ``__call__`` prefills a whole sequence and, given a cache,
|
| 15 |
+
leaves it in the state decode would have produced; ``step`` decodes one token
|
| 16 |
+
against that cache. They share the q/kv/output projections so the two cannot drift
|
| 17 |
+
apart.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
import mlx.core as mx
|
| 23 |
+
import mlx.nn as nn
|
| 24 |
+
|
| 25 |
+
from .compressor import Compressor
|
| 26 |
+
from .config import ModelArgs
|
| 27 |
+
from .fakequant import fake_quant_kv
|
| 28 |
+
from .indexer import Indexer
|
| 29 |
+
from .layers import RMSNorm, apply_rotary_emb, precompute_freqs_cis
|
| 30 |
+
from .sparse_attention import sparse_attn
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def window_topk_idxs(window_size: int, seqlen: int) -> mx.array:
|
| 34 |
+
"""[s, w] indices of the local window for each query, -1 where out of range."""
|
| 35 |
+
base = mx.arange(seqlen).reshape(-1, 1)
|
| 36 |
+
w = min(seqlen, window_size)
|
| 37 |
+
idx = mx.maximum(base - window_size + 1, 0) + mx.arange(w).reshape(1, -1)
|
| 38 |
+
return mx.where(idx > base, mx.array(-1, mx.int32), idx.astype(mx.int32))
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def compress_topk_idxs(ratio: int, seqlen: int, offset: int) -> mx.array:
|
| 42 |
+
"""[s, s//ratio] indices of every *complete* span, -1 where not yet visible."""
|
| 43 |
+
nb = seqlen // ratio
|
| 44 |
+
m = mx.broadcast_to(mx.arange(nb).reshape(1, -1), (seqlen, nb))
|
| 45 |
+
vis = m < (mx.arange(1, seqlen + 1).reshape(-1, 1) // ratio)
|
| 46 |
+
return mx.where(vis, (m + offset).astype(mx.int32), mx.array(-1, mx.int32))
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class Attention(nn.Module):
|
| 50 |
+
def __init__(self, layer_id: int, args: ModelArgs):
|
| 51 |
+
super().__init__()
|
| 52 |
+
self.layer_id = layer_id
|
| 53 |
+
self.dim = args.dim
|
| 54 |
+
self.n_heads = args.n_heads
|
| 55 |
+
self.head_dim = args.head_dim
|
| 56 |
+
self.rope_head_dim = args.rope_head_dim
|
| 57 |
+
self.q_lora_rank = args.q_lora_rank
|
| 58 |
+
self.o_lora_rank = args.o_lora_rank
|
| 59 |
+
self.n_groups = args.o_groups
|
| 60 |
+
self.window_size = args.window_size
|
| 61 |
+
self.compress_ratio = args.compress_ratio(layer_id)
|
| 62 |
+
self.eps = args.norm_eps
|
| 63 |
+
self.softmax_scale = args.head_dim ** -0.5
|
| 64 |
+
|
| 65 |
+
self.attn_sink = mx.zeros((self.n_heads,), dtype=mx.float32)
|
| 66 |
+
self.wq_a = nn.Linear(self.dim, self.q_lora_rank, bias=False)
|
| 67 |
+
self.q_norm = RMSNorm(self.q_lora_rank, self.eps)
|
| 68 |
+
self.wq_b = nn.Linear(self.q_lora_rank, self.n_heads * self.head_dim, bias=False)
|
| 69 |
+
self.wkv = nn.Linear(self.dim, self.head_dim, bias=False)
|
| 70 |
+
self.kv_norm = RMSNorm(self.head_dim, self.eps)
|
| 71 |
+
self.wo_a = nn.Linear(self.n_heads * self.head_dim // self.n_groups,
|
| 72 |
+
self.n_groups * self.o_lora_rank, bias=False)
|
| 73 |
+
self.wo_b = nn.Linear(self.n_groups * self.o_lora_rank, self.dim, bias=False)
|
| 74 |
+
|
| 75 |
+
if self.compress_ratio:
|
| 76 |
+
self.compressor = Compressor(args, self.compress_ratio, self.head_dim)
|
| 77 |
+
self.indexer = Indexer(args, self.compress_ratio) if self.compress_ratio == 4 else None
|
| 78 |
+
|
| 79 |
+
# compression layers use YaRN on a longer rope base; pure sliding-window
|
| 80 |
+
# layers disable YaRN entirely and use the standard base
|
| 81 |
+
if self.compress_ratio:
|
| 82 |
+
orig_len, theta = args.original_seq_len, args.compress_rope_theta
|
| 83 |
+
else:
|
| 84 |
+
orig_len, theta = 0, args.rope_theta
|
| 85 |
+
self._rope = (args.rope_head_dim, orig_len, theta, args.rope_factor,
|
| 86 |
+
args.beta_fast, args.beta_slow)
|
| 87 |
+
self._cos = None
|
| 88 |
+
self._sin = None
|
| 89 |
+
|
| 90 |
+
def _freqs(self, seqlen: int):
|
| 91 |
+
if self._cos is None or self._cos.shape[0] < seqlen:
|
| 92 |
+
rd, orig_len, theta, factor, bf, bs = self._rope
|
| 93 |
+
self._cos, self._sin = precompute_freqs_cis(rd, seqlen, orig_len, theta,
|
| 94 |
+
factor, bf, bs)
|
| 95 |
+
return self._cos[:seqlen], self._sin[:seqlen]
|
| 96 |
+
|
| 97 |
+
def _project_q(self, x, cos, sin, pos0: int):
|
| 98 |
+
"""Shared query path: low-rank, weightless per-head RMS, rope."""
|
| 99 |
+
bsz, seqlen, _ = x.shape
|
| 100 |
+
rd = self.rope_head_dim
|
| 101 |
+
qr = self.q_norm(self.wq_a(x))
|
| 102 |
+
q = self.wq_b(qr).reshape(bsz, seqlen, self.n_heads, self.head_dim)
|
| 103 |
+
qf = q.astype(mx.float32)
|
| 104 |
+
q = (qf * mx.rsqrt(mx.mean(mx.square(qf), axis=-1, keepdims=True) + self.eps)).astype(q.dtype)
|
| 105 |
+
c, s = cos[pos0:pos0 + seqlen], sin[pos0:pos0 + seqlen]
|
| 106 |
+
q = mx.concatenate([q[..., :-rd], apply_rotary_emb(q[..., -rd:], c, s)], axis=-1)
|
| 107 |
+
return q, qr
|
| 108 |
+
|
| 109 |
+
def _project_kv(self, x, cos, sin, pos0: int):
|
| 110 |
+
"""Shared KV path: single MLA vector, rope on the tail, FP8 on the rest."""
|
| 111 |
+
rd = self.rope_head_dim
|
| 112 |
+
seqlen = x.shape[1]
|
| 113 |
+
kv = self.kv_norm(self.wkv(x))
|
| 114 |
+
c, s = cos[pos0:pos0 + seqlen], sin[pos0:pos0 + seqlen]
|
| 115 |
+
kv = mx.concatenate([kv[..., :-rd], apply_rotary_emb(kv[..., -rd:], c, s)], axis=-1)
|
| 116 |
+
return fake_quant_kv(kv, rd, 64)
|
| 117 |
+
|
| 118 |
+
def _out(self, o, cos, sin, pos0: int, bsz: int, seqlen: int, dtype):
|
| 119 |
+
"""Inverse rope, then the grouped low-rank projection.
|
| 120 |
+
|
| 121 |
+
``dtype`` is the caller's working precision — hardcoding bf16 here silently
|
| 122 |
+
downcasts an fp32 forward.
|
| 123 |
+
"""
|
| 124 |
+
rd = self.rope_head_dim
|
| 125 |
+
c, s = cos[pos0:pos0 + seqlen], sin[pos0:pos0 + seqlen]
|
| 126 |
+
o = mx.concatenate([o[..., :-rd], apply_rotary_emb(o[..., -rd:], c, s, inverse=True)],
|
| 127 |
+
axis=-1)
|
| 128 |
+
o = o.reshape(bsz, seqlen, self.n_groups, -1)
|
| 129 |
+
wo_a = self.wo_a.weight.reshape(self.n_groups, self.o_lora_rank, -1)
|
| 130 |
+
o = mx.einsum("bsgd,grd->bsgr", o.astype(mx.float32), wo_a.astype(mx.float32))
|
| 131 |
+
return self.wo_b(o.reshape(bsz, seqlen, -1).astype(dtype))
|
| 132 |
+
|
| 133 |
+
def step(self, x: mx.array, cache) -> mx.array:
|
| 134 |
+
"""Decode one token against ``cache``. ``x`` is [b, 1, dim]."""
|
| 135 |
+
bsz = x.shape[0]
|
| 136 |
+
pos = cache.offset
|
| 137 |
+
cos, sin = self._freqs(pos + 1)
|
| 138 |
+
|
| 139 |
+
q, qr = self._project_q(x, cos, sin, pos)
|
| 140 |
+
kv = self._project_kv(x, cos, sin, pos)
|
| 141 |
+
cache.put_window(pos, kv[:, 0])
|
| 142 |
+
|
| 143 |
+
idxs = cache.window_idxs(pos)[None, None, :]
|
| 144 |
+
if self.compress_ratio:
|
| 145 |
+
if self.indexer is not None:
|
| 146 |
+
cidx = self.indexer.step(x, qr, cos, sin, pos, cache, self.window_size)
|
| 147 |
+
else:
|
| 148 |
+
ci = cache.compressed_idxs(pos)
|
| 149 |
+
cidx = ci[None, None, :] if ci.size else None
|
| 150 |
+
pooled = self.compressor.step(x, pos, cache.comp, cos, sin)
|
| 151 |
+
if pooled is not None:
|
| 152 |
+
cache.put_compressed(pos // self.compress_ratio, pooled[:, 0])
|
| 153 |
+
if cidx is not None and cidx.shape[-1] > 0:
|
| 154 |
+
idxs = mx.concatenate([mx.broadcast_to(idxs, (bsz, 1, idxs.shape[-1])),
|
| 155 |
+
mx.broadcast_to(cidx, (bsz, 1, cidx.shape[-1]))], axis=-1)
|
| 156 |
+
|
| 157 |
+
idxs = mx.broadcast_to(idxs, (bsz, 1, idxs.shape[-1]))
|
| 158 |
+
o = sparse_attn(q, cache.kv, self.attn_sink, idxs, self.softmax_scale)
|
| 159 |
+
out = self._out(o, cos, sin, pos, bsz, 1, x.dtype)
|
| 160 |
+
cache.offset = pos + 1
|
| 161 |
+
return out
|
| 162 |
+
|
| 163 |
+
def __call__(self, x: mx.array, cache=None) -> mx.array:
|
| 164 |
+
if cache is not None and x.shape[1] == 1 and cache.offset > 0:
|
| 165 |
+
return self.step(x, cache)
|
| 166 |
+
bsz, seqlen, _ = x.shape
|
| 167 |
+
rd = self.rope_head_dim
|
| 168 |
+
cos, sin = self._freqs(seqlen)
|
| 169 |
+
|
| 170 |
+
# --- queries: low-rank, then a weightless per-head RMS ---
|
| 171 |
+
qr = self.q_norm(self.wq_a(x))
|
| 172 |
+
q = self.wq_b(qr).reshape(bsz, seqlen, self.n_heads, self.head_dim)
|
| 173 |
+
qf = q.astype(mx.float32)
|
| 174 |
+
q = (qf * mx.rsqrt(mx.mean(mx.square(qf), axis=-1, keepdims=True) + self.eps)).astype(q.dtype)
|
| 175 |
+
q = mx.concatenate([q[..., :-rd], apply_rotary_emb(q[..., -rd:], cos, sin)], axis=-1)
|
| 176 |
+
|
| 177 |
+
# --- shared KV (MLA) ---
|
| 178 |
+
kv = self.kv_norm(self.wkv(x))
|
| 179 |
+
kv = mx.concatenate([kv[..., :-rd], apply_rotary_emb(kv[..., -rd:], cos, sin)], axis=-1)
|
| 180 |
+
kv = fake_quant_kv(kv, rd, 64)
|
| 181 |
+
|
| 182 |
+
idxs = mx.broadcast_to(window_topk_idxs(self.window_size, seqlen)[None],
|
| 183 |
+
(bsz, seqlen, min(seqlen, self.window_size)))
|
| 184 |
+
|
| 185 |
+
if self.compress_ratio:
|
| 186 |
+
offset = seqlen # compressed entries follow the window KV
|
| 187 |
+
if self.indexer is not None:
|
| 188 |
+
cidx = self.indexer(x, qr, cos, sin, offset)
|
| 189 |
+
else:
|
| 190 |
+
cidx = mx.broadcast_to(
|
| 191 |
+
compress_topk_idxs(self.compress_ratio, seqlen, offset)[None],
|
| 192 |
+
(bsz, seqlen, seqlen // self.compress_ratio))
|
| 193 |
+
if cidx is not None:
|
| 194 |
+
idxs = mx.concatenate([idxs, cidx], axis=-1)
|
| 195 |
+
kv_c = self.compressor(x, cos, sin)
|
| 196 |
+
if kv_c is not None:
|
| 197 |
+
kv = mx.concatenate([kv, kv_c], axis=1)
|
| 198 |
+
|
| 199 |
+
o = sparse_attn(q, kv, self.attn_sink, idxs, self.softmax_scale)
|
| 200 |
+
|
| 201 |
+
if cache is not None:
|
| 202 |
+
self._seed_cache(cache, x, kv, seqlen, cos, sin)
|
| 203 |
+
|
| 204 |
+
return self._out(o, cos, sin, 0, bsz, seqlen, x.dtype)
|
| 205 |
+
|
| 206 |
+
def _seed_cache(self, cache, x, kv, seqlen, cos, sin):
|
| 207 |
+
"""Leave the cache in exactly the state decode would have produced.
|
| 208 |
+
|
| 209 |
+
Prefill pools spans in one shot, so the compressors' rolling state has to be
|
| 210 |
+
replayed. Replay starts at the **last complete span**, not merely at the
|
| 211 |
+
remainder: for overlapping (ratio-4) compressors, closing that span is what
|
| 212 |
+
moves it into the "previous span" half of the state, which the next pooling
|
| 213 |
+
reads. Starting at the remainder would leave that half empty and quietly
|
| 214 |
+
change the first few decoded tokens.
|
| 215 |
+
|
| 216 |
+
Slots beyond the replayed remainder keep stale values, which is harmless —
|
| 217 |
+
a span only closes once every slot in the current half has been rewritten.
|
| 218 |
+
"""
|
| 219 |
+
cache.put_window_prefill(kv[:, :seqlen])
|
| 220 |
+
if not self.compress_ratio:
|
| 221 |
+
cache.offset = seqlen
|
| 222 |
+
return
|
| 223 |
+
|
| 224 |
+
ratio = self.compress_ratio
|
| 225 |
+
n_span = seqlen // ratio
|
| 226 |
+
if n_span:
|
| 227 |
+
cache.put_compressed_prefill(kv[:, seqlen:seqlen + n_span])
|
| 228 |
+
if self.indexer is not None:
|
| 229 |
+
idx_kv = self.indexer.compressor(x, cos, sin)
|
| 230 |
+
if idx_kv is not None:
|
| 231 |
+
cache._adopt(idx_kv.dtype)
|
| 232 |
+
cache.idx_kv[:, :idx_kv.shape[1]] = idx_kv
|
| 233 |
+
|
| 234 |
+
start = max(0, (n_span - 1) * ratio)
|
| 235 |
+
for p in range(start, seqlen):
|
| 236 |
+
self.compressor.step(x[:, p:p + 1], p, cache.comp, cos, sin)
|
| 237 |
+
if self.indexer is not None:
|
| 238 |
+
self.indexer.compressor.step(x[:, p:p + 1], p, cache.idx_comp, cos, sin)
|
| 239 |
+
cache.offset = seqlen
|
deepseek_v4_mlx/cache.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Incremental decode state for DeepSeek-V4.
|
| 2 |
+
|
| 3 |
+
Three things persist between tokens, and they have different shapes:
|
| 4 |
+
|
| 5 |
+
* **the sliding window** — a *circular* buffer of ``window_size`` KV entries.
|
| 6 |
+
Position ``p`` lives at slot ``p % window_size``, so reading it back in
|
| 7 |
+
chronological order means rotating the slot list, not slicing it.
|
| 8 |
+
* **compressed spans** — appended after the window in the same buffer, one entry
|
| 9 |
+
per completed span at slot ``window_size + p // ratio``.
|
| 10 |
+
* **the compressor's partial span** — the tokens seen since the last span closed,
|
| 11 |
+
held as running ``kv``/``score`` state. Ratio-4 layers keep two spans' worth,
|
| 12 |
+
because their pooling overlaps the previous span.
|
| 13 |
+
|
| 14 |
+
The indexer keeps its own compressor state and its own compressed-KV buffer,
|
| 15 |
+
independent of attention's.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
import mlx.core as mx
|
| 21 |
+
|
| 22 |
+
NEG_INF = -1e30
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class CompressorCache:
|
| 26 |
+
"""Rolling state for one compressor's in-progress span."""
|
| 27 |
+
|
| 28 |
+
def __init__(self, bsz: int, ratio: int, head_dim: int, overlap: bool):
|
| 29 |
+
coff = 1 + overlap
|
| 30 |
+
self.ratio = ratio
|
| 31 |
+
self.overlap = overlap
|
| 32 |
+
self.head_dim = head_dim
|
| 33 |
+
self.kv_state = mx.zeros((bsz, coff * ratio, coff * head_dim), dtype=mx.float32)
|
| 34 |
+
self.score_state = mx.full((bsz, coff * ratio, coff * head_dim), NEG_INF,
|
| 35 |
+
dtype=mx.float32)
|
| 36 |
+
|
| 37 |
+
def write(self, slot: int, kv: mx.array, score: mx.array):
|
| 38 |
+
"""Record one token's contribution at ``slot``."""
|
| 39 |
+
self.kv_state[:, slot] = kv
|
| 40 |
+
self.score_state[:, slot] = score
|
| 41 |
+
|
| 42 |
+
def shift(self):
|
| 43 |
+
"""After closing an overlapping span, the current half becomes the previous."""
|
| 44 |
+
r = self.ratio
|
| 45 |
+
self.kv_state[:, :r] = self.kv_state[:, r:]
|
| 46 |
+
self.score_state[:, :r] = self.score_state[:, r:]
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class LayerCache:
|
| 50 |
+
"""Per-layer decode state: window + compressed KV, plus compressor state."""
|
| 51 |
+
|
| 52 |
+
def __init__(self, bsz: int, args, layer_id: int, max_seq_len: int,
|
| 53 |
+
dtype=mx.bfloat16):
|
| 54 |
+
self.window = args.window_size
|
| 55 |
+
self.ratio = args.compress_ratio(layer_id)
|
| 56 |
+
self.offset = 0
|
| 57 |
+
self.dtype = dtype
|
| 58 |
+
|
| 59 |
+
n_comp = (max_seq_len // self.ratio) if self.ratio else 0
|
| 60 |
+
self.kv = mx.zeros((bsz, self.window + n_comp, args.head_dim), dtype=dtype)
|
| 61 |
+
|
| 62 |
+
if self.ratio:
|
| 63 |
+
self.comp = CompressorCache(bsz, self.ratio, args.head_dim, self.ratio == 4)
|
| 64 |
+
else:
|
| 65 |
+
self.comp = None
|
| 66 |
+
|
| 67 |
+
if self.ratio == 4:
|
| 68 |
+
self.idx_comp = CompressorCache(bsz, self.ratio, args.index_head_dim, True)
|
| 69 |
+
self.idx_kv = mx.zeros((bsz, n_comp, args.index_head_dim), dtype=dtype)
|
| 70 |
+
else:
|
| 71 |
+
self.idx_comp = None
|
| 72 |
+
self.idx_kv = None
|
| 73 |
+
|
| 74 |
+
def _adopt(self, dtype):
|
| 75 |
+
"""Match the precision actually flowing through the model.
|
| 76 |
+
|
| 77 |
+
Allocating bf16 while the forward runs in fp32 truncates every cached KV on
|
| 78 |
+
write. The absolute error is small, but it flips discrete decisions
|
| 79 |
+
downstream — expert top-k and indexer span selection — so decode drifts away
|
| 80 |
+
from full recomputation far faster than the rounding alone suggests.
|
| 81 |
+
"""
|
| 82 |
+
if dtype == self.dtype:
|
| 83 |
+
return
|
| 84 |
+
self.dtype = dtype
|
| 85 |
+
self.kv = self.kv.astype(dtype)
|
| 86 |
+
if self.idx_kv is not None:
|
| 87 |
+
self.idx_kv = self.idx_kv.astype(dtype)
|
| 88 |
+
|
| 89 |
+
# ---- window ----
|
| 90 |
+
|
| 91 |
+
def put_window(self, pos: int, kv: mx.array):
|
| 92 |
+
self._adopt(kv.dtype)
|
| 93 |
+
self.kv[:, pos % self.window] = kv
|
| 94 |
+
|
| 95 |
+
def put_window_prefill(self, kv: mx.array):
|
| 96 |
+
"""Seed the circular buffer from a prefill of ``s`` tokens.
|
| 97 |
+
|
| 98 |
+
Keeps the last ``window`` entries, placed so that position ``p`` sits at
|
| 99 |
+
slot ``p % window`` — the same invariant decode maintains.
|
| 100 |
+
"""
|
| 101 |
+
self._adopt(kv.dtype)
|
| 102 |
+
s = kv.shape[1]
|
| 103 |
+
if s <= self.window:
|
| 104 |
+
self.kv[:, :s] = kv
|
| 105 |
+
return
|
| 106 |
+
tail = kv[:, -self.window:] # positions s-window .. s-1
|
| 107 |
+
cut = s % self.window # where position s-1 lands + 1
|
| 108 |
+
self.kv[:, cut:self.window] = tail[:, :self.window - cut]
|
| 109 |
+
self.kv[:, :cut] = tail[:, self.window - cut:]
|
| 110 |
+
|
| 111 |
+
def put_compressed(self, span: int, kv: mx.array):
|
| 112 |
+
self._adopt(kv.dtype)
|
| 113 |
+
self.kv[:, self.window + span] = kv
|
| 114 |
+
|
| 115 |
+
def put_compressed_prefill(self, kv: mx.array):
|
| 116 |
+
self._adopt(kv.dtype)
|
| 117 |
+
n = kv.shape[1]
|
| 118 |
+
self.kv[:, self.window:self.window + n] = kv
|
| 119 |
+
|
| 120 |
+
# ---- index lists for one decoding step ----
|
| 121 |
+
|
| 122 |
+
def window_idxs(self, pos: int) -> mx.array:
|
| 123 |
+
"""Window slots in chronological order for a query at ``pos``."""
|
| 124 |
+
w = self.window
|
| 125 |
+
if pos >= w - 1:
|
| 126 |
+
p = pos % w
|
| 127 |
+
return mx.concatenate([mx.arange(p + 1, w), mx.arange(0, p + 1)]).astype(mx.int32)
|
| 128 |
+
return mx.concatenate([mx.arange(pos + 1),
|
| 129 |
+
mx.full((w - pos - 1,), -1)]).astype(mx.int32)
|
| 130 |
+
|
| 131 |
+
def compressed_idxs(self, pos: int) -> mx.array:
|
| 132 |
+
"""Every span completed by ``pos``, addressed in the shared buffer."""
|
| 133 |
+
n = (pos + 1) // self.ratio
|
| 134 |
+
if n <= 0:
|
| 135 |
+
return mx.zeros((0,), dtype=mx.int32)
|
| 136 |
+
return (mx.arange(n) + self.window).astype(mx.int32)
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def make_cache(args, bsz: int = 1, max_seq_len: int | None = None):
|
| 140 |
+
"""One :class:`LayerCache` per layer."""
|
| 141 |
+
max_seq_len = max_seq_len or 4096
|
| 142 |
+
return [LayerCache(bsz, args, i, max_seq_len) for i in range(args.n_layers)]
|
deepseek_v4_mlx/compressor.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Learned KV compression — V4 pools spans of tokens into single cache entries.
|
| 2 |
+
|
| 3 |
+
Instead of caching every position, compression layers pool ``compress_ratio``
|
| 4 |
+
consecutive tokens into one entry using a *learned gate*: a projection scores each
|
| 5 |
+
position, the scores are softmaxed across the span, and the KV vectors are summed
|
| 6 |
+
under those weights. A per-slot positional embedding (``ape``) lets the gate know
|
| 7 |
+
where inside the span a token sat.
|
| 8 |
+
|
| 9 |
+
Ratio-4 layers additionally **overlap**: each pooled entry sees 2x ratio positions
|
| 10 |
+
— the current span plus the preceding one — so information is not lost at span
|
| 11 |
+
boundaries. That is what the doubled ``wkv`` / ``wgate`` output width is for: the
|
| 12 |
+
first ``head_dim`` channels carry the overlapping (previous-span) contribution and
|
| 13 |
+
the second ``head_dim`` the current one.
|
| 14 |
+
|
| 15 |
+
Only the prefill path (``start_pos == 0``) lives here. Incremental decode
|
| 16 |
+
maintains rolling state buffers and is added alongside the attention cache.
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
from __future__ import annotations
|
| 20 |
+
|
| 21 |
+
import mlx.core as mx
|
| 22 |
+
import mlx.nn as nn
|
| 23 |
+
|
| 24 |
+
from .config import ModelArgs
|
| 25 |
+
from .fakequant import fake_quant_fp4, fake_quant_kv
|
| 26 |
+
from .layers import RMSNorm, apply_rotary_emb, hadamard_transform
|
| 27 |
+
|
| 28 |
+
NEG_INF = -1e30
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class Compressor(nn.Module):
|
| 32 |
+
"""Pools ``compress_ratio`` tokens into one KV entry via a learned softmax gate."""
|
| 33 |
+
|
| 34 |
+
def __init__(self, args: ModelArgs, compress_ratio: int = 4,
|
| 35 |
+
head_dim: int = 512, rotate: bool = False):
|
| 36 |
+
super().__init__()
|
| 37 |
+
self.dim = args.dim
|
| 38 |
+
self.head_dim = head_dim
|
| 39 |
+
self.rope_head_dim = args.rope_head_dim
|
| 40 |
+
self.compress_ratio = compress_ratio
|
| 41 |
+
self.overlap = compress_ratio == 4
|
| 42 |
+
self.rotate = rotate
|
| 43 |
+
coff = 1 + self.overlap
|
| 44 |
+
|
| 45 |
+
self.ape = mx.zeros((compress_ratio, coff * head_dim), dtype=mx.float32)
|
| 46 |
+
self.wkv = nn.Linear(self.dim, coff * head_dim, bias=False)
|
| 47 |
+
self.wgate = nn.Linear(self.dim, coff * head_dim, bias=False)
|
| 48 |
+
self.norm = RMSNorm(head_dim, args.norm_eps)
|
| 49 |
+
|
| 50 |
+
def _overlap_transform(self, t: mx.array, value: float) -> mx.array:
|
| 51 |
+
"""[b, n, ratio, 2d] -> [b, n, 2*ratio, d].
|
| 52 |
+
|
| 53 |
+
Slots ``[ratio:]`` take the *second* half of the channels from this span;
|
| 54 |
+
slots ``[:ratio]`` take the *first* half from the **previous** span. The
|
| 55 |
+
first span has no predecessor, so its leading slots stay at ``value``
|
| 56 |
+
(0 for KV, -inf for scores, so the softmax ignores them).
|
| 57 |
+
"""
|
| 58 |
+
b, n, r, _ = t.shape
|
| 59 |
+
d = self.head_dim
|
| 60 |
+
cur = t[..., d:] # [b, n, ratio, d]
|
| 61 |
+
prev = t[..., :d] # [b, n, ratio, d]
|
| 62 |
+
pad = mx.full((b, 1, r, d), value, dtype=t.dtype)
|
| 63 |
+
prev_shifted = mx.concatenate([pad, prev[:, :-1]], axis=1)
|
| 64 |
+
return mx.concatenate([prev_shifted, cur], axis=2) # [b, n, 2*ratio, d]
|
| 65 |
+
|
| 66 |
+
def __call__(self, x: mx.array, cos: mx.array, sin: mx.array):
|
| 67 |
+
"""Prefill: compress ``x`` [b, s, dim] into [b, s//ratio, head_dim].
|
| 68 |
+
|
| 69 |
+
Returns ``None`` when the sequence is shorter than one full span, matching
|
| 70 |
+
the reference's ``should_compress`` short-circuit.
|
| 71 |
+
"""
|
| 72 |
+
bsz, seqlen, _ = x.shape
|
| 73 |
+
ratio, d, rd = self.compress_ratio, self.head_dim, self.rope_head_dim
|
| 74 |
+
dtype = x.dtype
|
| 75 |
+
|
| 76 |
+
if seqlen < ratio:
|
| 77 |
+
return None
|
| 78 |
+
|
| 79 |
+
xf = x.astype(mx.float32)
|
| 80 |
+
kv = self.wkv(xf)
|
| 81 |
+
score = self.wgate(xf)
|
| 82 |
+
|
| 83 |
+
remainder = seqlen % ratio
|
| 84 |
+
cutoff = seqlen - remainder
|
| 85 |
+
if remainder:
|
| 86 |
+
kv = kv[:, :cutoff]
|
| 87 |
+
score = score[:, :cutoff]
|
| 88 |
+
|
| 89 |
+
nb = cutoff // ratio
|
| 90 |
+
kv = kv.reshape(bsz, nb, ratio, -1)
|
| 91 |
+
score = score.reshape(bsz, nb, ratio, -1) + self.ape
|
| 92 |
+
|
| 93 |
+
if self.overlap:
|
| 94 |
+
kv = self._overlap_transform(kv, 0.0)
|
| 95 |
+
score = self._overlap_transform(score, NEG_INF)
|
| 96 |
+
|
| 97 |
+
kv = mx.sum(kv * mx.softmax(score, axis=2), axis=2) # [b, nb, d]
|
| 98 |
+
kv = self.norm(kv.astype(dtype))
|
| 99 |
+
|
| 100 |
+
# one rope position per pooled span, taken at the span's first token
|
| 101 |
+
c = cos[:cutoff:ratio]
|
| 102 |
+
s = sin[:cutoff:ratio]
|
| 103 |
+
rot = apply_rotary_emb(kv[..., -rd:], c, s)
|
| 104 |
+
kv = mx.concatenate([kv[..., :-rd], rot], axis=-1)
|
| 105 |
+
|
| 106 |
+
return self._finish(kv, rd)
|
| 107 |
+
|
| 108 |
+
def _finish(self, kv: mx.array, rd: int) -> mx.array:
|
| 109 |
+
# QAT simulation: the indexer's compressor rotates then fakes FP4 over the
|
| 110 |
+
# whole vector; the attention compressor fakes FP8 on the non-rope half only.
|
| 111 |
+
if self.rotate:
|
| 112 |
+
kv = hadamard_transform(kv)
|
| 113 |
+
return fake_quant_fp4(kv, 32)
|
| 114 |
+
return fake_quant_kv(kv, rd, 64)
|
| 115 |
+
|
| 116 |
+
def step(self, x: mx.array, pos: int, cache, cos: mx.array, sin: mx.array):
|
| 117 |
+
"""Decode one token. Returns the pooled entry when ``pos`` closes a span.
|
| 118 |
+
|
| 119 |
+
``x`` is [b, 1, dim]. Between span boundaries this only accumulates state
|
| 120 |
+
and returns ``None`` — the caller writes nothing to the KV buffer.
|
| 121 |
+
"""
|
| 122 |
+
ratio, d, rd = self.compress_ratio, self.head_dim, self.rope_head_dim
|
| 123 |
+
dtype = x.dtype
|
| 124 |
+
xf = x.astype(mx.float32)
|
| 125 |
+
kv = self.wkv(xf)[:, 0] # [b, coff*d]
|
| 126 |
+
score = self.wgate(xf)[:, 0] + self.ape[pos % ratio]
|
| 127 |
+
|
| 128 |
+
if self.overlap:
|
| 129 |
+
cache.write(ratio + pos % ratio, kv, score)
|
| 130 |
+
else:
|
| 131 |
+
cache.write(pos % ratio, kv, score)
|
| 132 |
+
|
| 133 |
+
if (pos + 1) % ratio != 0:
|
| 134 |
+
return None
|
| 135 |
+
|
| 136 |
+
if self.overlap:
|
| 137 |
+
# first half of the channels from the previous span, second half from this
|
| 138 |
+
kv_s = mx.concatenate([cache.kv_state[:, :ratio, :d],
|
| 139 |
+
cache.kv_state[:, ratio:, d:]], axis=1)
|
| 140 |
+
sc_s = mx.concatenate([cache.score_state[:, :ratio, :d],
|
| 141 |
+
cache.score_state[:, ratio:, d:]], axis=1)
|
| 142 |
+
else:
|
| 143 |
+
kv_s, sc_s = cache.kv_state, cache.score_state
|
| 144 |
+
|
| 145 |
+
pooled = mx.sum(kv_s * mx.softmax(sc_s, axis=1), axis=1, keepdims=True)
|
| 146 |
+
if self.overlap:
|
| 147 |
+
cache.shift()
|
| 148 |
+
|
| 149 |
+
pooled = self.norm(pooled.astype(dtype))
|
| 150 |
+
# the span's rope position is its first token
|
| 151 |
+
p = pos + 1 - ratio
|
| 152 |
+
rot = apply_rotary_emb(pooled[..., -rd:], cos[p:p + 1], sin[p:p + 1])
|
| 153 |
+
pooled = mx.concatenate([pooled[..., :-rd], rot], axis=-1)
|
| 154 |
+
return self._finish(pooled, rd)
|
deepseek_v4_mlx/config.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Configuration for DeepSeek-V4 (MLX port).
|
| 2 |
+
|
| 3 |
+
Parses the HuggingFace ``config.json`` layout published with
|
| 4 |
+
``deepseek-ai/DeepSeek-V4-Flash-0731``. The reference ``inference/config.json``
|
| 5 |
+
uses different key names for the same values (``dim`` vs ``hidden_size``,
|
| 6 |
+
``n_layers`` vs ``num_hidden_layers``, ...), so both spellings are accepted —
|
| 7 |
+
handy for running the reference and this port off one file during parity work.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
from dataclasses import dataclass, field
|
| 13 |
+
from typing import Any
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _get(d: dict, *names, default=None):
|
| 17 |
+
for n in names:
|
| 18 |
+
if n in d and d[n] is not None:
|
| 19 |
+
return d[n]
|
| 20 |
+
return default
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
@dataclass
|
| 24 |
+
class ModelArgs:
|
| 25 |
+
vocab_size: int = 129280
|
| 26 |
+
dim: int = 4096
|
| 27 |
+
n_layers: int = 43
|
| 28 |
+
n_hash_layers: int = 3
|
| 29 |
+
moe_inter_dim: int = 2048
|
| 30 |
+
|
| 31 |
+
# attention (MLA)
|
| 32 |
+
n_heads: int = 64
|
| 33 |
+
head_dim: int = 512
|
| 34 |
+
rope_head_dim: int = 64
|
| 35 |
+
q_lora_rank: int = 1024
|
| 36 |
+
o_lora_rank: int = 1024
|
| 37 |
+
o_groups: int = 8
|
| 38 |
+
window_size: int = 128
|
| 39 |
+
norm_eps: float = 1e-6
|
| 40 |
+
|
| 41 |
+
# MoE
|
| 42 |
+
n_routed_experts: int = 256
|
| 43 |
+
n_shared_experts: int = 1
|
| 44 |
+
n_activated_experts: int = 6
|
| 45 |
+
score_func: str = "sqrtsoftplus"
|
| 46 |
+
route_scale: float = 1.5
|
| 47 |
+
swiglu_limit: float = 10.0
|
| 48 |
+
|
| 49 |
+
# KV compression: one ratio per layer (0 = none, pure sliding window)
|
| 50 |
+
compress_ratios: tuple = ()
|
| 51 |
+
compress_rope_theta: float = 160000.0
|
| 52 |
+
|
| 53 |
+
# rope / YaRN
|
| 54 |
+
original_seq_len: int = 65536
|
| 55 |
+
rope_theta: float = 10000.0
|
| 56 |
+
rope_factor: float = 16.0
|
| 57 |
+
beta_fast: int = 32
|
| 58 |
+
beta_slow: int = 1
|
| 59 |
+
max_seq_len: int = 1048576
|
| 60 |
+
|
| 61 |
+
# sparse-attention indexer
|
| 62 |
+
index_n_heads: int = 64
|
| 63 |
+
index_head_dim: int = 128
|
| 64 |
+
index_topk: int = 512
|
| 65 |
+
|
| 66 |
+
# hyper-connections
|
| 67 |
+
hc_mult: int = 4
|
| 68 |
+
hc_sinkhorn_iters: int = 20
|
| 69 |
+
hc_eps: float = 1e-6
|
| 70 |
+
|
| 71 |
+
# REAP: experts kept in the *scored* layers (None = unpruned)
|
| 72 |
+
reap_kept_experts: int | None = None
|
| 73 |
+
|
| 74 |
+
# side-paths dropped for inference (kept for provenance)
|
| 75 |
+
n_mtp_layers: int = 0
|
| 76 |
+
dspark_block_size: int = 0
|
| 77 |
+
dspark_target_layer_ids: tuple = ()
|
| 78 |
+
|
| 79 |
+
@property
|
| 80 |
+
def nope_head_dim(self) -> int:
|
| 81 |
+
return self.head_dim - self.rope_head_dim
|
| 82 |
+
|
| 83 |
+
def compress_ratio(self, layer_id: int) -> int:
|
| 84 |
+
"""Compression ratio for a layer; 0 means pure sliding-window attention."""
|
| 85 |
+
if layer_id < len(self.compress_ratios):
|
| 86 |
+
return int(self.compress_ratios[layer_id])
|
| 87 |
+
return 0
|
| 88 |
+
|
| 89 |
+
def is_hash_layer(self, layer_id: int) -> bool:
|
| 90 |
+
"""First ``n_hash_layers`` route experts by token-id lookup, not by score."""
|
| 91 |
+
return layer_id < self.n_hash_layers
|
| 92 |
+
|
| 93 |
+
def n_experts_for(self, layer_id: int) -> int:
|
| 94 |
+
"""Routed-expert count for a layer — REAP prunes only the scored layers.
|
| 95 |
+
|
| 96 |
+
Hash layers index a fixed ``tid2eid`` table, so dropping an expert there
|
| 97 |
+
would leave table entries pointing at nothing. They keep the full set, and
|
| 98 |
+
a pruned checkpoint therefore has *two* expert counts. Storing one global
|
| 99 |
+
``n_routed_experts`` would silently build the hash layers too small and
|
| 100 |
+
load garbage into them.
|
| 101 |
+
"""
|
| 102 |
+
if self.reap_kept_experts and not self.is_hash_layer(layer_id):
|
| 103 |
+
return self.reap_kept_experts
|
| 104 |
+
return self.n_routed_experts
|
| 105 |
+
|
| 106 |
+
@classmethod
|
| 107 |
+
def from_dict(cls, c: dict) -> "ModelArgs":
|
| 108 |
+
rope = _get(c, "rope_scaling", default={}) or {}
|
| 109 |
+
return cls(
|
| 110 |
+
vocab_size=_get(c, "vocab_size", default=129280),
|
| 111 |
+
dim=_get(c, "hidden_size", "dim", default=4096),
|
| 112 |
+
n_layers=_get(c, "num_hidden_layers", "n_layers", default=43),
|
| 113 |
+
n_hash_layers=_get(c, "num_hash_layers", "n_hash_layers", default=0),
|
| 114 |
+
moe_inter_dim=_get(c, "moe_intermediate_size", "moe_inter_dim", default=2048),
|
| 115 |
+
n_heads=_get(c, "num_attention_heads", "n_heads", default=64),
|
| 116 |
+
head_dim=_get(c, "head_dim", default=512),
|
| 117 |
+
rope_head_dim=_get(c, "qk_rope_head_dim", "rope_head_dim", default=64),
|
| 118 |
+
q_lora_rank=_get(c, "q_lora_rank", default=1024),
|
| 119 |
+
o_lora_rank=_get(c, "o_lora_rank", default=1024),
|
| 120 |
+
o_groups=_get(c, "o_groups", default=8),
|
| 121 |
+
window_size=_get(c, "sliding_window", "window_size", default=128),
|
| 122 |
+
norm_eps=_get(c, "rms_norm_eps", "norm_eps", default=1e-6),
|
| 123 |
+
n_routed_experts=_get(c, "n_routed_experts", default=256),
|
| 124 |
+
n_shared_experts=_get(c, "n_shared_experts", default=1),
|
| 125 |
+
n_activated_experts=_get(c, "num_experts_per_tok", "n_activated_experts", default=6),
|
| 126 |
+
score_func=_get(c, "scoring_func", "score_func", default="sqrtsoftplus"),
|
| 127 |
+
route_scale=_get(c, "routed_scaling_factor", "route_scale", default=1.5),
|
| 128 |
+
swiglu_limit=_get(c, "swiglu_limit", default=0.0),
|
| 129 |
+
compress_ratios=tuple(_get(c, "compress_ratios", default=()) or ()),
|
| 130 |
+
compress_rope_theta=_get(c, "compress_rope_theta", default=160000.0),
|
| 131 |
+
original_seq_len=_get(rope, "original_max_position_embeddings",
|
| 132 |
+
default=_get(c, "original_seq_len", default=65536)),
|
| 133 |
+
rope_theta=_get(c, "rope_theta", default=10000.0),
|
| 134 |
+
rope_factor=_get(rope, "factor", default=_get(c, "rope_factor", default=16.0)),
|
| 135 |
+
beta_fast=_get(rope, "beta_fast", default=_get(c, "beta_fast", default=32)),
|
| 136 |
+
beta_slow=_get(rope, "beta_slow", default=_get(c, "beta_slow", default=1)),
|
| 137 |
+
max_seq_len=_get(c, "max_position_embeddings", "max_seq_len", default=1048576),
|
| 138 |
+
index_n_heads=_get(c, "index_n_heads", default=64),
|
| 139 |
+
index_head_dim=_get(c, "index_head_dim", default=128),
|
| 140 |
+
index_topk=_get(c, "index_topk", default=512),
|
| 141 |
+
hc_mult=_get(c, "hc_mult", default=4),
|
| 142 |
+
hc_sinkhorn_iters=_get(c, "hc_sinkhorn_iters", default=20),
|
| 143 |
+
hc_eps=_get(c, "hc_eps", default=1e-6),
|
| 144 |
+
reap_kept_experts=(c.get("reap") or {}).get("kept_experts"),
|
| 145 |
+
n_mtp_layers=_get(c, "num_nextn_predict_layers", "n_mtp_layers", default=0),
|
| 146 |
+
dspark_block_size=_get(c, "dspark_block_size", default=0),
|
| 147 |
+
dspark_target_layer_ids=tuple(_get(c, "dspark_target_layer_ids", default=()) or ()),
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
@property
|
| 151 |
+
def raw(self) -> dict[str, Any]:
|
| 152 |
+
return {"model_type": "deepseek_v4"}
|
deepseek_v4_mlx/convert.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Streaming DeepSeek-V4 checkpoint -> MLX, with optional affine quantization.
|
| 2 |
+
|
| 3 |
+
The published weights are FP8/FP4 (see :mod:`.dequant`), so conversion is
|
| 4 |
+
dequantize-then-requantize rather than a straight cast. It runs one source shard
|
| 5 |
+
at a time and the layout cooperates: no shard spans more than one layer, so a
|
| 6 |
+
layer's 256 experts are always resident together and can be stacked without
|
| 7 |
+
buffering the model.
|
| 8 |
+
|
| 9 |
+
Three transforms are applied:
|
| 10 |
+
|
| 11 |
+
* **dequantize** — every ``(weight, scale)`` pair to bf16; FP4 for routed experts,
|
| 12 |
+
FP8 for everything else;
|
| 13 |
+
* **stack experts** — the checkpoint stores ``ffn.experts.{0..255}.w{1,2,3}``
|
| 14 |
+
separately; MLX's ``SwitchGLU`` wants one ``[n_experts, out, in]`` tensor per
|
| 15 |
+
projection (``w1 -> gate_proj``, ``w3 -> up_proj``, ``w2 -> down_proj``);
|
| 16 |
+
* **drop the side-paths** — ``mtp.*`` covers both the multi-token-prediction head
|
| 17 |
+
and DSpark, neither of which is used for ordinary generation.
|
| 18 |
+
|
| 19 |
+
``bits=None`` writes bf16; ``bits in {2,3,4,5,6,8}`` affine-quantizes the matmul
|
| 20 |
+
weights, leaving norms, the router, ``tid2eid``, the compressor projections and the
|
| 21 |
+
Hyper-Connection parameters in their original precision.
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
from __future__ import annotations
|
| 25 |
+
|
| 26 |
+
import glob
|
| 27 |
+
import json
|
| 28 |
+
import os
|
| 29 |
+
import re
|
| 30 |
+
import shutil
|
| 31 |
+
|
| 32 |
+
import mlx.core as mx
|
| 33 |
+
|
| 34 |
+
from .dequant import dequant_pair, is_fp4_expert
|
| 35 |
+
|
| 36 |
+
_EXPERT_RE = re.compile(r"^(layers\.\d+\.ffn)\.experts\.(\d+)\.(w[123])$")
|
| 37 |
+
_PROJ = {"w1": "gate_proj", "w3": "up_proj", "w2": "down_proj"}
|
| 38 |
+
|
| 39 |
+
_SHARD_CAP_BYTES = 5_000_000_000
|
| 40 |
+
|
| 41 |
+
# Leaves that get affine-quantized; everything else keeps its source precision.
|
| 42 |
+
#
|
| 43 |
+
# `wo_a` is deliberately excluded. It is a *block-diagonal* projection — input
|
| 44 |
+
# group g multiplies only block g of the weight — which attention implements by
|
| 45 |
+
# reshaping the weight to [n_groups, o_lora_rank, -1]. After nn.quantize a
|
| 46 |
+
# Linear's `.weight` is the packed uint32 tensor, so that reshape would carve up
|
| 47 |
+
# packed bytes rather than logical weights. Keeping it unquantized costs ~1.4B
|
| 48 |
+
# parameters (about 2% of a 4-bit build) and keeps the reshape meaningful. Moving
|
| 49 |
+
# it to a SwitchLinear would recover that, at the cost of a layout change.
|
| 50 |
+
_QUANT_LEAVES = {"wq_a", "wq_b", "wkv", "wo_b", "w1", "w2", "w3",
|
| 51 |
+
"gate_proj", "up_proj", "down_proj", "embed", "head"}
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def should_drop(name: str) -> bool:
|
| 55 |
+
"""MTP and DSpark live under `mtp.` and are inference-irrelevant here."""
|
| 56 |
+
return name.startswith("mtp.")
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def is_routed_expert(out_name: str) -> bool:
|
| 60 |
+
"""The stacked routed-expert matmuls — 98% of all quantized parameters.
|
| 61 |
+
|
| 62 |
+
They ship natively as FP4 (16 levels), which 4-bit affine reproduces almost
|
| 63 |
+
exactly, so precision spent above 4 bits here buys little while costing nearly
|
| 64 |
+
the whole build size. Everything else is ~5B parameters and effectively free to
|
| 65 |
+
keep at high precision.
|
| 66 |
+
"""
|
| 67 |
+
return ".ffn.experts." in out_name and ".shared_experts." not in out_name
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def is_quant_target(out_name: str, in_dim: int, group_size: int) -> bool:
|
| 71 |
+
if not out_name.endswith(".weight"):
|
| 72 |
+
return False
|
| 73 |
+
leaf = out_name[: -len(".weight")].rsplit(".", 1)[-1]
|
| 74 |
+
if leaf not in _QUANT_LEAVES:
|
| 75 |
+
return False
|
| 76 |
+
# the router gate and tid2eid share the `gate` prefix but are not in _QUANT_LEAVES
|
| 77 |
+
return in_dim % group_size == 0
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def bits_for(out_name: str, bits: int, expert_bits: int | None) -> int:
|
| 81 |
+
"""Per-tensor bit width. ``expert_bits`` splits routed experts from the rest."""
|
| 82 |
+
if expert_bits is not None and is_routed_expert(out_name):
|
| 83 |
+
return expert_bits
|
| 84 |
+
return bits
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def _collect(tensors: dict) -> dict:
|
| 88 |
+
"""Dequantize pairs, stack experts, drop side-paths. Returns MLX-named bf16."""
|
| 89 |
+
out: dict[str, mx.array] = {}
|
| 90 |
+
experts: dict[str, dict[int, mx.array]] = {}
|
| 91 |
+
|
| 92 |
+
names = [n for n in tensors if not should_drop(n)]
|
| 93 |
+
for name in names:
|
| 94 |
+
if name.endswith(".scale"):
|
| 95 |
+
continue # consumed with its weight
|
| 96 |
+
arr = tensors[name]
|
| 97 |
+
base = name[: -len(".weight")] if name.endswith(".weight") else name
|
| 98 |
+
scale_name = base + ".scale"
|
| 99 |
+
if scale_name in tensors:
|
| 100 |
+
arr = dequant_pair(base, arr, tensors[scale_name], mx.bfloat16)
|
| 101 |
+
|
| 102 |
+
m = _EXPERT_RE.match(base)
|
| 103 |
+
if m:
|
| 104 |
+
prefix, idx, w = m.group(1), int(m.group(2)), m.group(3)
|
| 105 |
+
key = f"{prefix}.experts.{_PROJ[w]}.weight"
|
| 106 |
+
experts.setdefault(key, {})[idx] = arr
|
| 107 |
+
continue
|
| 108 |
+
|
| 109 |
+
if base.endswith("tid2eid"):
|
| 110 |
+
out[base] = arr.astype(mx.int32) # stored int64, only needs int32
|
| 111 |
+
continue
|
| 112 |
+
|
| 113 |
+
out[name if name.endswith(".weight") else base] = arr
|
| 114 |
+
|
| 115 |
+
for key, byidx in experts.items():
|
| 116 |
+
stacked = mx.stack([byidx[i] for i in sorted(byidx)], axis=0)
|
| 117 |
+
out[key] = stacked
|
| 118 |
+
return out
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def convert(src: str, dst: str, bits: int | None = None, group_size: int = 64,
|
| 122 |
+
out_dtype=mx.bfloat16, verbose: bool = True,
|
| 123 |
+
expert_bits: int | None = None) -> str:
|
| 124 |
+
os.makedirs(dst, exist_ok=True)
|
| 125 |
+
index = json.load(open(os.path.join(src, "model.safetensors.index.json")))
|
| 126 |
+
weight_map = index["weight_map"]
|
| 127 |
+
|
| 128 |
+
shard_to_names: dict[str, list[str]] = {}
|
| 129 |
+
for n, s in weight_map.items():
|
| 130 |
+
shard_to_names.setdefault(s, []).append(n)
|
| 131 |
+
|
| 132 |
+
out_index: dict[str, str] = {}
|
| 133 |
+
buf: dict[str, mx.array] = {}
|
| 134 |
+
nbytes = 0
|
| 135 |
+
shard_id = 0
|
| 136 |
+
dropped = 0
|
| 137 |
+
|
| 138 |
+
def flush():
|
| 139 |
+
nonlocal buf, nbytes, shard_id
|
| 140 |
+
if not buf:
|
| 141 |
+
return
|
| 142 |
+
shard_id += 1
|
| 143 |
+
fname = f"model-{shard_id:05d}.safetensors"
|
| 144 |
+
mx.save_safetensors(os.path.join(dst, fname), buf, metadata={"format": "mlx"})
|
| 145 |
+
for k in buf:
|
| 146 |
+
out_index[k] = fname
|
| 147 |
+
buf = {}
|
| 148 |
+
nbytes = 0
|
| 149 |
+
|
| 150 |
+
for shard in sorted(shard_to_names):
|
| 151 |
+
tensors = mx.load(os.path.join(src, shard))
|
| 152 |
+
dropped += sum(1 for n in tensors if should_drop(n))
|
| 153 |
+
converted = _collect(tensors)
|
| 154 |
+
for out_name, arr in converted.items():
|
| 155 |
+
if bits is not None and is_quant_target(out_name, arr.shape[-1], group_size):
|
| 156 |
+
b = bits_for(out_name, bits, expert_bits)
|
| 157 |
+
qw, sc, bi = mx.quantize(arr, group_size=group_size, bits=b)
|
| 158 |
+
base = out_name[: -len(".weight")]
|
| 159 |
+
items = ((out_name, qw), (base + ".scales", sc), (base + ".biases", bi))
|
| 160 |
+
else:
|
| 161 |
+
keep_fp32 = arr.dtype == mx.float32 or arr.dtype == mx.int32
|
| 162 |
+
items = ((out_name, arr if keep_fp32 else arr.astype(out_dtype)),)
|
| 163 |
+
for k, v in items:
|
| 164 |
+
mx.eval(v)
|
| 165 |
+
buf[k] = v
|
| 166 |
+
nbytes += v.nbytes
|
| 167 |
+
if nbytes >= _SHARD_CAP_BYTES:
|
| 168 |
+
flush()
|
| 169 |
+
del tensors, converted
|
| 170 |
+
if verbose:
|
| 171 |
+
print(f"[convert] {shard} -> {shard_id} shards written", flush=True)
|
| 172 |
+
flush()
|
| 173 |
+
|
| 174 |
+
_finalize(dst, out_index, shard_id)
|
| 175 |
+
_write_config(src, dst, bits, group_size, expert_bits)
|
| 176 |
+
_copy_aux(src, dst)
|
| 177 |
+
if verbose:
|
| 178 |
+
print(f"[convert] dropped {dropped} mtp/dspark tensors")
|
| 179 |
+
return dst
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def _finalize(dst, out_index, n):
|
| 183 |
+
remap = {}
|
| 184 |
+
for i in range(1, n + 1):
|
| 185 |
+
old, new = f"model-{i:05d}.safetensors", f"model-{i:05d}-of-{n:05d}.safetensors"
|
| 186 |
+
if os.path.exists(os.path.join(dst, old)):
|
| 187 |
+
os.rename(os.path.join(dst, old), os.path.join(dst, new))
|
| 188 |
+
remap[old] = new
|
| 189 |
+
wm = {k: remap[v] for k, v in out_index.items()}
|
| 190 |
+
total = sum(os.path.getsize(os.path.join(dst, f)) for f in set(wm.values()))
|
| 191 |
+
json.dump({"metadata": {"total_size": total}, "weight_map": wm},
|
| 192 |
+
open(os.path.join(dst, "model.safetensors.index.json"), "w"), indent=2)
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def _write_config(src, dst, bits, group_size, expert_bits=None):
|
| 196 |
+
cfg = json.load(open(os.path.join(src, "config.json")))
|
| 197 |
+
# the source quantization_config describes FP8/FP4 weights that no longer exist
|
| 198 |
+
cfg.pop("quantization_config", None)
|
| 199 |
+
cfg.pop("expert_dtype", None)
|
| 200 |
+
if bits is not None:
|
| 201 |
+
q = {"group_size": group_size, "bits": bits}
|
| 202 |
+
if expert_bits is not None:
|
| 203 |
+
q["expert_bits"] = expert_bits
|
| 204 |
+
cfg["quantization"] = q
|
| 205 |
+
json.dump(cfg, open(os.path.join(dst, "config.json"), "w"), indent=2)
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def _copy_aux(src, dst):
|
| 209 |
+
for pat in ("tokenizer*", "*.json", "*.jinja", "*.txt", "encoding"):
|
| 210 |
+
for p in glob.glob(os.path.join(src, pat)):
|
| 211 |
+
base = os.path.basename(p)
|
| 212 |
+
if base in ("config.json", "model.safetensors.index.json"):
|
| 213 |
+
continue
|
| 214 |
+
target = os.path.join(dst, base)
|
| 215 |
+
if os.path.isdir(p):
|
| 216 |
+
shutil.copytree(p, target, dirs_exist_ok=True)
|
| 217 |
+
else:
|
| 218 |
+
shutil.copy2(p, target)
|
deepseek_v4_mlx/dequant.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Dequantize DeepSeek-V4's natively-quantized checkpoint to bf16.
|
| 2 |
+
|
| 3 |
+
The published checkpoint is quantized in *two* different formats, and neither is
|
| 4 |
+
a format MLX can consume directly:
|
| 5 |
+
|
| 6 |
+
* **Most matmul weights** — FP8 ``e4m3`` stored as ``uint8`` ``[out, in]``, with
|
| 7 |
+
one ``e8m0`` (exponent-only) scale per ``128x128`` block:
|
| 8 |
+
``scale[ceil(out/128), ceil(in/128)]``.
|
| 9 |
+
* **Routed experts** (``expert_dtype: "fp4"``) — FP4 ``e2m1`` packed **two values
|
| 10 |
+
per byte** into ``int8 [out, in//2]``, with one ``e8m0`` scale per 32 values
|
| 11 |
+
along the input dim: ``scale[out, in//32]``.
|
| 12 |
+
|
| 13 |
+
Everything else (norms, gate weights, compressor projections, embeddings, the LM
|
| 14 |
+
head, the HC parameters) is already bf16 or fp32 and passes through untouched.
|
| 15 |
+
|
| 16 |
+
The `e8m0` format is exponent-only: byte ``b`` denotes ``2**(b - 127)``, with
|
| 17 |
+
``b == 0`` reserved for zero. That makes every scale an exact power of two, so
|
| 18 |
+
applying it is lossless in fp32.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
|
| 23 |
+
import mlx.core as mx
|
| 24 |
+
|
| 25 |
+
FP8_BLOCK = 128 # e4m3 weights: 128x128 scale blocks
|
| 26 |
+
FP4_BLOCK = 32 # e2m1 experts: 32 values per scale, along the input dim
|
| 27 |
+
|
| 28 |
+
# FP4 e2m1 code -> value. 1 sign bit, 2 exponent bits, 1 mantissa bit; the 8
|
| 29 |
+
# representable magnitudes are {0, .5, 1, 1.5, 2, 3, 4, 6} with a sign.
|
| 30 |
+
# Transcribed from FP4_TABLE in the reference inference/convert.py — note code 8
|
| 31 |
+
# ("negative zero") maps to +0.0 there, so it does the same here.
|
| 32 |
+
_E2M1 = [0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0,
|
| 33 |
+
0.0, -0.5, -1.0, -1.5, -2.0, -3.0, -4.0, -6.0]
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def e8m0_to_float(scale_u8: mx.array) -> mx.array:
|
| 37 |
+
"""Decode exponent-only fp8 scales: byte b -> 2**(b-127), with 0 -> 0."""
|
| 38 |
+
e = scale_u8.astype(mx.int32)
|
| 39 |
+
val = mx.power(mx.array(2.0, mx.float32), (e - 127).astype(mx.float32))
|
| 40 |
+
return mx.where(e == 0, mx.array(0.0, mx.float32), val)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _expand_blocks(scale: mx.array, rows: int, cols: int, br: int, bc: int) -> mx.array:
|
| 44 |
+
"""Broadcast a per-block scale grid up to full [rows, cols], then crop.
|
| 45 |
+
|
| 46 |
+
Crop matters: block counts use ceil, so the expanded grid can overhang the
|
| 47 |
+
weight when a dimension is not a multiple of the block size.
|
| 48 |
+
"""
|
| 49 |
+
s = mx.repeat(mx.repeat(scale, br, axis=0), bc, axis=1)
|
| 50 |
+
return s[:rows, :cols]
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def dequant_fp8(weight_u8: mx.array, scale_u8: mx.array, dtype=mx.bfloat16) -> mx.array:
|
| 54 |
+
"""FP8 e4m3 ``[out, in]`` + e8m0 ``[out/128, in/128]`` block scales -> ``dtype``."""
|
| 55 |
+
w = mx.from_fp8(weight_u8, mx.float32)
|
| 56 |
+
rows, cols = w.shape
|
| 57 |
+
s = _expand_blocks(e8m0_to_float(scale_u8), rows, cols, FP8_BLOCK, FP8_BLOCK)
|
| 58 |
+
return (w * s).astype(dtype)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def unpack_fp4(packed_i8: mx.array) -> mx.array:
|
| 62 |
+
"""Unpack ``int8 [out, in//2]`` of two e2m1 nibbles into float32 ``[out, in]``.
|
| 63 |
+
|
| 64 |
+
Low nibble holds the even (lower) input index, high nibble the odd one — the
|
| 65 |
+
same ordering torch uses for ``float4_e2m1fn_x2``.
|
| 66 |
+
"""
|
| 67 |
+
b = packed_i8.view(mx.uint8).astype(mx.int32)
|
| 68 |
+
lo = b & 0x0F
|
| 69 |
+
hi = (b >> 4) & 0x0F
|
| 70 |
+
lut = mx.array(_E2M1, mx.float32)
|
| 71 |
+
vals = mx.stack([lut[lo], lut[hi]], axis=-1) # [out, in//2, 2]
|
| 72 |
+
return vals.reshape(vals.shape[0], -1) # [out, in]
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def dequant_fp4(weight_i8: mx.array, scale_u8: mx.array, dtype=mx.bfloat16) -> mx.array:
|
| 76 |
+
"""Packed FP4 e2m1 ``[out, in//2]`` + e8m0 ``[out, in//32]`` scales -> ``dtype``."""
|
| 77 |
+
w = unpack_fp4(weight_i8) # [out, in]
|
| 78 |
+
rows, cols = w.shape
|
| 79 |
+
s = _expand_blocks(e8m0_to_float(scale_u8), rows, cols, 1, FP4_BLOCK)
|
| 80 |
+
return (w * s).astype(dtype)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def is_fp4_expert(name: str) -> bool:
|
| 84 |
+
"""Routed-expert matmuls are the FP4 ones; shared experts are FP8."""
|
| 85 |
+
return ".experts." in name and ".shared_experts." not in name
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def dequant_pair(name: str, weight: mx.array, scale: mx.array, dtype=mx.bfloat16) -> mx.array:
|
| 89 |
+
"""Dequantize one (weight, scale) pair, choosing the format from the name."""
|
| 90 |
+
if is_fp4_expert(name):
|
| 91 |
+
return dequant_fp4(weight, scale, dtype)
|
| 92 |
+
return dequant_fp8(weight, scale, dtype)
|
deepseek_v4_mlx/fakequant.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Activation fake-quantization — the inference-time half of DeepSeek-V4's QAT.
|
| 2 |
+
|
| 3 |
+
V4 was quantization-aware trained, and the reference *simulates* that quantization
|
| 4 |
+
during inference: several activations are rounded through FP8 (or FP4) and back to
|
| 5 |
+
bf16 before use. These are the ``inplace=True`` calls to ``act_quant`` /
|
| 6 |
+
``fp4_act_quant`` in ``inference/model.py``, and they are not optional — skipping
|
| 7 |
+
them changes the numbers the model was trained to see.
|
| 8 |
+
|
| 9 |
+
Both follow the same shape:
|
| 10 |
+
|
| 11 |
+
1. take the absolute max over each block along the last axis,
|
| 12 |
+
2. round the scale **up to a power of two**: ``2**ceil(log2(amax / dtype_max))``
|
| 13 |
+
(this is what ``scale_fmt="ue8m0"`` means),
|
| 14 |
+
3. divide, round to the target format, multiply back.
|
| 15 |
+
|
| 16 |
+
Applied at:
|
| 17 |
+
|
| 18 |
+
* ``kv[..., :-rope_dim]`` in attention and the compressor — FP8, 64-wide blocks;
|
| 19 |
+
the rope channels stay bf16 so positional precision is not degraded,
|
| 20 |
+
* indexer queries and its compressed KV — FP4, 32-wide blocks, after a Hadamard
|
| 21 |
+
rotation.
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
from __future__ import annotations
|
| 25 |
+
|
| 26 |
+
import mlx.core as mx
|
| 27 |
+
|
| 28 |
+
FP8_MAX = 448.0 # e4m3fn
|
| 29 |
+
FP4_MAX = 6.0 # e2m1
|
| 30 |
+
|
| 31 |
+
# e2m1 magnitudes; used to round a scaled value to the nearest representable FP4.
|
| 32 |
+
_E2M1_POS = mx.array([0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0], dtype=mx.float32)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _pow2_scale(amax: mx.array, dtype_max: float) -> mx.array:
|
| 36 |
+
"""``2**ceil(log2(amax / dtype_max))``, matching ``fast_round_scale``.
|
| 37 |
+
|
| 38 |
+
Guarded so an all-zero block yields scale 1 rather than 0 or a NaN.
|
| 39 |
+
"""
|
| 40 |
+
r = amax / dtype_max
|
| 41 |
+
r = mx.maximum(r, 1e-38)
|
| 42 |
+
return mx.power(mx.array(2.0, mx.float32), mx.ceil(mx.log2(r)))
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def _blockify(x: mx.array, block: int):
|
| 46 |
+
n = x.shape[-1]
|
| 47 |
+
if n % block:
|
| 48 |
+
raise ValueError(f"last dim {n} not divisible by block {block}")
|
| 49 |
+
return x.reshape(*x.shape[:-1], n // block, block)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def fake_quant_fp8(x: mx.array, block: int = 64) -> mx.array:
|
| 53 |
+
"""Round ``x`` through e4m3 with per-block power-of-two scales, back to input dtype."""
|
| 54 |
+
dtype = x.dtype
|
| 55 |
+
shape = x.shape
|
| 56 |
+
xb = _blockify(x.astype(mx.float32), block)
|
| 57 |
+
amax = mx.max(mx.abs(xb), axis=-1, keepdims=True)
|
| 58 |
+
s = _pow2_scale(amax, FP8_MAX)
|
| 59 |
+
q = mx.from_fp8(mx.to_fp8(xb / s), mx.float32) * s
|
| 60 |
+
return q.reshape(shape).astype(dtype)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def fake_quant_fp4(x: mx.array, block: int = 32) -> mx.array:
|
| 64 |
+
"""Round ``x`` through e2m1 with per-block power-of-two scales, back to input dtype.
|
| 65 |
+
|
| 66 |
+
MLX has no fp4 cast, so rounding is done by snapping |v| to the nearest of the
|
| 67 |
+
eight e2m1 magnitudes (ties-to-even is not modelled; the reference kernel casts
|
| 68 |
+
via hardware, which rounds to nearest-even, so extreme midpoints can differ by
|
| 69 |
+
one code — well below the noise this simulation represents).
|
| 70 |
+
"""
|
| 71 |
+
dtype = x.dtype
|
| 72 |
+
shape = x.shape
|
| 73 |
+
xb = _blockify(x.astype(mx.float32), block)
|
| 74 |
+
amax = mx.max(mx.abs(xb), axis=-1, keepdims=True)
|
| 75 |
+
s = _pow2_scale(amax, FP4_MAX)
|
| 76 |
+
v = mx.clip(xb / s, -FP4_MAX, FP4_MAX)
|
| 77 |
+
|
| 78 |
+
sign = mx.sign(v)
|
| 79 |
+
mag = mx.abs(v)
|
| 80 |
+
# nearest of the 8 magnitudes: compare against midpoints
|
| 81 |
+
lut = _E2M1_POS
|
| 82 |
+
diffs = mx.abs(mag[..., None] - lut.reshape(*([1] * mag.ndim), -1))
|
| 83 |
+
nearest = mx.argmin(diffs, axis=-1)
|
| 84 |
+
q = sign * lut[nearest]
|
| 85 |
+
return (q * s).reshape(shape).astype(dtype)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def fake_quant_kv(kv: mx.array, rope_dim: int, block: int = 64) -> mx.array:
|
| 89 |
+
"""FP8-simulate the non-rope channels of a KV vector, leaving rope channels alone.
|
| 90 |
+
|
| 91 |
+
Mirrors ``act_quant(kv[..., :-rd], 64, ..., inplace=True)`` in the reference,
|
| 92 |
+
which quantizes a *slice* in place.
|
| 93 |
+
"""
|
| 94 |
+
nope = fake_quant_fp8(kv[..., :-rope_dim], block)
|
| 95 |
+
return mx.concatenate([nope, kv[..., -rope_dim:]], axis=-1)
|
deepseek_v4_mlx/generate.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Greedy generation with an incremental cache."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import mlx.core as mx
|
| 6 |
+
|
| 7 |
+
from .cache import make_cache
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def greedy_generate(model, args, prompt_ids, max_new_tokens: int = 32,
|
| 11 |
+
eos_id: int | None = 1, max_seq_len: int | None = None,
|
| 12 |
+
verbose: bool = False):
|
| 13 |
+
"""Prefill the prompt, then decode one token at a time against the cache."""
|
| 14 |
+
ids = list(prompt_ids)
|
| 15 |
+
cache = make_cache(args, bsz=1,
|
| 16 |
+
max_seq_len=max_seq_len or (len(ids) + max_new_tokens + 8))
|
| 17 |
+
|
| 18 |
+
logits = model(mx.array([ids]), last_logit_only=True, cache=cache)
|
| 19 |
+
nxt = int(mx.argmax(logits[0, -1]).item())
|
| 20 |
+
ids.append(nxt)
|
| 21 |
+
if verbose:
|
| 22 |
+
print(f"[gen] prefilled {len(prompt_ids)} tokens", flush=True)
|
| 23 |
+
|
| 24 |
+
for _ in range(max_new_tokens - 1):
|
| 25 |
+
if eos_id is not None and nxt == eos_id:
|
| 26 |
+
break
|
| 27 |
+
logits = model(mx.array([[nxt]]), last_logit_only=True, cache=cache)
|
| 28 |
+
nxt = int(mx.argmax(logits[0, -1]).item())
|
| 29 |
+
ids.append(nxt)
|
| 30 |
+
return ids
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def load_tokenizer(path_or_repo: str = "deepseek-ai/DeepSeek-V3"):
|
| 34 |
+
"""DeepSeek-V4 ships no tokenizer; V3's matches (same padded 129280 vocab,
|
| 35 |
+
same bos/eos, all V4 special tokens present)."""
|
| 36 |
+
from transformers import AutoTokenizer
|
| 37 |
+
return AutoTokenizer.from_pretrained(path_or_repo, trust_remote_code=True)
|
deepseek_v4_mlx/hyper_connections.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Hyper-Connections (HC) — DeepSeek-V4's replacement for the plain residual stream.
|
| 2 |
+
|
| 3 |
+
Instead of carrying one hidden state, a V4 block carries ``hc_mult`` (=4) parallel
|
| 4 |
+
copies. Each sub-layer:
|
| 5 |
+
|
| 6 |
+
* ``hc_pre`` reduces the 4 copies to 1 via learned per-copy weights, and also
|
| 7 |
+
emits the ``post`` (expand) weights and a ``comb`` (combination) matrix,
|
| 8 |
+
* the sub-layer (attention or MoE) runs on that single reduced state,
|
| 9 |
+
* ``hc_post`` writes the result back out to 4 copies, mixing in the previous 4
|
| 10 |
+
via ``comb``.
|
| 11 |
+
|
| 12 |
+
All three weight sets come from one projection of the flattened state, split and
|
| 13 |
+
normalized by :func:`split_sinkhorn`. ``comb`` is made doubly-stochastic-ish by
|
| 14 |
+
Sinkhorn iteration (alternating row/column normalization), which is what keeps the
|
| 15 |
+
4 streams from collapsing into each other.
|
| 16 |
+
|
| 17 |
+
Ported from the reference ``hc_split_sinkhorn_kernel`` in ``inference/kernel.py``
|
| 18 |
+
and ``Block.hc_pre`` / ``Block.hc_post`` in ``inference/model.py``. The kernel is
|
| 19 |
+
pure elementwise + reductions, so this is a faithful transcription rather than an
|
| 20 |
+
approximation.
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
from __future__ import annotations
|
| 24 |
+
|
| 25 |
+
import mlx.core as mx
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def split_sinkhorn(mixes: mx.array, hc_scale: mx.array, hc_base: mx.array,
|
| 29 |
+
hc_mult: int = 4, sinkhorn_iters: int = 20, eps: float = 1e-6):
|
| 30 |
+
"""Split one projection into (pre, post, comb).
|
| 31 |
+
|
| 32 |
+
``mixes``: [..., (2 + hc) * hc] -- the projected+normalized state
|
| 33 |
+
``hc_scale``: [3] -- one scale per output group
|
| 34 |
+
``hc_base``: [(2 + hc) * hc] -- one bias per output element
|
| 35 |
+
|
| 36 |
+
Returns ``pre`` [..., hc], ``post`` [..., hc], ``comb`` [..., hc, hc].
|
| 37 |
+
|
| 38 |
+
Layout matches the reference kernel exactly: the first ``hc`` entries feed
|
| 39 |
+
``pre``, the next ``hc`` feed ``post``, and the remaining ``hc*hc`` form
|
| 40 |
+
``comb`` in row-major order.
|
| 41 |
+
"""
|
| 42 |
+
hc = hc_mult
|
| 43 |
+
mixes = mixes.astype(mx.float32)
|
| 44 |
+
scale = hc_scale.astype(mx.float32)
|
| 45 |
+
base = hc_base.astype(mx.float32)
|
| 46 |
+
|
| 47 |
+
# pre: sigmoid(x*s0 + b) + eps -- strictly positive reduce weights
|
| 48 |
+
pre = mx.sigmoid(mixes[..., :hc] * scale[0] + base[:hc]) + eps
|
| 49 |
+
# post: 2*sigmoid(x*s1 + b) -- expand weights, mean 1 at init
|
| 50 |
+
post = 2.0 * mx.sigmoid(mixes[..., hc:2 * hc] * scale[1] + base[hc:2 * hc])
|
| 51 |
+
|
| 52 |
+
comb = mixes[..., 2 * hc:] * scale[2] + base[2 * hc:]
|
| 53 |
+
comb = comb.reshape(*comb.shape[:-1], hc, hc)
|
| 54 |
+
|
| 55 |
+
# Row softmax, then one column normalization, then (iters-1) full Sinkhorn
|
| 56 |
+
# sweeps. The eps terms are inside the divisions in the reference kernel;
|
| 57 |
+
# keeping them there matters because comb rows can get very small.
|
| 58 |
+
comb = mx.softmax(comb, axis=-1) + eps
|
| 59 |
+
comb = comb / (comb.sum(axis=-2, keepdims=True) + eps)
|
| 60 |
+
for _ in range(sinkhorn_iters - 1):
|
| 61 |
+
comb = comb / (comb.sum(axis=-1, keepdims=True) + eps)
|
| 62 |
+
comb = comb / (comb.sum(axis=-2, keepdims=True) + eps)
|
| 63 |
+
|
| 64 |
+
return pre, post, comb
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def hc_pre(x: mx.array, hc_fn: mx.array, hc_scale: mx.array, hc_base: mx.array,
|
| 68 |
+
hc_mult: int, sinkhorn_iters: int, norm_eps: float, hc_eps: float):
|
| 69 |
+
"""Reduce the ``hc`` residual copies to one state.
|
| 70 |
+
|
| 71 |
+
``x``: [b, s, hc, d] -> returns ([b, s, d], post [b,s,hc], comb [b,s,hc,hc]).
|
| 72 |
+
|
| 73 |
+
The projection input is the *flattened* [b, s, hc*d] state scaled by its RMS
|
| 74 |
+
reciprocal — note the reference multiplies by rsqrt AFTER the linear, which is
|
| 75 |
+
equivalent here only because rsqrt is per-token; it is transcribed in that
|
| 76 |
+
order to keep the arithmetic bit-for-bit comparable.
|
| 77 |
+
"""
|
| 78 |
+
shape = x.shape
|
| 79 |
+
dtype = x.dtype
|
| 80 |
+
xf = x.reshape(*shape[:2], -1).astype(mx.float32)
|
| 81 |
+
rsqrt = mx.rsqrt(mx.mean(mx.square(xf), axis=-1, keepdims=True) + norm_eps)
|
| 82 |
+
mixes = (xf @ hc_fn.astype(mx.float32).T) * rsqrt
|
| 83 |
+
pre, post, comb = split_sinkhorn(mixes, hc_scale, hc_base, hc_mult, sinkhorn_iters, hc_eps)
|
| 84 |
+
y = mx.sum(pre[..., None] * xf.reshape(shape), axis=2)
|
| 85 |
+
return y.astype(dtype), post, comb
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def hc_post(x: mx.array, residual: mx.array, post: mx.array, comb: mx.array) -> mx.array:
|
| 89 |
+
"""Expand the sub-layer output back to ``hc`` copies and mix in the residual.
|
| 90 |
+
|
| 91 |
+
``x``: [b, s, d]; ``residual``: [b, s, hc, d]; returns [b, s, hc, d].
|
| 92 |
+
|
| 93 |
+
Output copy ``k`` is ``post[k]*x + sum_j comb[j, k] * residual[j]`` — residual is
|
| 94 |
+
indexed by ``j``, the axis being summed, so it must sit on axis 2 of the product
|
| 95 |
+
(the reference's ``residual.unsqueeze(-2)``). Broadcasting it onto axis 3 instead
|
| 96 |
+
silently computes ``(sum_j comb[j, k]) * residual[k]``, which has the right shape
|
| 97 |
+
and stays finite, so only a value comparison catches it.
|
| 98 |
+
"""
|
| 99 |
+
prod = comb[..., None] * residual[..., :, None, :] # [b, s, j, k, d]
|
| 100 |
+
out = post[..., None] * x[..., None, :] + mx.sum(prod, axis=2)
|
| 101 |
+
return out.astype(x.dtype)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def hc_head(x: mx.array, hc_fn: mx.array, hc_scale: mx.array, hc_base: mx.array,
|
| 105 |
+
norm_eps: float, hc_eps: float) -> mx.array:
|
| 106 |
+
"""Final reduction before the LM head: like :func:`hc_pre` but weights only.
|
| 107 |
+
|
| 108 |
+
Uses the raw sigmoid gate without the Sinkhorn split, matching
|
| 109 |
+
``Block.hc_head`` in the reference.
|
| 110 |
+
"""
|
| 111 |
+
shape = x.shape
|
| 112 |
+
dtype = x.dtype
|
| 113 |
+
xf = x.reshape(*shape[:2], -1).astype(mx.float32)
|
| 114 |
+
rsqrt = mx.rsqrt(mx.mean(mx.square(xf), axis=-1, keepdims=True) + norm_eps)
|
| 115 |
+
mixes = (xf @ hc_fn.astype(mx.float32).T) * rsqrt
|
| 116 |
+
pre = mx.sigmoid(mixes * hc_scale + hc_base) + hc_eps
|
| 117 |
+
y = mx.sum(pre[..., None] * xf.reshape(shape), axis=2)
|
| 118 |
+
return y.astype(dtype)
|
deepseek_v4_mlx/indexer.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""The sparse-attention indexer — a second attention that decides what the first reads.
|
| 2 |
+
|
| 3 |
+
Each ratio-4 layer runs this alongside its sliding window. It keeps its own
|
| 4 |
+
:class:`Compressor` (the Hadamard-rotated, FP4 variant), scores every compressed
|
| 5 |
+
span against the current queries, and returns the indices of the top
|
| 6 |
+
``index_topk`` spans. Attention then reads its local window **plus exactly those**.
|
| 7 |
+
|
| 8 |
+
Scoring is deliberately cheap and low-precision: queries are Hadamard-rotated and
|
| 9 |
+
FP4-simulated, scores are ReLU'd before a learned per-head weighting collapses the
|
| 10 |
+
head axis. Positions the query may not see are marked ``-1`` rather than dropped,
|
| 11 |
+
so the returned index block stays rectangular for the gather downstream.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import mlx.core as mx
|
| 17 |
+
import mlx.nn as nn
|
| 18 |
+
|
| 19 |
+
from .compressor import Compressor
|
| 20 |
+
from .config import ModelArgs
|
| 21 |
+
from .fakequant import fake_quant_fp4
|
| 22 |
+
from .layers import apply_rotary_emb, hadamard_transform
|
| 23 |
+
|
| 24 |
+
NEG_INF = -1e30
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class Indexer(nn.Module):
|
| 28 |
+
def __init__(self, args: ModelArgs, compress_ratio: int = 4):
|
| 29 |
+
super().__init__()
|
| 30 |
+
self.n_heads = args.index_n_heads
|
| 31 |
+
self.head_dim = args.index_head_dim
|
| 32 |
+
self.rope_head_dim = args.rope_head_dim
|
| 33 |
+
self.index_topk = args.index_topk
|
| 34 |
+
self.compress_ratio = compress_ratio
|
| 35 |
+
self.softmax_scale = self.head_dim ** -0.5
|
| 36 |
+
|
| 37 |
+
self.wq_b = nn.Linear(args.q_lora_rank, self.n_heads * self.head_dim, bias=False)
|
| 38 |
+
self.weights_proj = nn.Linear(args.dim, self.n_heads, bias=False)
|
| 39 |
+
self.compressor = Compressor(args, compress_ratio, self.head_dim, rotate=True)
|
| 40 |
+
|
| 41 |
+
def __call__(self, x: mx.array, qr: mx.array, cos: mx.array, sin: mx.array,
|
| 42 |
+
offset: int):
|
| 43 |
+
"""Prefill: choose compressed spans for every query position.
|
| 44 |
+
|
| 45 |
+
``x`` [b, s, dim] — hidden state, feeds the compressor and weighting
|
| 46 |
+
``qr`` [b, s, q_lora_rank] — the shared low-rank query, before wq_b
|
| 47 |
+
``offset`` — where compressed entries begin in attention's KV cache, so the
|
| 48 |
+
returned indices address that cache directly.
|
| 49 |
+
|
| 50 |
+
Returns ``[b, s, k]`` int32 indices, with ``-1`` marking "not visible".
|
| 51 |
+
"""
|
| 52 |
+
bsz, seqlen, _ = x.shape
|
| 53 |
+
ratio, rd = self.compress_ratio, self.rope_head_dim
|
| 54 |
+
|
| 55 |
+
q = self.wq_b(qr).reshape(bsz, seqlen, self.n_heads, self.head_dim)
|
| 56 |
+
rot = apply_rotary_emb(q[..., -rd:], cos[:seqlen], sin[:seqlen])
|
| 57 |
+
q = mx.concatenate([q[..., :-rd], rot], axis=-1)
|
| 58 |
+
q = hadamard_transform(q)
|
| 59 |
+
q = fake_quant_fp4(q, 32)
|
| 60 |
+
|
| 61 |
+
kv = self.compressor(x, cos, sin) # [b, nb, head_dim] or None
|
| 62 |
+
if kv is None:
|
| 63 |
+
return None
|
| 64 |
+
nb = kv.shape[1]
|
| 65 |
+
|
| 66 |
+
# per-head score, ReLU'd, then collapsed by a learned per-head weight
|
| 67 |
+
w = self.weights_proj(x) * (self.softmax_scale * self.n_heads ** -0.5)
|
| 68 |
+
scores = mx.einsum("bshd,btd->bsht", q.astype(mx.float32), kv.astype(mx.float32))
|
| 69 |
+
scores = mx.maximum(scores, 0.0) * w[..., None].astype(mx.float32)
|
| 70 |
+
scores = mx.sum(scores, axis=2) # [b, s, nb]
|
| 71 |
+
|
| 72 |
+
# span t is visible to query s only once the span is complete: t < (s+1)//ratio
|
| 73 |
+
vis = mx.arange(nb)[None, :] < (mx.arange(1, seqlen + 1)[:, None] // ratio)
|
| 74 |
+
scores = scores + mx.where(vis, 0.0, NEG_INF)[None]
|
| 75 |
+
|
| 76 |
+
k = min(self.index_topk, nb)
|
| 77 |
+
# argpartition yields unsigned indices; go to int32 before the -1 sentinel
|
| 78 |
+
idx = mx.argpartition(-scores, k - 1, axis=-1)[..., :k].astype(mx.int32)
|
| 79 |
+
|
| 80 |
+
# invisible spans may still be picked when fewer than k are visible; mark them
|
| 81 |
+
picked_vis = mx.take_along_axis(
|
| 82 |
+
mx.broadcast_to(vis[None], (bsz, seqlen, nb)), idx, axis=-1)
|
| 83 |
+
return mx.where(picked_vis, idx + offset, mx.array(-1, mx.int32))
|
| 84 |
+
|
| 85 |
+
def step(self, x: mx.array, qr: mx.array, cos: mx.array, sin: mx.array,
|
| 86 |
+
pos: int, cache, offset: int):
|
| 87 |
+
"""Decode one token: extend the compressed index, then pick top-k spans."""
|
| 88 |
+
rd = self.rope_head_dim
|
| 89 |
+
bsz = x.shape[0]
|
| 90 |
+
|
| 91 |
+
pooled = self.compressor.step(x, pos, cache.idx_comp, cos, sin)
|
| 92 |
+
if pooled is not None:
|
| 93 |
+
cache._adopt(pooled.dtype)
|
| 94 |
+
cache.idx_kv[:, pos // self.compress_ratio] = pooled[:, 0]
|
| 95 |
+
|
| 96 |
+
nb = (pos + 1) // self.compress_ratio
|
| 97 |
+
if nb <= 0:
|
| 98 |
+
return None
|
| 99 |
+
|
| 100 |
+
q = self.wq_b(qr).reshape(bsz, 1, self.n_heads, self.head_dim)
|
| 101 |
+
rot = apply_rotary_emb(q[..., -rd:], cos[pos:pos + 1], sin[pos:pos + 1])
|
| 102 |
+
q = mx.concatenate([q[..., :-rd], rot], axis=-1)
|
| 103 |
+
q = fake_quant_fp4(hadamard_transform(q), 32)
|
| 104 |
+
|
| 105 |
+
kv = cache.idx_kv[:, :nb]
|
| 106 |
+
w = self.weights_proj(x) * (self.softmax_scale * self.n_heads ** -0.5)
|
| 107 |
+
scores = mx.einsum("bshd,btd->bsht", q.astype(mx.float32), kv.astype(mx.float32))
|
| 108 |
+
scores = mx.maximum(scores, 0.0) * w[..., None].astype(mx.float32)
|
| 109 |
+
scores = mx.sum(scores, axis=2) # [b, 1, nb]
|
| 110 |
+
|
| 111 |
+
k = min(self.index_topk, nb)
|
| 112 |
+
idx = mx.argpartition(-scores, k - 1, axis=-1)[..., :k].astype(mx.int32)
|
| 113 |
+
return idx + offset
|
deepseek_v4_mlx/layers.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Core DeepSeek-V4 layers: RMSNorm, YaRN rope, and the Hadamard rotation.
|
| 2 |
+
|
| 3 |
+
Transcribed from ``inference/model.py``. Two details are easy to get wrong and are
|
| 4 |
+
called out where they occur:
|
| 5 |
+
|
| 6 |
+
* rope is applied to the **last** ``rope_head_dim`` channels only, pairing
|
| 7 |
+
*adjacent* elements as (real, imag) — not the split-halves convention used by
|
| 8 |
+
Llama-style models;
|
| 9 |
+
* attention applies the rope **inverse** to its output (``apply_rotary_emb(...,
|
| 10 |
+
inverse=True)``), which is why the conjugate path exists here at all.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import math
|
| 16 |
+
|
| 17 |
+
import mlx.core as mx
|
| 18 |
+
import mlx.nn as nn
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class RMSNorm(nn.Module):
|
| 22 |
+
"""RMSNorm computed in fp32, returning the input dtype (matches reference)."""
|
| 23 |
+
|
| 24 |
+
def __init__(self, dim: int, eps: float = 1e-6):
|
| 25 |
+
super().__init__()
|
| 26 |
+
self.eps = eps
|
| 27 |
+
self.weight = mx.ones((dim,), dtype=mx.float32)
|
| 28 |
+
|
| 29 |
+
def __call__(self, x: mx.array) -> mx.array:
|
| 30 |
+
dtype = x.dtype
|
| 31 |
+
xf = x.astype(mx.float32)
|
| 32 |
+
var = mx.mean(mx.square(xf), axis=-1, keepdims=True)
|
| 33 |
+
xf = xf * mx.rsqrt(var + self.eps)
|
| 34 |
+
return (self.weight * xf).astype(dtype)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def precompute_freqs_cis(dim: int, seqlen: int, original_seq_len: int, base: float,
|
| 38 |
+
factor: float, beta_fast: int, beta_slow: int):
|
| 39 |
+
"""YaRN-scaled rotary frequencies, returned as (cos, sin) of shape [seqlen, dim//2].
|
| 40 |
+
|
| 41 |
+
MLX has no complex dtype in the ops we need, so the reference's ``polar``
|
| 42 |
+
representation is carried as an explicit cos/sin pair.
|
| 43 |
+
|
| 44 |
+
``original_seq_len == 0`` disables YaRN entirely, which the reference uses for
|
| 45 |
+
the pure sliding-window layers.
|
| 46 |
+
"""
|
| 47 |
+
def find_correction_dim(num_rotations, dim, base, max_seq_len):
|
| 48 |
+
return dim * math.log(max_seq_len / (num_rotations * 2 * math.pi)) / (2 * math.log(base))
|
| 49 |
+
|
| 50 |
+
def find_correction_range(low_rot, high_rot, dim, base, max_seq_len):
|
| 51 |
+
low = math.floor(find_correction_dim(low_rot, dim, base, max_seq_len))
|
| 52 |
+
high = math.ceil(find_correction_dim(high_rot, dim, base, max_seq_len))
|
| 53 |
+
return max(low, 0), min(high, dim - 1)
|
| 54 |
+
|
| 55 |
+
def linear_ramp_factor(mn, mx_, d):
|
| 56 |
+
if mn == mx_:
|
| 57 |
+
mx_ += 0.001
|
| 58 |
+
lin = (mx.arange(d, dtype=mx.float32) - mn) / (mx_ - mn)
|
| 59 |
+
return mx.clip(lin, 0, 1)
|
| 60 |
+
|
| 61 |
+
freqs = 1.0 / (base ** (mx.arange(0, dim, 2, dtype=mx.float32) / dim))
|
| 62 |
+
if original_seq_len > 0:
|
| 63 |
+
low, high = find_correction_range(beta_fast, beta_slow, dim, base, original_seq_len)
|
| 64 |
+
smooth = 1 - linear_ramp_factor(low, high, dim // 2)
|
| 65 |
+
freqs = freqs / factor * (1 - smooth) + freqs * smooth
|
| 66 |
+
|
| 67 |
+
t = mx.arange(seqlen, dtype=mx.float32)
|
| 68 |
+
ang = t[:, None] * freqs[None, :] # [seqlen, dim//2]
|
| 69 |
+
return mx.cos(ang), mx.sin(ang)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def apply_rotary_emb(x: mx.array, cos: mx.array, sin: mx.array, inverse: bool = False) -> mx.array:
|
| 73 |
+
"""Rotate ``x`` [..., s, (h,) d] using adjacent-pair complex multiplication.
|
| 74 |
+
|
| 75 |
+
``cos``/``sin`` are [s, d//2]. ``inverse=True`` conjugates (de-rotates), which
|
| 76 |
+
attention uses on its output.
|
| 77 |
+
"""
|
| 78 |
+
dtype = x.dtype
|
| 79 |
+
xf = x.astype(mx.float32)
|
| 80 |
+
shape = xf.shape
|
| 81 |
+
xf = xf.reshape(*shape[:-1], shape[-1] // 2, 2)
|
| 82 |
+
xr, xi = xf[..., 0], xf[..., 1]
|
| 83 |
+
|
| 84 |
+
# broadcast [s, d//2] against [b, s, d//2] or [b, s, h, d//2]
|
| 85 |
+
if xr.ndim == 4:
|
| 86 |
+
c = cos[None, :, None, :]
|
| 87 |
+
s = sin[None, :, None, :]
|
| 88 |
+
else:
|
| 89 |
+
c = cos[None, :, :]
|
| 90 |
+
s = sin[None, :, :]
|
| 91 |
+
if inverse:
|
| 92 |
+
s = -s
|
| 93 |
+
|
| 94 |
+
out = mx.stack([xr * c - xi * s, xr * s + xi * c], axis=-1)
|
| 95 |
+
return out.reshape(shape).astype(dtype)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def hadamard_transform(x: mx.array, scale: float | None = None) -> mx.array:
|
| 99 |
+
"""Fast Walsh-Hadamard transform along the last axis (power-of-two length).
|
| 100 |
+
|
| 101 |
+
Stands in for ``fast_hadamard_transform.hadamard_transform``, which the indexer
|
| 102 |
+
uses to spread information across channels before FP4 quantization. The
|
| 103 |
+
reference always calls it with ``scale = d ** -0.5``; that is the default here.
|
| 104 |
+
"""
|
| 105 |
+
d = x.shape[-1]
|
| 106 |
+
if d & (d - 1):
|
| 107 |
+
raise ValueError(f"hadamard_transform needs a power-of-two last dim, got {d}")
|
| 108 |
+
if scale is None:
|
| 109 |
+
scale = d ** -0.5
|
| 110 |
+
|
| 111 |
+
dtype = x.dtype
|
| 112 |
+
y = x.astype(mx.float32)
|
| 113 |
+
lead = y.shape[:-1]
|
| 114 |
+
h = 1
|
| 115 |
+
while h < d:
|
| 116 |
+
# pair up elements h apart and apply the 2x2 butterfly
|
| 117 |
+
y = y.reshape(*lead, d // (2 * h), 2, h)
|
| 118 |
+
a, b = y[..., 0, :], y[..., 1, :]
|
| 119 |
+
y = mx.stack([a + b, a - b], axis=-2)
|
| 120 |
+
y = y.reshape(*lead, d)
|
| 121 |
+
h *= 2
|
| 122 |
+
return (y * scale).astype(dtype)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def swiglu(gate: mx.array, up: mx.array, limit: float = 0.0) -> mx.array:
|
| 126 |
+
"""SwiGLU with V4's asymmetric clamp: ``up`` two-sided, ``gate`` upper-only."""
|
| 127 |
+
g = gate.astype(mx.float32)
|
| 128 |
+
u = up.astype(mx.float32)
|
| 129 |
+
if limit > 0:
|
| 130 |
+
u = mx.clip(u, -limit, limit)
|
| 131 |
+
g = mx.minimum(g, limit)
|
| 132 |
+
return (g * mx.sigmoid(g)) * u
|
deepseek_v4_mlx/load.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Load a converted DeepSeek-V4 MLX build."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import glob
|
| 6 |
+
import json
|
| 7 |
+
import os
|
| 8 |
+
|
| 9 |
+
import mlx.core as mx
|
| 10 |
+
import mlx.nn as nn
|
| 11 |
+
from mlx.utils import tree_flatten
|
| 12 |
+
|
| 13 |
+
from .config import ModelArgs
|
| 14 |
+
from .convert import bits_for, is_quant_target
|
| 15 |
+
from .model import Model
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def quant_predicate(group_size: int, bits: int, expert_bits: int | None):
|
| 19 |
+
"""Quantize exactly what the converter did, at the same per-tensor bit width.
|
| 20 |
+
|
| 21 |
+
Returning a dict rather than True lets a mixed build load: MLX applies the
|
| 22 |
+
per-module ``{"group_size", "bits"}`` it returns. Answering plain ``True`` would
|
| 23 |
+
quantize every module at the top-level ``bits`` and silently mis-read the
|
| 24 |
+
experts, which are stored at ``expert_bits``.
|
| 25 |
+
"""
|
| 26 |
+
def pred(path, module):
|
| 27 |
+
if not hasattr(module, "to_quantized"):
|
| 28 |
+
return False
|
| 29 |
+
w = getattr(module, "weight", None)
|
| 30 |
+
if w is None:
|
| 31 |
+
return False
|
| 32 |
+
name = path + ".weight"
|
| 33 |
+
if not is_quant_target(name, w.shape[-1], group_size):
|
| 34 |
+
return False
|
| 35 |
+
return {"group_size": group_size, "bits": bits_for(name, bits, expert_bits)}
|
| 36 |
+
return pred
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def load(path: str, lazy: bool = False):
|
| 40 |
+
cfg = json.load(open(os.path.join(path, "config.json")))
|
| 41 |
+
args = ModelArgs.from_dict(cfg)
|
| 42 |
+
model = Model(args)
|
| 43 |
+
|
| 44 |
+
q = cfg.get("quantization")
|
| 45 |
+
if q:
|
| 46 |
+
nn.quantize(model, group_size=q["group_size"], bits=q["bits"],
|
| 47 |
+
class_predicate=quant_predicate(q["group_size"], q["bits"],
|
| 48 |
+
q.get("expert_bits")))
|
| 49 |
+
|
| 50 |
+
loaded = set()
|
| 51 |
+
for shard in sorted(glob.glob(os.path.join(path, "*.safetensors"))):
|
| 52 |
+
w = mx.load(shard)
|
| 53 |
+
model.load_weights(list(w.items()), strict=False)
|
| 54 |
+
if not lazy:
|
| 55 |
+
# materialize this shard now (bounded graph) rather than building one
|
| 56 |
+
# enormous eval over every parameter at the end
|
| 57 |
+
mx.eval(list(w.values()))
|
| 58 |
+
loaded.update(w.keys())
|
| 59 |
+
del w
|
| 60 |
+
|
| 61 |
+
expected = {k for k, _ in tree_flatten(model.parameters())}
|
| 62 |
+
missing = expected - loaded
|
| 63 |
+
if missing:
|
| 64 |
+
raise ValueError(f"{len(missing)} params missing from checkpoint, "
|
| 65 |
+
f"e.g. {sorted(missing)[:3]}")
|
| 66 |
+
|
| 67 |
+
model.eval()
|
| 68 |
+
return model, args
|
deepseek_v4_mlx/model.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""DeepSeek-V4 block and full model.
|
| 2 |
+
|
| 3 |
+
The block is where Hyper-Connections meet the sub-layers: instead of
|
| 4 |
+
``x = x + attn(norm(x))`` it is
|
| 5 |
+
|
| 6 |
+
residual = x # [b, s, hc, d]
|
| 7 |
+
x, post, comb = hc_pre(x, ...) # 4 copies -> 1
|
| 8 |
+
x = attn(attn_norm(x))
|
| 9 |
+
x = hc_post(x, residual, post, comb) # 1 -> 4, mixing the previous 4
|
| 10 |
+
|
| 11 |
+
and the same again around the MoE. The MoE additionally takes ``input_ids``,
|
| 12 |
+
because hash-routed layers look their experts up by token id.
|
| 13 |
+
|
| 14 |
+
The model expands the embedding to ``hc_mult`` identical copies on the way in and
|
| 15 |
+
collapses them with a single gate (``hc_head``) on the way out.
|
| 16 |
+
|
| 17 |
+
DSpark and the MTP head are deliberately absent: they hang off the reference's
|
| 18 |
+
``forward_spec`` path and are not used for ordinary generation.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
|
| 23 |
+
import mlx.core as mx
|
| 24 |
+
import mlx.nn as nn
|
| 25 |
+
|
| 26 |
+
from .attention import Attention
|
| 27 |
+
from .config import ModelArgs
|
| 28 |
+
from .hyper_connections import hc_head, hc_post, hc_pre
|
| 29 |
+
from .layers import RMSNorm
|
| 30 |
+
from .moe import MoE
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class Block(nn.Module):
|
| 34 |
+
def __init__(self, layer_id: int, args: ModelArgs):
|
| 35 |
+
super().__init__()
|
| 36 |
+
self.layer_id = layer_id
|
| 37 |
+
self.norm_eps = args.norm_eps
|
| 38 |
+
self.hc_mult = args.hc_mult
|
| 39 |
+
self.hc_iters = args.hc_sinkhorn_iters
|
| 40 |
+
self.hc_eps = args.hc_eps
|
| 41 |
+
|
| 42 |
+
self.attn = Attention(layer_id, args)
|
| 43 |
+
self.ffn = MoE(layer_id, args)
|
| 44 |
+
self.attn_norm = RMSNorm(args.dim, args.norm_eps)
|
| 45 |
+
self.ffn_norm = RMSNorm(args.dim, args.norm_eps)
|
| 46 |
+
|
| 47 |
+
mix_hc = (2 + args.hc_mult) * args.hc_mult
|
| 48 |
+
hc_dim = args.hc_mult * args.dim
|
| 49 |
+
self.hc_attn_fn = mx.zeros((mix_hc, hc_dim), dtype=mx.float32)
|
| 50 |
+
self.hc_ffn_fn = mx.zeros((mix_hc, hc_dim), dtype=mx.float32)
|
| 51 |
+
self.hc_attn_base = mx.zeros((mix_hc,), dtype=mx.float32)
|
| 52 |
+
self.hc_ffn_base = mx.zeros((mix_hc,), dtype=mx.float32)
|
| 53 |
+
self.hc_attn_scale = mx.zeros((3,), dtype=mx.float32)
|
| 54 |
+
self.hc_ffn_scale = mx.zeros((3,), dtype=mx.float32)
|
| 55 |
+
|
| 56 |
+
def __call__(self, x: mx.array, input_ids: mx.array | None = None,
|
| 57 |
+
cache=None) -> mx.array:
|
| 58 |
+
residual = x
|
| 59 |
+
h, post, comb = hc_pre(x, self.hc_attn_fn, self.hc_attn_scale, self.hc_attn_base,
|
| 60 |
+
self.hc_mult, self.hc_iters, self.norm_eps, self.hc_eps)
|
| 61 |
+
h = self.attn(self.attn_norm(h), cache)
|
| 62 |
+
x = hc_post(h, residual, post, comb)
|
| 63 |
+
|
| 64 |
+
residual = x
|
| 65 |
+
h, post, comb = hc_pre(x, self.hc_ffn_fn, self.hc_ffn_scale, self.hc_ffn_base,
|
| 66 |
+
self.hc_mult, self.hc_iters, self.norm_eps, self.hc_eps)
|
| 67 |
+
h = self.ffn(self.ffn_norm(h), input_ids)
|
| 68 |
+
return hc_post(h, residual, post, comb)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
class Model(nn.Module):
|
| 72 |
+
"""embed -> expand to hc copies -> blocks -> collapse -> norm -> logits."""
|
| 73 |
+
|
| 74 |
+
def __init__(self, args: ModelArgs):
|
| 75 |
+
super().__init__()
|
| 76 |
+
self.args = args
|
| 77 |
+
self.hc_mult = args.hc_mult
|
| 78 |
+
self.embed = nn.Embedding(args.vocab_size, args.dim)
|
| 79 |
+
self.layers = [Block(i, args) for i in range(args.n_layers)]
|
| 80 |
+
self.norm = RMSNorm(args.dim, args.norm_eps)
|
| 81 |
+
self.head = nn.Linear(args.dim, args.vocab_size, bias=False)
|
| 82 |
+
|
| 83 |
+
self.hc_head_fn = mx.zeros((args.hc_mult, args.hc_mult * args.dim), dtype=mx.float32)
|
| 84 |
+
self.hc_head_base = mx.zeros((args.hc_mult,), dtype=mx.float32)
|
| 85 |
+
self.hc_head_scale = mx.zeros((1,), dtype=mx.float32)
|
| 86 |
+
|
| 87 |
+
def __call__(self, input_ids: mx.array, last_logit_only: bool = False,
|
| 88 |
+
cache=None) -> mx.array:
|
| 89 |
+
h = self.embed(input_ids)
|
| 90 |
+
# the four Hyper-Connection streams start as identical copies
|
| 91 |
+
h = mx.broadcast_to(h[:, :, None, :], (*h.shape[:2], self.hc_mult, h.shape[-1]))
|
| 92 |
+
|
| 93 |
+
for i, layer in enumerate(self.layers):
|
| 94 |
+
h = layer(h, input_ids, cache[i] if cache is not None else None)
|
| 95 |
+
|
| 96 |
+
h = hc_head(h, self.hc_head_fn, self.hc_head_scale, self.hc_head_base,
|
| 97 |
+
self.args.norm_eps, self.args.hc_eps)
|
| 98 |
+
h = self.norm(h)
|
| 99 |
+
if last_logit_only:
|
| 100 |
+
h = h[:, -1:]
|
| 101 |
+
# the reference keeps the LM head in fp32
|
| 102 |
+
return self.head(h.astype(mx.float32))
|
deepseek_v4_mlx/moe.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""DeepSeek-V4 MoE: hash-or-score routing, clamped-SwiGLU experts, one shared expert.
|
| 2 |
+
|
| 3 |
+
Two things differ from every other MoE in ``mlx-lm``:
|
| 4 |
+
|
| 5 |
+
* **The first ``n_hash_layers`` layers do not score experts at all.** They read the
|
| 6 |
+
six expert indices straight out of a ``tid2eid[vocab_size, n_activated]`` table
|
| 7 |
+
indexed by *token id*, so routing there is independent of context.
|
| 8 |
+
* **The selection bias does not reach the routing weights.** Scores are captured
|
| 9 |
+
*before* the bias is added; the bias only reorders the top-k. Applying it to the
|
| 10 |
+
weights too is the obvious mistake and would be invisible on a smoke test.
|
| 11 |
+
|
| 12 |
+
Routing weights are applied after ``down_proj`` rather than before it as in the
|
| 13 |
+
reference. ``down_proj`` is linear and the weight is one scalar per token, so
|
| 14 |
+
``w2(w * h) == w * w2(h)``; doing it afterwards is what lets the batched
|
| 15 |
+
:class:`SwitchGLU` gather-matmul be used instead of a 256-iteration Python loop.
|
| 16 |
+
|
| 17 |
+
That rewrite sums each token's expert outputs, whereas the reference scatters them.
|
| 18 |
+
The two agree as long as a token's expert indices are distinct — which holds for
|
| 19 |
+
this checkpoint: every sampled ``tid2eid`` row contains 6 distinct experts, and
|
| 20 |
+
scored routing takes a top-k, which cannot repeat.
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
from __future__ import annotations
|
| 24 |
+
|
| 25 |
+
import mlx.core as mx
|
| 26 |
+
import mlx.nn as nn
|
| 27 |
+
from mlx_lm.models.switch_layers import SwitchGLU
|
| 28 |
+
|
| 29 |
+
from .config import ModelArgs
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class ClampedSwiGLU(nn.Module):
|
| 33 |
+
"""SwiGLU with V4's asymmetric clamp: ``up`` two-sided, ``gate`` upper-only.
|
| 34 |
+
|
| 35 |
+
Called as ``activation(x_up, x_gate)`` to match ``SwitchGLU``.
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
def __init__(self, limit: float = 0.0):
|
| 39 |
+
super().__init__()
|
| 40 |
+
self.limit = limit
|
| 41 |
+
|
| 42 |
+
def __call__(self, x, gate):
|
| 43 |
+
if self.limit > 0:
|
| 44 |
+
x = mx.clip(x, -self.limit, self.limit)
|
| 45 |
+
gate = mx.minimum(gate, self.limit)
|
| 46 |
+
return nn.silu(gate) * x
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class Gate(nn.Module):
|
| 50 |
+
"""Routes each token to ``n_activated_experts`` experts.
|
| 51 |
+
|
| 52 |
+
``hash=True`` layers use the ``tid2eid`` lookup and carry no bias; scored
|
| 53 |
+
layers carry a per-expert selection bias.
|
| 54 |
+
"""
|
| 55 |
+
|
| 56 |
+
def __init__(self, layer_id: int, args: ModelArgs):
|
| 57 |
+
super().__init__()
|
| 58 |
+
self.topk = args.n_activated_experts
|
| 59 |
+
self.score_func = args.score_func
|
| 60 |
+
self.route_scale = args.route_scale
|
| 61 |
+
self.hash = args.is_hash_layer(layer_id)
|
| 62 |
+
n_exp = args.n_experts_for(layer_id)
|
| 63 |
+
self.weight = mx.zeros((n_exp, args.dim))
|
| 64 |
+
if self.hash:
|
| 65 |
+
self.tid2eid = mx.zeros((args.vocab_size, args.n_activated_experts), dtype=mx.int32)
|
| 66 |
+
else:
|
| 67 |
+
self.bias = mx.zeros((n_exp,), dtype=mx.float32)
|
| 68 |
+
|
| 69 |
+
def __call__(self, x: mx.array, input_ids: mx.array | None = None):
|
| 70 |
+
scores = x.astype(mx.float32) @ self.weight.astype(mx.float32).T
|
| 71 |
+
if self.score_func == "softmax":
|
| 72 |
+
scores = mx.softmax(scores, axis=-1)
|
| 73 |
+
elif self.score_func == "sigmoid":
|
| 74 |
+
scores = mx.sigmoid(scores)
|
| 75 |
+
else: # sqrtsoftplus
|
| 76 |
+
scores = mx.sqrt(nn.softplus(scores))
|
| 77 |
+
|
| 78 |
+
original_scores = scores
|
| 79 |
+
if not self.hash:
|
| 80 |
+
# bias shifts *selection* only — weights are read from original_scores
|
| 81 |
+
scores = scores + self.bias
|
| 82 |
+
indices = mx.argpartition(-scores, self.topk - 1, axis=-1)[..., :self.topk]
|
| 83 |
+
# argpartition does not order within the partition; sort so the expert
|
| 84 |
+
# order matches topk() and the gathered weights line up.
|
| 85 |
+
part = mx.take_along_axis(scores, indices, axis=-1)
|
| 86 |
+
order = mx.argsort(-part, axis=-1)
|
| 87 |
+
indices = mx.take_along_axis(indices, order, axis=-1)
|
| 88 |
+
else:
|
| 89 |
+
indices = self.tid2eid[input_ids]
|
| 90 |
+
|
| 91 |
+
weights = mx.take_along_axis(original_scores, indices, axis=-1)
|
| 92 |
+
if self.score_func != "softmax":
|
| 93 |
+
weights = weights / mx.sum(weights, axis=-1, keepdims=True)
|
| 94 |
+
weights = weights * self.route_scale
|
| 95 |
+
return weights, indices
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
class MoE(nn.Module):
|
| 99 |
+
"""256 routed experts (top-6) plus 1 always-on shared expert."""
|
| 100 |
+
|
| 101 |
+
def __init__(self, layer_id: int, args: ModelArgs):
|
| 102 |
+
super().__init__()
|
| 103 |
+
self.dim = args.dim
|
| 104 |
+
self.gate = Gate(layer_id, args)
|
| 105 |
+
act = ClampedSwiGLU(args.swiglu_limit)
|
| 106 |
+
self.experts = SwitchGLU(args.dim, args.moe_inter_dim,
|
| 107 |
+
args.n_experts_for(layer_id), activation=act)
|
| 108 |
+
self.shared_experts = SharedExpert(args.dim, args.moe_inter_dim, args.swiglu_limit)
|
| 109 |
+
|
| 110 |
+
def __call__(self, x: mx.array, input_ids: mx.array | None = None) -> mx.array:
|
| 111 |
+
shape = x.shape
|
| 112 |
+
xf = x.reshape(-1, self.dim)
|
| 113 |
+
ids = input_ids.reshape(-1) if input_ids is not None else None
|
| 114 |
+
|
| 115 |
+
weights, indices = self.gate(xf, ids)
|
| 116 |
+
y = self.experts(xf, indices) # [tokens, topk, dim]
|
| 117 |
+
y = (y * weights[..., None].astype(y.dtype)).sum(axis=-2)
|
| 118 |
+
y = y + self.shared_experts(xf)
|
| 119 |
+
return y.reshape(shape).astype(x.dtype)
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
class SharedExpert(nn.Module):
|
| 123 |
+
"""The always-on expert. Same clamped SwiGLU, no routing weight."""
|
| 124 |
+
|
| 125 |
+
def __init__(self, dim: int, inter_dim: int, limit: float = 0.0):
|
| 126 |
+
super().__init__()
|
| 127 |
+
self.w1 = nn.Linear(dim, inter_dim, bias=False)
|
| 128 |
+
self.w2 = nn.Linear(inter_dim, dim, bias=False)
|
| 129 |
+
self.w3 = nn.Linear(dim, inter_dim, bias=False)
|
| 130 |
+
self.limit = limit
|
| 131 |
+
|
| 132 |
+
def __call__(self, x: mx.array) -> mx.array:
|
| 133 |
+
dtype = x.dtype
|
| 134 |
+
gate = self.w1(x).astype(mx.float32)
|
| 135 |
+
up = self.w3(x).astype(mx.float32)
|
| 136 |
+
if self.limit > 0:
|
| 137 |
+
up = mx.clip(up, -self.limit, self.limit)
|
| 138 |
+
gate = mx.minimum(gate, self.limit)
|
| 139 |
+
h = nn.silu(gate) * up
|
| 140 |
+
return self.w2(h.astype(dtype))
|
deepseek_v4_mlx/sparse_attention.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Gather-based sparse attention — the MLX stand-in for the TileLang kernel.
|
| 2 |
+
|
| 3 |
+
Each query position attends to an explicit list of KV indices rather than a
|
| 4 |
+
contiguous range. Semantics transcribed from ``sparse_attn_kernel`` in
|
| 5 |
+
``inference/kernel.py``:
|
| 6 |
+
|
| 7 |
+
* ``kv`` is a **single** shared vector per position — MLA, so key and value are
|
| 8 |
+
the same tensor, not two projections;
|
| 9 |
+
* an index of ``-1`` means "not visible": its logit is ``-inf`` and it contributes
|
| 10 |
+
nothing;
|
| 11 |
+
* the learned ``attn_sink`` enters the softmax **denominator only**. It carries no
|
| 12 |
+
value vector, so a head whose real logits are all small attends to essentially
|
| 13 |
+
nothing rather than being forced to distribute weight over whatever is present.
|
| 14 |
+
|
| 15 |
+
The reference computes this with an online (FlashAttention-style) softmax for
|
| 16 |
+
memory reasons. A plain max-subtracted softmax is numerically equivalent; queries
|
| 17 |
+
are chunked here so the gathered ``[b, chunk, k, d]`` block stays bounded.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
import mlx.core as mx
|
| 23 |
+
|
| 24 |
+
NEG_INF = -1e30
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _gather_kv(kv: mx.array, idx: mx.array) -> mx.array:
|
| 28 |
+
"""``kv`` [b, n, d], ``idx`` [b, m, k] -> [b, m, k, d], row 0 for negative idx."""
|
| 29 |
+
b, n, d = kv.shape
|
| 30 |
+
flat = kv.reshape(b * n, d)
|
| 31 |
+
safe = mx.maximum(idx, 0).astype(mx.int32)
|
| 32 |
+
base = (mx.arange(b, dtype=mx.int32) * n).reshape(b, 1, 1)
|
| 33 |
+
return flat[(safe + base).reshape(-1)].reshape(*idx.shape, d)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def sparse_attn(q: mx.array, kv: mx.array, attn_sink: mx.array, topk_idxs: mx.array,
|
| 37 |
+
softmax_scale: float, chunk: int = 256) -> mx.array:
|
| 38 |
+
"""Attend each query to its own list of KV positions.
|
| 39 |
+
|
| 40 |
+
``q`` [b, m, h, d]
|
| 41 |
+
``kv`` [b, n, d] shared key/value (MLA)
|
| 42 |
+
``attn_sink`` [h] per-head null-attention logit
|
| 43 |
+
``topk_idxs`` [b, m, k] int32, -1 = masked
|
| 44 |
+
returns [b, m, h, d]
|
| 45 |
+
"""
|
| 46 |
+
b, m, h, d = q.shape
|
| 47 |
+
sink = attn_sink.astype(mx.float32).reshape(1, 1, h, 1)
|
| 48 |
+
|
| 49 |
+
outs = []
|
| 50 |
+
for start in range(0, m, chunk):
|
| 51 |
+
stop = min(start + chunk, m)
|
| 52 |
+
qc = q[:, start:stop].astype(mx.float32) # [b, c, h, d]
|
| 53 |
+
ic = topk_idxs[:, start:stop] # [b, c, k]
|
| 54 |
+
kvc = _gather_kv(kv, ic).astype(mx.float32) # [b, c, k, d]
|
| 55 |
+
|
| 56 |
+
# [b, c, h, k]
|
| 57 |
+
logits = mx.einsum("bchd,bckd->bchk", qc, kvc) * softmax_scale
|
| 58 |
+
valid = (ic >= 0)[:, :, None, :]
|
| 59 |
+
logits = mx.where(valid, logits, NEG_INF)
|
| 60 |
+
|
| 61 |
+
mmax = mx.max(logits, axis=-1, keepdims=True) # [b, c, h, 1]
|
| 62 |
+
# the sink competes with the real logits for the running max
|
| 63 |
+
mmax = mx.maximum(mmax, sink)
|
| 64 |
+
w = mx.exp(logits - mmax)
|
| 65 |
+
w = mx.where(valid, w, 0.0)
|
| 66 |
+
denom = mx.sum(w, axis=-1, keepdims=True) + mx.exp(sink - mmax)
|
| 67 |
+
|
| 68 |
+
o = mx.einsum("bchk,bckd->bchd", w, kvc) / denom
|
| 69 |
+
outs.append(o.astype(q.dtype))
|
| 70 |
+
|
| 71 |
+
return mx.concatenate(outs, axis=1) if len(outs) > 1 else outs[0]
|
generation_config.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_from_model_config": true,
|
| 3 |
+
"bos_token_id": 0,
|
| 4 |
+
"eos_token_id": 1,
|
| 5 |
+
"do_sample": true,
|
| 6 |
+
"temperature": 1.0,
|
| 7 |
+
"top_p": 1.0,
|
| 8 |
+
"transformers_version": "4.46.3"
|
| 9 |
+
}
|
model-00001-of-00025.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:69c564c2ec6e4a688d7460cdc87bf5e9268b8a1c8a95807c70b9d5d049669a2c
|
| 3 |
+
size 5436806040
|
model-00002-of-00025.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4d56f47f0d43b87b265347a2bc6224c1f8ee42ea26f07d168819f09e7a985c84
|
| 3 |
+
size 5355630769
|
model-00003-of-00025.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8ce3abddaee4ff1e739746b4fb9f07e8e2830072d979c5d444b5fc063a30cf84
|
| 3 |
+
size 5032677715
|
model-00004-of-00025.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e17646e2ebb2f7d3076b926c68b4d826241b0232780f160e1307a574ef46efbd
|
| 3 |
+
size 5020874379
|
model-00005-of-00025.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:80cee940cc1b8275796f1b7e5b21b1e297c22d75e54c007fa67b8fdcbef8bd7d
|
| 3 |
+
size 5643415454
|
model-00006-of-00025.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c7047e1a3b400b438181ec89cf58a34817c02aaa33ce7732b6db00454b612324
|
| 3 |
+
size 5023215191
|
model-00007-of-00025.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b6189b80d7742493395872cf2c895222849eff90950a3b2631b5222103fe83f0
|
| 3 |
+
size 5699008910
|
model-00008-of-00025.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1026a8ecd7dcf739660bf91a8db9c4461ba4aa61e6de4360367d27630e00e5f4
|
| 3 |
+
size 5004876411
|
model-00009-of-00025.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:81b8e752cd30250358cbbe42268c8eff90ff5aabb57a532a5e008155dc056834
|
| 3 |
+
size 5417984314
|
model-00010-of-00025.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4e56fd8c4fe18036336a8a57252d134e3da67b94680b55f06def4e249f84823c
|
| 3 |
+
size 5000256653
|
model-00011-of-00025.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:983caf5380dc6f369338f7cb47166cf91c021e1009e397ad5f85ca2c32458e01
|
| 3 |
+
size 5029122176
|
model-00012-of-00025.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d406af482af4b3a2d06065fabb6cba2dd0282c4220679bff04488c16ccfc0b9c
|
| 3 |
+
size 5603569790
|
model-00013-of-00025.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:abb96ea31791fc15440c101a5c8f2cf36ce1a188060ffabb047fa024426f4988
|
| 3 |
+
size 5053432424
|
model-00014-of-00025.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0a6f8679d4085160fb8bba4b7b812988ad7542fd97682c264ea05940f6c088bb
|
| 3 |
+
size 5063649550
|
model-00015-of-00025.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:02e03eba5e371337adf5f6bc011feea5f7629a86ae7b4b1cad8ee523cfe37467
|
| 3 |
+
size 5578617059
|
model-00016-of-00025.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7b79881e04d45a988e7f9c0ae091552f473bc35441ea8c58dfcac491742aba2e
|
| 3 |
+
size 5742264789
|
model-00017-of-00025.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ca3b4e2a6334c10c695425c820caac128cdb96e1cca917412d9943158ab6f7fb
|
| 3 |
+
size 5246779122
|
model-00018-of-00025.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2299145c9c09e8199c3aea5a7e4ad3487f8097b727c7224c7df887daee23bc5f
|
| 3 |
+
size 5226205499
|
model-00019-of-00025.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3e37047aa189522115706bb05a838a11c687dd99e36c3ea0d2ca18abf366a7ff
|
| 3 |
+
size 5104788615
|
model-00020-of-00025.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e95a73c4de94ec15729b64e120df35ec6cada6407cb5342524f6a128905313c7
|
| 3 |
+
size 5686325586
|
model-00021-of-00025.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3c9178c606f1fe02b564f959659e73a33da75f85c2881b8f5c9c7ecf5424ed3b
|
| 3 |
+
size 5732777176
|
model-00022-of-00025.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:33490aa69fcd26c407c5dc438d1f46809f46a1a155ee99013e7e14dbe866b398
|
| 3 |
+
size 5003800685
|
model-00023-of-00025.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:199606bb4212972c64a25fe10f5ad1c3e25f74a5061b34470003fa39aa101995
|
| 3 |
+
size 5008867619
|
model-00024-of-00025.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:bd36364122fe7ee8e945225f9af580caaa94ef11c55ddf0ef2eea114b3000cf6
|
| 3 |
+
size 5692721355
|
model-00025-of-00025.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3fb1cc7fe173e967e1d4bc191b82fb32da1c9574f98b3a5179e26803184e8fca
|
| 3 |
+
size 1579230717
|
model.safetensors.index.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
tokenizer.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
tokenizer_config.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"add_bos_token": false,
|
| 3 |
+
"add_eos_token": false,
|
| 4 |
+
"bos_token": {
|
| 5 |
+
"__type": "AddedToken",
|
| 6 |
+
"content": "<|begin▁of▁sentence|>",
|
| 7 |
+
"lstrip": false,
|
| 8 |
+
"normalized": true,
|
| 9 |
+
"rstrip": false,
|
| 10 |
+
"single_word": false
|
| 11 |
+
},
|
| 12 |
+
"clean_up_tokenization_spaces": false,
|
| 13 |
+
"eos_token": {
|
| 14 |
+
"__type": "AddedToken",
|
| 15 |
+
"content": "<|end▁of▁sentence|>",
|
| 16 |
+
"lstrip": false,
|
| 17 |
+
"normalized": true,
|
| 18 |
+
"rstrip": false,
|
| 19 |
+
"single_word": false
|
| 20 |
+
},
|
| 21 |
+
"legacy": true,
|
| 22 |
+
"model_max_length": 1048576,
|
| 23 |
+
"pad_token": {
|
| 24 |
+
"__type": "AddedToken",
|
| 25 |
+
"content": "<|end▁of▁sentence|>",
|
| 26 |
+
"lstrip": false,
|
| 27 |
+
"normalized": true,
|
| 28 |
+
"rstrip": false,
|
| 29 |
+
"single_word": false
|
| 30 |
+
},
|
| 31 |
+
"sp_model_kwargs": {},
|
| 32 |
+
"unk_token": null,
|
| 33 |
+
"tokenizer_class": "PreTrainedTokenizerFast"
|
| 34 |
+
}
|