Text Generation
MLX
Safetensors
English
small-language-model
from-scratch
conversational
apple-silicon
research
Instructions to use OpenSML/Solstice-1 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use OpenSML/Solstice-1 with MLX:
# Make sure mlx-lm is installed # pip install --upgrade mlx-lm # Generate text with mlx-lm from mlx_lm import load, generate model, tokenizer = load("OpenSML/Solstice-1") prompt = "Write a story about Einstein" messages = [{"role": "user", "content": prompt}] prompt = tokenizer.apply_chat_template( messages, add_generation_prompt=True ) text = generate(model, tokenizer, prompt=prompt, verbose=True) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
- MLX LM
How to use OpenSML/Solstice-1 with MLX LM:
Generate or start a chat session
# Install MLX LM uv tool install mlx-lm # Interactive chat REPL mlx_lm.chat --model "OpenSML/Solstice-1"
Run an OpenAI-compatible server
# Install MLX LM uv tool install mlx-lm # Start the server mlx_lm.server --model "OpenSML/Solstice-1" # Calling the OpenAI-compatible server with curl curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "OpenSML/Solstice-1", "messages": [ {"role": "user", "content": "Hello"} ] }' - Atomic Chat
Download inference_native/chat_format.py from OpenSML/Solstice-1: direct link, hf CLI and curl.
- Browser
- Download file 4.13 kB
-
https://huggingface.co/OpenSML/Solstice-1/resolve/main/inference_native/chat_format.py
- Command line
-
hf download hf://OpenSML/Solstice-1/inference_native/chat_format.py
-
curl -L -o chat_format.py https://huggingface.co/OpenSML/Solstice-1/resolve/main/inference_native/chat_format.py
4.13 kB
| """One complete conversation per sequence; deterministic, resumable batches.""" | |
| import math | |
| from pathlib import Path | |
| import numpy as np | |
| def serialize(tokenizer, messages, *, generation=False): | |
| ids, mask = [], [] | |
| previous = None | |
| for i, message in enumerate(messages): | |
| role, text = message['role'], message['content'] | |
| if role not in ('system', 'user', 'assistant') or not isinstance(text, str) or not text.strip(): | |
| raise ValueError('Invalid chat message') | |
| if role == 'system': | |
| if i: raise ValueError('System message must be first') | |
| elif role == 'user': | |
| if previous not in (None, 'system', 'assistant'): raise ValueError('User turn order') | |
| elif previous != 'user': raise ValueError('Assistant turn order') | |
| header = [2] + tokenizer.encode(role + '\n') | |
| content = tokenizer.assert_roundtrip(text) | |
| ids += header + content + [3] | |
| mask += [False] * len(header) + [role == 'assistant'] * (len(content) + 1) | |
| previous = role | |
| if generation: | |
| if previous != 'user': raise ValueError('Generation requires a final user turn') | |
| header = [2] + tokenizer.encode('assistant\n') | |
| ids += header; mask += [False] * len(header) | |
| else: | |
| if previous != 'assistant': raise ValueError('Training requires an assistant answer') | |
| ids.append(1); mask.append(True) | |
| return np.asarray(ids, dtype=np.int32), np.asarray(mask, dtype=np.bool_) | |
| def shifted(ids, mask): | |
| return ids[:-1], np.where(mask[1:], ids[1:], -100).astype(np.int32) | |
| def epoch_order(lengths, seed, batch_size=64): | |
| rng = np.random.default_rng(seed) | |
| order = rng.permutation(len(lengths)) | |
| # Randomized length buckets reduce padding without changing membership. | |
| chunks = [] | |
| window_size = ((1024 + batch_size - 1) // batch_size) * batch_size | |
| for start in range(0, len(order), window_size): | |
| window = order[start:start+window_size] | |
| window = window[np.argsort(np.asarray(lengths)[window], kind='stable')] | |
| chunks.extend(window[i:i+batch_size] for i in range(0, len(window), batch_size)) | |
| # Every complete window is divisible by the requested batch size. | |
| full = [c for c in chunks if len(c) == batch_size] | |
| partial = [c for c in chunks if len(c) != batch_size] | |
| rng.shuffle(full) | |
| return full + partial | |
| def training_order(lengths, seed, batch_size=64, epochs=1): | |
| if type(epochs)!=int or not 1<=epochs<=2: raise ValueError('Expected one or two SFT passes') | |
| return [batch for epoch in range(epochs) for batch in epoch_order(lengths,seed+epoch,batch_size)] | |
| class Corpus: | |
| def __init__(self, directory, split): | |
| path = Path(directory) | |
| self.ids = np.load(path/f'{split}.tokens.npy', mmap_mode='r', allow_pickle=False) | |
| self.mask = np.load(path/f'{split}.mask.npy', mmap_mode='r', allow_pickle=False) | |
| self.offsets = np.load(path/f'{split}.offsets.npy', mmap_mode='r', allow_pickle=False) | |
| self.lengths = np.diff(self.offsets).astype(np.int64)-1 | |
| def __len__(self): return len(self.offsets)-1 | |
| def batch(self, indices, size=None, max_length=2048): | |
| size = size or len(indices) | |
| if len(indices) > size or not len(indices): raise ValueError('Empty/oversized batch') | |
| length = int(max(self.lengths[indices])) | |
| if length > max_length: raise ValueError('Conversation exceeds context') | |
| length = min(max_length, max(16, 2**math.ceil(math.log2(length)))) | |
| x = np.zeros((size, length), np.int32) | |
| y = np.full((size, length), -100, np.int32) | |
| for row, index in enumerate(indices): | |
| a, b = map(int, self.offsets[index:index+2]) | |
| xi, yi = shifted(self.ids[a:b], self.mask[a:b]) | |
| x[row, :len(xi)] = xi; y[row, :len(yi)] = yi | |
| return x, y | |
| def learning_rate(step, total, peak, final, warmup_fraction): | |
| warmup = max(1, math.ceil(total*warmup_fraction)) | |
| if step <= warmup: return peak*step/warmup | |
| progress = min(1., (step-warmup)/max(1, total-warmup)) | |
| return final+(peak-final)*(1+math.cos(math.pi*progress))/2 | |