pipenetwork commited on
Commit
afed642
·
verified ·
1 Parent(s): a14d438

Add files using upload-large-folder tool

Browse files
Files changed (46) hide show
  1. README.md +136 -0
  2. config.json +122 -0
  3. deepseek_v4_mlx/attention.py +239 -0
  4. deepseek_v4_mlx/cache.py +142 -0
  5. deepseek_v4_mlx/compressor.py +154 -0
  6. deepseek_v4_mlx/config.py +152 -0
  7. deepseek_v4_mlx/convert.py +218 -0
  8. deepseek_v4_mlx/dequant.py +92 -0
  9. deepseek_v4_mlx/fakequant.py +95 -0
  10. deepseek_v4_mlx/generate.py +37 -0
  11. deepseek_v4_mlx/hyper_connections.py +118 -0
  12. deepseek_v4_mlx/indexer.py +113 -0
  13. deepseek_v4_mlx/layers.py +132 -0
  14. deepseek_v4_mlx/load.py +68 -0
  15. deepseek_v4_mlx/model.py +102 -0
  16. deepseek_v4_mlx/moe.py +140 -0
  17. deepseek_v4_mlx/sparse_attention.py +71 -0
  18. generation_config.json +9 -0
  19. model-00001-of-00025.safetensors +3 -0
  20. model-00002-of-00025.safetensors +3 -0
  21. model-00003-of-00025.safetensors +3 -0
  22. model-00004-of-00025.safetensors +3 -0
  23. model-00005-of-00025.safetensors +3 -0
  24. model-00006-of-00025.safetensors +3 -0
  25. model-00007-of-00025.safetensors +3 -0
  26. model-00008-of-00025.safetensors +3 -0
  27. model-00009-of-00025.safetensors +3 -0
  28. model-00010-of-00025.safetensors +3 -0
  29. model-00011-of-00025.safetensors +3 -0
  30. model-00012-of-00025.safetensors +3 -0
  31. model-00013-of-00025.safetensors +3 -0
  32. model-00014-of-00025.safetensors +3 -0
  33. model-00015-of-00025.safetensors +3 -0
  34. model-00016-of-00025.safetensors +3 -0
  35. model-00017-of-00025.safetensors +3 -0
  36. model-00018-of-00025.safetensors +3 -0
  37. model-00019-of-00025.safetensors +3 -0
  38. model-00020-of-00025.safetensors +3 -0
  39. model-00021-of-00025.safetensors +3 -0
  40. model-00022-of-00025.safetensors +3 -0
  41. model-00023-of-00025.safetensors +3 -0
  42. model-00024-of-00025.safetensors +3 -0
  43. model-00025-of-00025.safetensors +3 -0
  44. model.safetensors.index.json +0 -0
  45. tokenizer.json +0 -0
  46. 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
+ }