Ilides commited on
Commit
e72cbe1
·
verified ·
1 Parent(s): b5f69d9

Upload folder using huggingface_hub

Browse files
README.md ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ library_name: mlx
3
+ pipeline_tag: text-generation
4
+ license: apache-2.0
5
+ base_model: Ilides/cortex-0.3-0.02b
6
+ tags:
7
+ - cortex
8
+ - mlx
9
+ - apple-silicon
10
+ ---
11
+
12
+ # cortex-0.3-0.02b · MLX (Apple Silicon)
13
+
14
+ Versión **MLX** (Metal) de [Ilides/cortex-0.3-0.02b](https://huggingface.co/Ilides/cortex-0.3-0.02b), el GPT conversacional de 20.6 M parámetros entrenado desde cero en NumPy puro. Ejecuta nativamente en Apple Silicon sin PyTorch.
15
+
16
+ ## Requisitos
17
+
18
+ ```bash
19
+ pip install mlx safetensors transformers regex
20
+ ```
21
+
22
+ ## Uso
23
+
24
+ ```bash
25
+ # batería de prompts de prueba (por defecto)
26
+ python generate_mlx.py
27
+
28
+ # una pregunta
29
+ python generate_mlx.py "¿Qué eres?" --max-new 128
30
+
31
+ # chat interactivo (escribe "salir" para terminar)
32
+ python generate_mlx.py --interactive --temperature 0.8
33
+ ```
34
+
35
+ Bundle autocontenido: `weights.safetensors` (45 tensores) + `model.py` (arquitectura) + `generate_mlx.py` (CLI) + tokenizer. No depende de `mlx-lm` ni de código externo del Hub.
36
+
37
+ ## Verificación
38
+
39
+ Paridad de logits MLX vs NumPy (implementación de referencia): **max|Δ| = 3.65e-05** (< 1e-4). Tokens EOS efectivos: `{0, 6}`.
40
+
41
+ ## Arquitectura
42
+
43
+ `cortex`: embeddings de posición aprendidos (sin RoPE), RMSNorm + atención multi-cabeza + SwiGLU, embeddings atados (`tie_word_embeddings`). Contexto 512, 6 capas, d_model 192, vocab 16384 (BPE byte-level).
config.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_name_or_path": "cortex-0.3-0.02b",
3
+ "architectures": [
4
+ "CortexForCausalLM"
5
+ ],
6
+ "auto_map": {
7
+ "AutoConfig": "configuration_cortex.CortexConfig",
8
+ "AutoModelForCausalLM": "modeling_cortex.CortexForCausalLM",
9
+ "AutoTokenizer": "tokenization_cortex.CortexTokenizer"
10
+ },
11
+ "bos_token_id": 0,
12
+ "dtype": "float32",
13
+ "eos_token_id": 0,
14
+ "hidden_size": 384,
15
+ "initializer_std": 0.02,
16
+ "intermediate_size": 1536,
17
+ "max_position_embeddings": 512,
18
+ "model_type": "cortex",
19
+ "num_attention_heads": 8,
20
+ "num_hidden_layers": 6,
21
+ "pad_token_id": 1,
22
+ "rms_norm_eps": 1e-05,
23
+ "transformers_version": "4.57.6",
24
+ "vocab_size": 16384
25
+ }
generate_mlx.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Chat con Cortex-1 en MLX (Apple Silicon, Metal).
3
+
4
+ Uso:
5
+ python generate_mlx.py # batería de prueba
6
+ python generate_mlx.py "tu pregunta" # one-shot
7
+ python generate_mlx.py --interactive # REPL
8
+ Requisitos: pip install mlx transformers regex safetensors
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import os
14
+ import sys
15
+
16
+ import mlx.core as mx
17
+ import numpy as np
18
+
19
+ HERE = os.path.dirname(os.path.abspath(__file__))
20
+ sys.path.insert(0, HERE)
21
+
22
+ from model import Model
23
+ from tokenization_cortex import CortexTokenizer
24
+
25
+ SYSTEM = "Eres Cortex, un asistente en español. Habla claro y conciso."
26
+ BATERIA = [
27
+ "Hola, ¿qué eres y quién te creó?",
28
+ "Explícame la fotosíntesis en dos frases.",
29
+ "¿Cuánto es 17 × 24? Razona paso a paso.",
30
+ "Escribe una frase creativa sobre un robot que aprende a pintar.",
31
+ "What is the capital of France? Answer in English.",
32
+ ]
33
+
34
+
35
+ def construir_prompt(tok, pregunta: str) -> str:
36
+ return (f"<|system|>\n{SYSTEM}\n<|end|>\n"
37
+ f"<|user|>\n{pregunta}\n<|end|>\n"
38
+ f"<|assistant|>\n")
39
+
40
+
41
+ def responder(model, tok, pregunta, temp=0.8, top_k=50, max_new=160):
42
+ prompt = construir_prompt(tok, pregunta)
43
+ ids = np.array([tok.encode(prompt, add_special_tokens=False)], dtype=np.int64)
44
+ out_ids = []
45
+ for _ in range(max_new):
46
+ logits = np.asarray(model.logits(mx.array(ids[:, -512:]))[0, -1])
47
+ if out_ids and out_ids[-1] == 0:
48
+ break
49
+ if temp <= 0:
50
+ nxt = int(np.argmax(logits))
51
+ else:
52
+ logits = logits / temp
53
+ if top_k > 0:
54
+ kth = np.partition(logits, -top_k)[-top_k]
55
+ logits = np.where(logits < kth, -np.inf, logits)
56
+ p = np.exp(logits - logits.max())
57
+ p /= p.sum()
58
+ nxt = int(np.random.choice(len(p), p=p))
59
+ out_ids.append(nxt)
60
+ ids = np.concatenate([ids, [[nxt]]], axis=1)
61
+ if nxt == 0:
62
+ break
63
+ return tok.decode(out_ids, skip_special_tokens=True).strip()
64
+
65
+
66
+ def main() -> int:
67
+ ap = argparse.ArgumentParser()
68
+ ap.add_argument("prompt", nargs="*")
69
+ ap.add_argument("--interactive", action="store_true")
70
+ ap.add_argument("--temperature", type=float, default=0.8)
71
+ ap.add_argument("--top-k", type=int, default=50)
72
+ ap.add_argument("--max-new", type=int, default=160)
73
+ args = ap.parse_args()
74
+
75
+ print(f"cargando modelo MLX desde {HERE} …", flush=True)
76
+ model = Model.load(HERE)
77
+ tok = CortexTokenizer(vocab_file=os.path.join(HERE, "vocab.json"),
78
+ merges_file=os.path.join(HERE, "merges.txt"),
79
+ model_max_length=512, unk_token="<|unk|>",
80
+ bos_token="</s>", eos_token="</s>",
81
+ pad_token="<|pad|>")
82
+ print("listo (MLX/Metal). Escribe 'salir' para terminar.\n", flush=True)
83
+
84
+ if args.interactive:
85
+ while True:
86
+ try:
87
+ q = input("TÚ > ").strip()
88
+ except (EOFError, KeyboardInterrupt):
89
+ break
90
+ if not q or q == "salir":
91
+ break
92
+ print("CORTEX >", responder(model, tok, q, args.temperature,
93
+ args.top_k, args.max_new), "\n")
94
+ return 0
95
+
96
+ for q in args.prompt or BATERIA:
97
+ print(f"TÚ > {q}")
98
+ r = responder(model, tok, q, args.temperature, args.top_k, args.max_new)
99
+ print(f"CORTEX > {r}\n")
100
+ return 0
101
+
102
+
103
+ if __name__ == "__main__":
104
+ raise SystemExit(main())
merges.txt ADDED
The diff for this file is too large to render. See raw diff
 
