Text Generation
MLX
English
apple-silicon
pretrained-from-scratch
gated-deltanet
linear-attention
product-key-memory
long-context
Instructions to use junafinity/Gala-598M-MLX with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use junafinity/Gala-598M-MLX with MLX:
# Make sure mlx-lm is installed # pip install --upgrade mlx-lm # if on a CUDA device, also pip install mlx[cuda] # Generate text with mlx-lm from mlx_lm import load, generate model, tokenizer = load("junafinity/Gala-598M-MLX") prompt = "Once upon a time in" text = generate(model, tokenizer, prompt=prompt, verbose=True) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
- MLX LM
How to use junafinity/Gala-598M-MLX with MLX LM:
Generate or start a chat session
# Install MLX LM uv tool install mlx-lm # Generate some text mlx_lm.generate --model "junafinity/Gala-598M-MLX" --prompt "Once upon a time"
- Atomic Chat
Download train.py from junafinity/Gala-598M-MLX: direct link, hf CLI and curl.
- Browser
- Download file 11.9 kB
-
https://huggingface.co/junafinity/Gala-598M-MLX/resolve/main/train.py
- Command line
-
hf download hf://junafinity/Gala-598M-MLX/train.py
-
curl -L -o train.py https://huggingface.co/junafinity/Gala-598M-MLX/resolve/main/train.py
11.9 kB
| """ | |
| Train HOARD (or a matched baseline) on one Mac. | |
| python train.py --config hoard_small --data data/fineweb --steps 3000 --batch_tokens 65536 | |
| python train.py --config transformer_small --data data/fineweb --steps 3000 --batch_tokens 65536 | |
| Optimiser: Muon for 2-D hidden matrices, AdamW for embeddings / head / norms / | |
| gates / hoard block weights. LR: linear warmup, flat, linear cooldown. | |
| """ | |
| import argparse, json, math, os, sys, time | |
| import numpy as np | |
| import mlx.core as mx | |
| import mlx.nn as nn | |
| import mlx.optimizers as optim | |
| from mlx.utils import tree_flatten | |
| import configs | |
| from model import HOARD, HoardConfig | |
| # --------------------------------------------------------------------------- # | |
| class TokenLoader: | |
| def __init__(self, path, seq_len, batch, seed=0): | |
| self.data = np.memmap(path, dtype=np.uint16, mode="r") | |
| self.T, self.B = seq_len, batch | |
| self.rng = np.random.default_rng(seed) | |
| assert len(self.data) > seq_len + 1, "dataset too small for seq_len" | |
| def next(self): | |
| ix = self.rng.integers(0, len(self.data) - self.T - 1, size=self.B) | |
| x = np.stack([self.data[i: i + self.T + 1] for i in ix]).astype(np.int32) | |
| return mx.array(x) | |
| def sequential(self, n_batches): | |
| """Deterministic contiguous batches for validation. Shrinks the batch | |
| when the split is smaller than one full B x (T+1) span (tiny datasets | |
| like shakespeare's 6.7K-token val split) instead of yielding nothing.""" | |
| B = self.B | |
| if B * (self.T + 1) > len(self.data): | |
| B = max(1, len(self.data) // (self.T + 1)) | |
| span = B * (self.T + 1) | |
| for j in range(n_batches): | |
| st = j * span | |
| if st + span > len(self.data): | |
| break | |
| x = np.asarray(self.data[st: st + span]).astype(np.int32).reshape(B, self.T + 1) | |
| yield mx.array(x) | |
| def lr_at(step, total, base, warmup, cooldown_frac): | |
| if step < warmup: | |
| return base * (step + 1) / warmup | |
| cool_start = int(total * (1 - cooldown_frac)) | |
| if step >= cool_start: | |
| return base * max(0.0, (total - step) / max(1, total - cool_start)) | |
| return base | |
| def fp32_master(opt_cls): | |
| """Wrap an MLX optimizer so bf16 parameters train with fp32 master weights | |
| and fp32 optimizer state. bf16-only AdamW updates diverge (measured: nan by | |
| step 10 on shakespeare); compute stays bf16, the update math moves to fp32. | |
| The master copy is created lazily in apply_single because Optimizer.init | |
| receives the gradient tree, not the parameters.""" | |
| class FP32Master(opt_cls): | |
| def apply_single(self, gradient, parameter, state): | |
| if parameter.dtype == mx.float32: | |
| return super().apply_single(gradient, parameter, state) | |
| if "master" not in state: | |
| state["master"] = parameter.astype(mx.float32) | |
| for k in list(state): | |
| if k != "master" and isinstance(state[k], mx.array): | |
| state[k] = state[k].astype(mx.float32) | |
| new = super().apply_single(gradient.astype(mx.float32), state["master"], state) | |
| state["master"] = new | |
| return new.astype(parameter.dtype) | |
| return FP32Master | |
| class NorMuonLite(optim.Muon): | |
| """Muon plus per-row second-moment normalization of the orthogonalized | |
| update (NorMuon-style), scale-preserving: rows are rebalanced, the global | |
| update RMS is unchanged so learning-rate semantics carry over.""" | |
| def apply_single(self, gradient, parameter, state): | |
| if parameter.ndim < 2: | |
| return super().apply_single(gradient, parameter, state) | |
| if self.weight_decay != 0: | |
| gradient = gradient + self.weight_decay * parameter | |
| v = self.momentum * state["v"] + (1 - self.momentum) * gradient | |
| state["v"] = v | |
| update = gradient * (1 - self.momentum) + v * self.momentum if self.nesterov else v | |
| original_shape = update.shape | |
| reshape_needed = update.ndim > 2 | |
| if reshape_needed: | |
| update = mx.reshape(update, (update.shape[0], -1)) | |
| update = self._zeropower_via_newtonschulz5(update, steps=self.ns_steps) | |
| row_ms = mx.mean(mx.square(update), axis=-1, keepdims=True) | |
| vn = state.get("vn") | |
| vn = row_ms if vn is None else 0.95 * vn + 0.05 * row_ms | |
| state["vn"] = vn | |
| pre_rms = mx.sqrt(mx.mean(mx.square(update)) + 1e-12) | |
| update = update / (mx.sqrt(vn) + 1e-8) | |
| update = update * (pre_rms / (mx.sqrt(mx.mean(mx.square(update))) + 1e-12)) | |
| if reshape_needed: | |
| update = mx.reshape(update, original_shape) | |
| lr = self.learning_rate.astype(gradient.dtype) | |
| lr *= max(1, update.shape[-2] / update.shape[-1]) ** 0.5 | |
| return parameter - lr * update | |
| def is_hoard_param(path: str, p: mx.array) -> bool: | |
| return "W_up" in path or "W_down" in path | |
| def is_muon_param(path: str, p: mx.array) -> bool: | |
| if p.ndim != 2: | |
| return False | |
| for bad in ("embed", "head", "W_up", "W_down", "norm", "loop_embed", "conv"): | |
| if bad in path: | |
| return False | |
| return True | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--config", default="hoard_small") | |
| ap.add_argument("--data", default="data/fineweb") | |
| ap.add_argument("--out", default=None) | |
| ap.add_argument("--steps", type=int, default=2000) | |
| ap.add_argument("--seq_len", type=int, default=1024) | |
| ap.add_argument("--batch_tokens", type=int, default=65536) | |
| ap.add_argument("--lr", type=float, default=3e-3, help="AdamW lr (embeddings etc.)") | |
| ap.add_argument("--muon_lr", type=float, default=0.02) | |
| ap.add_argument("--wd", type=float, default=0.0) | |
| ap.add_argument("--warmup", type=int, default=100) | |
| ap.add_argument("--cooldown_frac", type=float, default=0.3) | |
| ap.add_argument("--grad_clip", type=float, default=1.0) | |
| ap.add_argument("--eval_every", type=int, default=200) | |
| ap.add_argument("--eval_batches", type=int, default=8) | |
| ap.add_argument("--log_every", type=int, default=10) | |
| ap.add_argument("--seed", type=int, default=0) | |
| ap.add_argument("--loops", type=int, default=None, help="override n_loops") | |
| ap.add_argument("--loop_schedule", default=None, choices=[None, "fixed", "random"]) | |
| ap.add_argument("--dtype", default="float32", choices=["float32", "bfloat16"]) | |
| ap.add_argument("--resume", default=None) | |
| ap.add_argument("--vocab", type=int, default=None, help="override vocab_size (e.g. 4096 for synthetic data)") | |
| ap.add_argument("--tie", action="store_true", help="tie input/output embeddings") | |
| ap.add_argument("--window", type=int, default=None, help="override attention window") | |
| ap.add_argument("--gate", action="store_true", help="silu-gated hoard output") | |
| ap.add_argument("--hoard_lr", type=float, default=None, | |
| help="separate AdamW lr for hoard W_up/W_down (memory-layer recipe)") | |
| ap.add_argument("--normuon", action="store_true", help="NorMuon-lite instead of Muon") | |
| ap.add_argument("--ckpt", action="store_true", help="gradient-checkpoint each cell") | |
| ap.add_argument("--compile_step", action="store_true", help="mx.compile the fwd+bwd step") | |
| a = ap.parse_args() | |
| mx.random.seed(a.seed) | |
| cfg: HoardConfig = configs.get(a.config) | |
| if a.loops: | |
| cfg.n_loops = a.loops | |
| if a.loop_schedule: | |
| cfg.loop_schedule = a.loop_schedule | |
| if a.vocab: | |
| cfg.vocab_size = a.vocab | |
| if a.tie: | |
| cfg.tie_embeddings = True | |
| if a.window: | |
| cfg.window = a.window | |
| if a.gate: | |
| cfg.hoard_gate = True | |
| out = a.out or os.path.join("runs", f"{a.config}_{int(time.time())}") | |
| os.makedirs(out, exist_ok=True) | |
| json.dump({"config": cfg.to_dict(), "args": vars(a)}, open(os.path.join(out, "config.json"), "w"), indent=2) | |
| model = HOARD(cfg) | |
| if a.dtype == "bfloat16": | |
| # parameters in bf16; the GDN core casts to fp32 internally, state is fp32 | |
| model.set_dtype(mx.bfloat16) | |
| if a.resume: | |
| model.load_weights(a.resume) | |
| counts = model.param_counts() | |
| print(f"[{a.config}] params: total {counts['total']/1e6:.1f}M | hoard {counts['hoard']/1e6:.1f}M | " | |
| f"active/token/loop {counts['active_per_token_per_loop']/1e6:.1f}M | active x loops {counts['active_x_loops']/1e6:.1f}M") | |
| B = max(1, a.batch_tokens // a.seq_len) | |
| train_loader = TokenLoader(os.path.join(a.data, "train.bin"), a.seq_len, B, a.seed) | |
| val_loader = TokenLoader(os.path.join(a.data, "val.bin"), a.seq_len, B, a.seed) | |
| muon_cls = NorMuonLite if a.normuon else optim.Muon | |
| muon = fp32_master(muon_cls)(learning_rate=a.muon_lr, weight_decay=a.wd) | |
| adamw = fp32_master(optim.AdamW)(learning_rate=a.lr, betas=(0.9, 0.95), weight_decay=a.wd) | |
| sched = [(muon, a.muon_lr), (adamw, a.lr)] | |
| if a.hoard_lr: | |
| hoard_opt = fp32_master(optim.AdamW)(learning_rate=a.hoard_lr, betas=(0.9, 0.95), weight_decay=a.wd) | |
| sched.insert(1, (hoard_opt, a.hoard_lr)) | |
| opt = optim.MultiOptimizer([muon, hoard_opt, adamw], [is_muon_param, is_hoard_param]) | |
| else: | |
| opt = optim.MultiOptimizer([muon, adamw], [is_muon_param]) | |
| if a.ckpt: | |
| from mlx.nn.utils import checkpoint as _checkpoint | |
| model._ckpt_cells = [_checkpoint(c) for c in model.cells] | |
| def loss_fn(model, batch, n_loops): | |
| x, y = batch[:, :-1], batch[:, 1:] | |
| logits = model(x, n_loops=n_loops) | |
| ce = nn.losses.cross_entropy(logits.astype(mx.float32), y, reduction="mean") | |
| return ce + model.balance_loss(), ce | |
| grad_fn = nn.value_and_grad(model, loss_fn) | |
| compiled = None | |
| if a.compile_step and cfg.loop_schedule == "fixed": | |
| _state = [model.state, mx.random.state] | |
| def _compute(batch): | |
| return grad_fn(model, batch, cfg.n_loops) | |
| compiled = mx.compile(_compute, inputs=_state, outputs=_state) | |
| def train_step(batch, n_loops): | |
| if compiled is not None and n_loops == cfg.n_loops: | |
| (loss, ce), grads = compiled(batch) | |
| else: | |
| (loss, ce), grads = grad_fn(model, batch, n_loops) | |
| if a.grad_clip > 0: | |
| grads, _ = optim.clip_grad_norm(grads, a.grad_clip) | |
| opt.update(model, grads) | |
| return loss, ce | |
| def evaluate(n_loops): | |
| model.eval() | |
| losses = [] | |
| for batch in val_loader.sequential(a.eval_batches): | |
| _, ce = loss_fn(model, batch, n_loops) | |
| losses.append(ce.item()) | |
| model.train() | |
| return float(np.mean(losses)) if losses else float("nan") | |
| log = open(os.path.join(out, "log.txt"), "a") | |
| t0 = time.time(); tok_seen = 0 | |
| for step in range(a.steps): | |
| lr_scale = lr_at(step, a.steps, 1.0, a.warmup, a.cooldown_frac) | |
| for o, base_lr in sched: | |
| o.learning_rate = base_lr * lr_scale | |
| n_loops = cfg.n_loops | |
| if cfg.loop_schedule == "random": | |
| n_loops = int(np.random.randint(1, cfg.n_loops + 1)) | |
| batch = train_loader.next() | |
| loss, ce = train_step(batch, n_loops) | |
| mx.eval(model.parameters(), opt.state, loss) | |
| tok_seen += B * a.seq_len | |
| if step % a.log_every == 0 or step == a.steps - 1: | |
| dt = time.time() - t0 | |
| msg = (f"step {step:5d} | loss {loss.item():.4f} ce {ce.item():.4f} | loops {n_loops} | " | |
| f"lr x{lr_scale:.2f} | {tok_seen/dt:,.0f} tok/s | {dt/60:.1f} min") | |
| print(msg); log.write(msg + "\n"); log.flush() | |
| if (step + 1) % a.eval_every == 0 or step == a.steps - 1: | |
| vl = evaluate(cfg.n_loops) | |
| msg = f" val loss {vl:.4f} @ step {step+1} ({tok_seen/1e6:.1f}M tokens)" | |
| print(msg); log.write(msg + "\n"); log.flush() | |
| model.save_weights(os.path.join(out, "model.safetensors")) | |
| print(f"done. checkpoint in {out}") | |
| if __name__ == "__main__": | |
| main() | |