TiGa-RCE commited on
Commit
980b1b3
·
verified ·
1 Parent(s): ff98ffd

Upload verified Needle MLX conversion

Browse files
README.md ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ library_name: mlx
4
+ base_model: Cactus-Compute/needle
5
+ tags:
6
+ - mlx
7
+ - function-calling
8
+ - tool-use
9
+ - encoder-decoder
10
+ - apple-silicon
11
+ ---
12
+
13
+ # Needle MLX
14
+
15
+ An MLX safetensors conversion of
16
+ [Cactus-Compute/needle](https://huggingface.co/Cactus-Compute/needle), a
17
+ 26M-parameter encoder-decoder function-calling model.
18
+
19
+ ## What is included
20
+
21
+ - `model.safetensors`: 31 converted tensors, 26,315,421 parameters.
22
+ - `needle_mlx.py`: a small custom MLX inference implementation for Needle's
23
+ architecture.
24
+ - `tokenizer/`: the upstream SentencePiece tokenizer files.
25
+ - `config.json` and `manifest.json`: architecture and conversion provenance.
26
+
27
+ ## Use
28
+
29
+ ```python
30
+ from needle_mlx import NeedleModel
31
+
32
+ model = NeedleModel.from_pretrained(".")
33
+ logits = model.forward([[1, 2, 3]], [[1, 4]])
34
+ ```
35
+
36
+ This is an MLX-native package, not an MLX-LM or oMLX model yet. Needle uses a
37
+ custom JAX/Flax architecture, so load it with the included runtime or add a
38
+ dedicated adapter to the serving runtime.
39
+
40
+ ## Conversion and verification
41
+
42
+ The published `needle.pkl` checkpoint was read with a restricted NumPy-only
43
+ pickle loader, converted directly to MLX safetensors, and checked
44
+ tensor-for-tensor against the source.
45
+
46
+ - Source SHA-256:
47
+ `40a32e91d1d4197bf15ba559b74f6727c342dc8746918742fc7d8e2c1f18df40`
48
+ - Converted `model.safetensors` SHA-256:
49
+ `7b9d5f0d6ddeb7fbb20f4e45f3f616919357e5d08b5778859fdb762a33d60dae`
50
+ - Verification: 31/31 tensors equal the source; an MLX encoder-decoder smoke
51
+ pass produced logits with shape `(1, 2, 8192)`.
52
+
53
+ The converter source is available at
54
+ [seeker-cyber-maker/needle-mlx-depicklinator](https://github.com/seeker-cyber-maker/needle-mlx-depicklinator).
55
+
56
+ ## Credits and license
57
+
58
+ Needle was created by Cactus Compute. See the
59
+ [upstream model card](https://huggingface.co/Cactus-Compute/needle) and
60
+ [source repository](https://github.com/cactus-compute/needle) for the model,
61
+ training details, and citation. The upstream model card publishes Needle under
62
+ the MIT license; this converted package retains that license.
config.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "vocab_size": 8192,
3
+ "d_model": 512,
4
+ "num_heads": 8,
5
+ "num_kv_heads": 4,
6
+ "num_encoder_layers": 12,
7
+ "num_decoder_layers": 8,
8
+ "d_ff": 2048,
9
+ "max_seq_len": 1024,
10
+ "pad_token_id": 0,
11
+ "rope_theta": 10000.0,
12
+ "dtype": "bfloat16",
13
+ "activation": "swiglu",
14
+ "num_memory_slots": 64,
15
+ "n_mels": 80,
16
+ "dropout_rate": 0.1,
17
+ "contrastive_dim": 128,
18
+ "enable_speech": false,
19
+ "no_feedforward": true,
20
+ "model_type": "needle_mlx",
21
+ "architectures": [
22
+ "NeedleModel"
23
+ ],
24
+ "format": "mlx-safetensors",
25
+ "format_version": 1,
26
+ "eos_token_id": 1,
27
+ "source_checkpoint": "needle.pkl",
28
+ "source_sha256": "40a32e91d1d4197bf15ba559b74f6727c342dc8746918742fc7d8e2c1f18df40"
29
+ }
manifest.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "format": "needle-mlx",
3
+ "format_version": 1,
4
+ "tensor_count": 31,
5
+ "parameter_count": 26315421,
6
+ "source_sha256": "40a32e91d1d4197bf15ba559b74f6727c342dc8746918742fc7d8e2c1f18df40",
7
+ "source_checkpoint": "needle.pkl"
8
+ }
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7b9d5f0d6ddeb7fbb20f4e45f3f616919357e5d08b5778859fdb762a33d60dae
3
+ size 52634442
needle_mlx.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Small MLX inference implementation for Cactus Needle."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import math
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import mlx.core as mx
11
+
12
+
13
+ class NeedleModel:
14
+ def __init__(self, weights: dict[str, mx.array], config: dict[str, Any]):
15
+ self.weights = weights
16
+ self.config = config
17
+ self.num_heads = int(config["num_heads"])
18
+ self.num_kv_heads = int(config["num_kv_heads"])
19
+ self.d_model = int(config["d_model"])
20
+ self.head_dim = self.d_model // self.num_heads
21
+
22
+ @classmethod
23
+ def from_pretrained(cls, path: str | Path) -> "NeedleModel":
24
+ path = Path(path)
25
+ config = json.loads((path / "config.json").read_text())
26
+ weights = mx.load(str(path / "model.safetensors"))
27
+ return cls(weights, config)
28
+
29
+ def _weight(self, name: str, layer: int | None = None) -> mx.array:
30
+ value = self.weights[name]
31
+ return value if layer is None else value[layer]
32
+
33
+ @staticmethod
34
+ def _linear(x: mx.array, kernel: mx.array, bias: mx.array | None = None) -> mx.array:
35
+ output = mx.matmul(x, kernel)
36
+ return output if bias is None else output + bias
37
+
38
+ @staticmethod
39
+ def _zcrms_norm(x: mx.array, scale: mx.array) -> mx.array:
40
+ rms = mx.sqrt(mx.mean(mx.square(x.astype(mx.float32)), axis=-1, keepdims=True) + 1e-6)
41
+ return ((1.0 + scale) * x / rms).astype(x.dtype)
42
+
43
+ def _rope(self, x: mx.array) -> mx.array:
44
+ sequence_length = x.shape[2]
45
+ half_dim = self.head_dim // 2
46
+ positions = mx.arange(sequence_length, dtype=mx.float32)
47
+ inv_freq = 1.0 / (
48
+ float(self.config["rope_theta"])
49
+ ** (mx.arange(0, self.head_dim, 2, dtype=mx.float32) / self.head_dim)
50
+ )
51
+ angles = mx.outer(positions, inv_freq)
52
+ cos = mx.reshape(mx.cos(angles), (1, 1, sequence_length, half_dim))
53
+ sin = mx.reshape(mx.sin(angles), (1, 1, sequence_length, half_dim))
54
+ first, second = x[..., :half_dim], x[..., half_dim:]
55
+ return mx.concatenate([first * cos - second * sin, second * cos + first * sin], axis=-1)
56
+
57
+ def _attention(
58
+ self,
59
+ q_input: mx.array,
60
+ kv_input: mx.array,
61
+ prefix: str,
62
+ layer: int,
63
+ mask: mx.array | None,
64
+ rope: bool,
65
+ ) -> mx.array:
66
+ q = self._linear(q_input, self._weight(f"{prefix}/q_proj/kernel", layer))
67
+ k = self._linear(kv_input, self._weight(f"{prefix}/k_proj/kernel", layer))
68
+ v = self._linear(kv_input, self._weight(f"{prefix}/v_proj/kernel", layer))
69
+ batch = q.shape[0]
70
+ q = mx.transpose(mx.reshape(q, (batch, -1, self.num_heads, self.head_dim)), (0, 2, 1, 3))
71
+ k = mx.transpose(mx.reshape(k, (batch, -1, self.num_kv_heads, self.head_dim)), (0, 2, 1, 3))
72
+ v = mx.transpose(mx.reshape(v, (batch, -1, self.num_kv_heads, self.head_dim)), (0, 2, 1, 3))
73
+ q = self._zcrms_norm(q, self._weight(f"{prefix}/q_norm/scale", layer))
74
+ k = self._zcrms_norm(k, self._weight(f"{prefix}/k_norm/scale", layer))
75
+ if self.num_heads != self.num_kv_heads:
76
+ repeats = self.num_heads // self.num_kv_heads
77
+ k = mx.repeat(k, repeats, axis=1)
78
+ v = mx.repeat(v, repeats, axis=1)
79
+ if rope:
80
+ q, k = self._rope(q), self._rope(k)
81
+ scores = mx.matmul(q.astype(mx.float32), mx.transpose(k.astype(mx.float32), (0, 1, 3, 2)))
82
+ scores = scores / math.sqrt(self.head_dim)
83
+ if mask is not None:
84
+ scores = mx.where(mask, scores, mx.array(-1e9, dtype=mx.float32))
85
+ output = mx.matmul(mx.softmax(scores, axis=-1).astype(v.dtype), v)
86
+ output = mx.reshape(mx.transpose(output, (0, 2, 1, 3)), (batch, -1, self.d_model))
87
+ return self._linear(output, self._weight(f"{prefix}/out_proj/kernel", layer))
88
+
89
+ def encode(self, source_tokens: mx.array) -> tuple[mx.array, mx.array]:
90
+ source_tokens = mx.array(source_tokens, dtype=mx.int32)
91
+ padding_mask = source_tokens != int(self.config["pad_token_id"])
92
+ mask = mx.reshape(padding_mask, (source_tokens.shape[0], 1, 1, source_tokens.shape[1]))
93
+ x = self.weights["embedding/embedding"][source_tokens] * math.sqrt(self.d_model)
94
+ for layer in range(int(self.config["num_encoder_layers"])):
95
+ prefix = "encoder/layers/EncoderBlock_0"
96
+ residual = x
97
+ x = self._zcrms_norm(x, self._weight(f"{prefix}/ZCRMSNorm_0/scale", layer))
98
+ x = self._attention(x, x, f"{prefix}/self_attn", layer, mask, rope=True)
99
+ gate = mx.sigmoid(self._weight(f"{prefix}/attn_gate", layer))
100
+ x = residual + gate * x
101
+ return self._zcrms_norm(x, self.weights["encoder/final_norm/scale"]), mask
102
+
103
+ def decode(self, target_tokens: mx.array, encoder_out: mx.array, cross_mask: mx.array) -> mx.array:
104
+ target_tokens = mx.array(target_tokens, dtype=mx.int32)
105
+ length = target_tokens.shape[1]
106
+ causal = mx.tril(mx.ones((length, length), dtype=mx.bool_))
107
+ target_valid = target_tokens != int(self.config["pad_token_id"])
108
+ self_mask = mx.reshape(causal, (1, 1, length, length)) & mx.reshape(
109
+ target_valid, (target_tokens.shape[0], 1, 1, length)
110
+ )
111
+ x = self.weights["embedding/embedding"][target_tokens] * math.sqrt(self.d_model)
112
+ prefix = "decoder/layers/DecoderBlock_0"
113
+ for layer in range(int(self.config["num_decoder_layers"])):
114
+ residual = x
115
+ x = self._zcrms_norm(x, self._weight(f"{prefix}/ZCRMSNorm_0/scale", layer))
116
+ x = self._attention(x, x, f"{prefix}/self_attn", layer, self_mask, rope=True)
117
+ x = residual + mx.sigmoid(self._weight(f"{prefix}/self_attn_gate", layer)) * x
118
+ residual = x
119
+ x = self._zcrms_norm(x, self._weight(f"{prefix}/ZCRMSNorm_1/scale", layer))
120
+ x = self._attention(x, encoder_out, f"{prefix}/cross_attn", layer, cross_mask, rope=False)
121
+ x = residual + mx.sigmoid(self._weight(f"{prefix}/cross_attn_gate", layer)) * x
122
+ x = self._zcrms_norm(x, self.weights["decoder/ZCRMSNorm_0/scale"])
123
+ return mx.matmul(x.astype(mx.float32), mx.transpose(self.weights["embedding/embedding"].astype(mx.float32)))
124
+
125
+ def forward(self, source_tokens: mx.array, target_tokens: mx.array) -> mx.array:
126
+ encoder_out, cross_mask = self.encode(source_tokens)
127
+ return self.decode(target_tokens, encoder_out, cross_mask)
tokenizer/needle.model ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0823f5b9133c68a8140addc5d7a425fa9119c4c8cb4a550363b4bffa4ba1c8c7
3
+ size 124960
tokenizer/needle.vocab ADDED
The diff for this file is too large to render. See raw diff