model.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Cortex-1 en MLX (Apple Silicon) — equivalente a la implementación NumPy/PyTorch.
2
+
3
+ Misma arquitectura: GPT pre-norm, RMSNorm, atención causal con QKV fusionado,
4
+ SwiGLU, embeddings de posición aprendidos, lm_head atado al embedding.
5
+ Paridad verificada: max|Δ logits| < 1e-4 vs el modelo NumPy original.
6
+
7
+ Uso: python generate_mlx.py "La capital de Francia es"
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+
14
+ import mlx.core as mx
15
+ import mlx.nn as nn
16
+
17
+ MODEL_FILE = "weights.safetensors"
18
+
19
+
20
+ class Attention(nn.Module):
21
+ def __init__(self, d: int, n_heads: int):
22
+ super().__init__()
23
+ self.n_heads = n_heads
24
+ self.d_head = d // n_heads
25
+ self.scale = self.d_head ** -0.5
26
+ self.qkv = nn.Linear(d, 3 * d, bias=False)
27
+ self.proj = nn.Linear(d, d, bias=False)
28
+
29
+ def __call__(self, x: mx.array) -> mx.array:
30
+ B, T, d = x.shape
31
+ qkv = self.qkv(x).reshape(B, T, 3, self.n_heads, self.d_head)
32
+ q, k, v = (qkv[:, :, i].transpose(0, 2, 1, 3) for i in range(3))
33
+ att = (q @ k.transpose(0, 1, 3, 2)) * self.scale # (B,H,T,T)
34
+ mask = mx.triu(mx.full((T, T), -mx.inf, dtype=att.dtype), k=1)
35
+ att = mx.softmax(att + mask, axis=-1)
36
+ y = (att @ v).transpose(0, 2, 1, 3).reshape(B, T, d)
37
+ return self.proj(y)
38
+
39
+
40
+ class MLP(nn.Module):
41
+ def __init__(self, d: int, d_ff: int):
42
+ super().__init__()
43
+ self.gate_proj = nn.Linear(d, d_ff, bias=False)
44
+ self.up_proj = nn.Linear(d, d_ff, bias=False)
45
+ self.down_proj = nn.Linear(d_ff, d, bias=False)
46
+
47
+ def __call__(self, x: mx.array) -> mx.array:
48
+ return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
49
+
50
+
51
+ class Block(nn.Module):
52
+ def __init__(self, d: int, n_heads: int, d_ff: int, eps: float):
53
+ super().__init__()
54
+ self.input_layernorm = nn.RMSNorm(d, eps=eps)
55
+ self.self_attn = Attention(d, n_heads)
56
+ self.post_attention_layernorm = nn.RMSNorm(d, eps=eps)
57
+ self.mlp = MLP(d, d_ff)
58
+
59
+ def __call__(self, x):
60
+ x = x + self.self_attn(self.input_layernorm(x))
61
+ return x + self.mlp(self.post_attention_layernorm(x))
62
+
63
+
64
+ class Model(nn.Module):
65
+ def __init__(self, cfg: dict):
66
+ super().__init__()
67
+ d = cfg["hidden_size"]
68
+ self.cfg = cfg
69
+ self.embed_tokens = nn.Embedding(cfg["vocab_size"], d)
70
+ self.embed_positions = nn.Embedding(cfg["max_position_embeddings"], d)
71
+ self.layers = [Block(d, cfg["num_attention_heads"],
72
+ cfg["intermediate_size"], cfg["rms_norm_eps"])
73
+ for _ in range(cfg["num_hidden_layers"])]
74
+ self.norm = nn.RMSNorm(d, eps=cfg["rms_norm_eps"])
75
+
76
+ def __call__(self, input_ids: mx.array) -> mx.array:
77
+ T = input_ids.shape[1]
78
+ h = self.embed_tokens(input_ids) + self.embed_positions(mx.arange(T))
79
+ for layer in self.layers:
80
+ h = layer(h)
81
+ return self.norm(h)
82
+
83
+ def logits(self, input_ids: mx.array) -> mx.array:
84
+ """(B,T) int -> (B,T,V); lm_head atado = embed_tokens."""
85
+ h = self(input_ids)
86
+ w = self.embed_tokens.weight # (V, d)
87
+ return h @ w.T
88
+
89
+ @classmethod
90
+ def load(cls, repo: str) -> "Model":
91
+ with open(os.path.join(repo, "config.json")) as f:
92
+ cfg = json.load(f)
93
+ model = cls(cfg)
94
+ weights = mx.load(os.path.join(repo, MODEL_FILE))
95
+ model.load_weights(list(weights.items()), strict=True)
96
+ return model
special_tokens_map.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "bos_token": "<|endoftext|>",
3
+ "eos_token": "<|endoftext|>",
4
+ "pad_token": "<|pad|>",
5
+ "unk_token": "<|unk|>",
6
+ "additional_special_tokens": [
7
+ "<|system|>",
8
+ "<|user|>",
9
+ "<|assistant|>",
10
+ "<|end|>",
11
+ "<|doc|>",
12
+ "<|code|>"
13
+ ]
14
+ }
tokenization_cortex.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tokenizer de Cortex-1 para Hugging Face (equivalente exacto al BPE byte-level propio)."""
2
+ import json
3
+ import regex
4
+ from transformers import PreTrainedTokenizer
5
+
6
+ PRETOKEN = regex.compile(
7
+ r"""'(?:[sdmt]|ll|ve|re)|[^\r\n\p{L}\p{N}]?+\p{L}+|\p{N}{1,3}| ?[^\s\p{L}\p{N}]++[\r\n]*|\s*[\r\n]|\s+(?!\S)|\s+"""
8
+ )
9
+
10
+
11
+ def bytes_to_unicode():
12
+ """Tabla GPT-2: byte -> carácter imprimible (idéntica a la del modelo propio)."""
13
+ bs = (list(range(ord("!"), ord("~") + 1)) + list(range(ord("¡"), ord("¬") + 1))
14
+ + list(range(ord("®"), ord("ÿ") + 1)))
15
+ cs = bs[:]
16
+ n = 0
17
+ for b in range(256):
18
+ if b not in bs:
19
+ bs.append(b)
20
+ cs.append(256 + n)
21
+ n += 1
22
+ return dict(zip(bs, (chr(c) for c in cs)))
23
+
24
+
25
+ _B2U = bytes_to_unicode()
26
+ _U2B = {v: k for k, v in _B2U.items()}
27
+
28
+
29
+ class CortexTokenizer(PreTrainedTokenizer):
30
+ """BPE byte-level idéntico a ``cortex.tokenizer.encoder.BPETokenizer``.
31
+
32
+ El pre-tokenizador usa el MISMO regex (poseedores y lookahead incluidos,
33
+ vía librería ``regex``) y los merges se aplican por orden de aprendizaje;
34
+ el test de paridad encode(tok propio) == encode(tok HF) lo verifica.
35
+ """
36
+
37
+ vocab_files_names = {"vocab_file": "vocab.json", "merges_file": "merges.txt"}
38
+ model_input_names = ["input_ids", "attention_mask"]
39
+
40
+ def __init__(self, vocab_file=None, merges_file=None, **kwargs):
41
+ with open(vocab_file, encoding="utf-8") as f:
42
+ self._vocab = json.load(f) # {token: id}
43
+ with open(merges_file, encoding="utf-8") as f:
44
+ merge_lines = [ln.strip() for ln in f if ln.strip() and not ln.startswith("#")]
45
+ self._merges = {tuple(ln.split()): i for i, ln in enumerate(merge_lines)}
46
+ self._id2tok = {i: t for t, i in self._vocab.items()}
47
+ self._cache = {}
48
+ super().__init__(vocab_file=vocab_file, merges_file=merges_file, **kwargs)
49
+
50
+ @property
51
+ def vocab_size(self):
52
+ return len(self._vocab)
53
+
54
+ def get_vocab(self):
55
+ """Requerido por transformers (_add_tokens lo llama en __init__)."""
56
+ return dict(self._vocab)
57
+
58
+ # ------------------------------------------------------------ tokenización
59
+ def _bpe(self, word: str):
60
+ if word in self._cache:
61
+ return self._cache[word]
62
+ parts = list(word)
63
+ while len(parts) > 1:
64
+ pairs = list(zip(parts, parts[1:]))
65
+ best = min(pairs, key=lambda p: self._merges.get(p, float("inf")))
66
+ if best not in self._merges:
67
+ break
68
+ out, i = [], 0
69
+ while i < len(parts):
70
+ if i < len(parts) - 1 and (parts[i], parts[i + 1]) == best:
71
+ out.append(parts[i] + parts[i + 1])
72
+ i += 2
73
+ else:
74
+ out.append(parts[i])
75
+ i += 1
76
+ parts = out
77
+ self._cache[word] = parts
78
+ return parts
79
+
80
+ def _split_specials(self, text: str):
81
+ """Parte el texto conservando literales <|x|> como tokens (ids 0-8)."""
82
+ sp = sorted((s for s in self._vocab if s.startswith("<|") and s.endswith("|>")),
83
+ key=len, reverse=True)
84
+ out, i, buf = [], 0, ""
85
+ while i < len(text):
86
+ hit = next((s for s in sp if text.startswith(s, i)), None)
87
+ if hit:
88
+ if buf:
89
+ out.append(("text", buf)); buf = ""
90
+ out.append(("special", hit)); i += len(hit)
91
+ else:
92
+ buf += text[i]; i += 1
93
+ if buf:
94
+ out.append(("text", buf))
95
+ return out
96
+
97
+ def _tokenize(self, text):
98
+ tokens = []
99
+ for kind, chunk in self._split_specials(text):
100
+ if kind == "special":
101
+ tokens.append(chunk)
102
+ continue
103
+ for word in PRETOKEN.findall(chunk):
104
+ encoded = "".join(_B2U[b] for b in word.encode("utf-8"))
105
+ tokens.extend(self._bpe(encoded))
106
+ return tokens
107
+
108
+ # ------------------------------------------------------------ conversión
109
+ def _convert_token_to_id(self, token):
110
+ return self._vocab.get(token, self._vocab.get("<|unk|>", 0))
111
+
112
+ def _convert_id_to_token(self, index):
113
+ return self._id2tok.get(index, "<|unk|>")
114
+
115
+ def convert_tokens_to_string(self, tokens):
116
+ return "".join(tokens)
117
+
118
+ def _decode(self, token_ids, skip_special_tokens=False, **kwargs):
119
+ pieces = []
120
+ for i in token_ids:
121
+ tok = self._id2tok.get(int(i))
122
+ if tok is None:
123
+ continue
124
+ if skip_special_tokens and tok in self.all_special_tokens:
125
+ continue
126
+ pieces.append(tok)
127
+ raw = "".join(pieces)
128
+ data = bytes(_U2B[c] for c in raw if c in _U2B)
129
+ return data.decode("utf-8", errors="replace")
tokenizer_config.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "tokenizer_class": "CortexTokenizer",
3
+ "auto_map": {
4
+ "AutoTokenizer": [
5
+ "tokenization_cortex.CortexTokenizer",
6
+ null
7
+ ]
8
+ },
9
+ "model_max_length": 512,
10
+ "bos_token": "<|endoftext|>",
11
+ "eos_token": "<|endoftext|>",
12
+ "pad_token": "<|pad|>",
13
+ "unk_token": "<|unk|>",
14
+ "additional_special_tokens": [
15
+ "<|system|>",
16
+ "<|user|>",
17
+ "<|assistant|>",
18
+ "<|end|>",
19
+ "<|doc|>",
20
+ "<|code|>"
21
+ ],
22
+ "chat_template": "{% for message in messages %}{% if message['role'] == 'system' %}<|system|>\n{{ message['content'] }}\n<|end|>\n{% elif message['role'] == 'user' %}<|user|>\n{{ message['content'] }}\n<|end|>\n{% else %}<|assistant|>\n{{ message['content'] }}\n<|end|>\n{% endif %}{% endfor %}{% if add_generation_prompt %}<|assistant|>\n{% endif %}",
23
+ "tokenizer_files": [
24
+ "vocab.json",
25
+ "merges.txt"
26
+ ]
27
+ }
vocab.json ADDED
The diff for this file is too large to render. See raw diff
 
weights.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6c6de87791adce6695e426f7b6aa5d29ac6005c5391da34ff7f82f639d870d72
3
+ size 82599888