Now, let's build your very own AI.
XSLM overview
means Extra Small Language Model, a term SONUB coined for learning to build . Pronunciation: 엑스에스엘엠. We aim for a complete learning project, not fluent answers.
Text becomes ; a learns to predict the next ; the loads its learned numbers; sends questions to the Runner. You write each part yourself. supplies arithmetic and files, not a pretrained brain.
Type and try
Open the and type the below. Then explain the five parts aloud. The small English sample keeps typing short; you can later replace it with text in your own language.
python3 -c "print('XSLM: text -> tokens -> GPT -> Runner -> XChat')"
Practice in the terminal
Check your understanding
Can you distinguish a , a , a Runner and a chat ? Completion means your own are trained, saved, reloaded and used by XChat, even if the response is nonsense.
If you get stuck
New to Python? Review , , , and in the Python course. Use the optional warm-up to see next-word prediction before tackling a .
Optional warm-up: count words with a bigram
A Plan That Leads to Real Development
Start small like a toy, understand the principles, then grow it bigger over time.
Collect sentences
Collect sentences about the topic you want to learn. Just 10 sentences is enough to start.
Count next words
Make a table of how many times each word follows another word.
Compute probability scores
Count how many times each word comes right after the chosen word — the end of the sentence counts too. Then divide each count by the total. If “is” is followed by “blue” three times and “gray” once, blue gets 3 ÷ 4 = 75% and gray gets 25%.
Compare two selection methods
Take the top candidate every time (), or draw by the percentages () — a draw can still land on the same word again. When the end of the sentence is chosen, it stops. Nothing in this step checks whether the sentence is true.
Improve it your way
Add better sentences, more , and different selection rules to grow it into your own .
First time here? Follow these steps
This lab builds a tiny that only looks at the word right before. It counts which word came after which, turns those counts into and then rolls a weighted die to pick the next word.
- Leave the sentences as they are and press "Generate again". A new sentence appears, made only of words from the training text.
- Look at the probability formula: the number of times the next word came after the previous word, divided by the number of times the previous word appeared, is the probability.
- In the count table, each row is a previous word and each column is a next word. The darker the cell, the more often that pair appeared.
- Switch the mode to and press again: you get the same sentence every time. Go back to and raise the — the sentences become stranger.
- Add one sentence of your own to the training text. The table and the probabilities change right away, because the is only these counts.
Want to go further? Build it yourself in Python (AI Course) How the next word is chosen
A Tiny Probability Calculator
The percentages in the table are at temperature 1. Temperature only changes how likely each word is to be drawn — not the counts or whether anything is true.
There are no candidates yet following the last word of the starting sentence.
| Previous wordNext word | |
|---|---|
Many words — showing only the first 8
Key Concept
The idea is simple: read many sentences and count which word comes right after which — including where a sentence ends. Those counts become a small table, and the table already lets the guess the next word. That is the whole model you build on this page.
Real works on the same idea, only far bigger. Instead of a small table of counts it learns billions of numbers, called , from examples, and it looks at much more than the one word before — your whole question and the conversation so far. It does not look up a stored sentence. But the last step is the same as yours: give every candidate a score, then choose.
This lab vs a real language model
| This lab () | Neural | |
|---|---|---|
| What it looks at | Only the previous word | The long it is given |
| What it learns | A table of counts | Numeric |
| and | No | Yes |
| Checks facts by itself | No | No |
FAI and reference documents
Based on your FAI project; the examples here are rewritten for Python. FAI uses , and a larger model. Environment references checked on 2026-10-18.
Environment
The runs through . is already installed. This course uses only its : no installation, account, or is required.
FAI uses for and . accelerates PyTorch on supported Apple computers; it is not available in this emulated . can run Python calculations, but ordinary listening and background services are constrained. We therefore complete every required step in this Alpine terminal. We write a small differentiation engine ourselves.
Type and try
Type the three checks. Look for the Python , a 32bit architecture and ready. All later belong in this terminal, not your computer’s terminal.
python3 --version
uname -m
python3 -c "import json, math, random, http.server; print('ready')"
Practice in the terminal
Check your understanding
Explain why importing torch or enabling MPS is unnecessary here. Python performs the on the emulated by your .
If you get stuck
Do not try to fix a missing torch package with pip install torch: this lesson does not import it. If the terminal will not load, check the connection and retry. A simulated success message is not a working Python environment.
Project structure
Keep everything in ~/xslm. holds raw text, text, text and the . checkpoints holds learned . The files live at the . Run all from that folder.
Each raw line contains Q: question | A: answer. prepare.py removes duplicate lines and keeps every fifth distinct line for validation. It replaces the separator with a . Validation is an unseen quiz; do not train the tokenizer on it. Use text you are allowed to use and invent examples instead of copying personal data.
Type and try
Create the folders first. Open each source below, type it into with exactly that filename, save and close. settings.json is , so copy its punctuation carefully. Run prepare.py only after saving both the data and the .
mkdir -p ~/xslm/data ~/xslm/checkpoints
cd ~/xslm
nano settings.json
nano data/raw.txt
nano prepare.py
python3 prepare.py
wc -l data/train.txt data/val.txt
Practice in the terminal
settings.jsonComplete source to type
These are the small course defaults. width is the size of a ; is the number of visible tokens; is the update size. Keep the same settings when resuming.
{
"width": 8,
"context": 12,
"merges": 12,
"steps": 20,
"learning_rate": 0.03,
"seed": 7,
"tokens": 24,
"max_tokens": 64,
"port": 8765
}
data/raw.txtComplete source to type
Type one Q/A pair per line. These invented English examples keep the first experiment short. Replace them later with your own consistent text.
Q: hello | A: hello friend.
Q: hi | A: hi friend.
Q: name | A: i am xslm.
Q: sun | A: the sun is warm.
Q: cat | A: a cat can nap.
Q: dog | A: a dog can run.
Q: red | A: red is a color.
Q: blue | A: blue is a color.
Q: learn | A: i learn from text.
Q: code | A: i can learn code.
Q: moon | A: the moon is in the sky.
Q: rain | A: rain is water.
Q: book | A: i can read a book.
Q: tea | A: tea is warm.
Q: bye | A: bye friend.
prepare.pyComplete source to type
Read text → remove duplicates → split whole records → write train and validation files. Separating records before tokenization avoids sharing the exact same example between both sets.
from pathlib import Path
lines = [line.strip() for line in Path('data/raw.txt').read_text(encoding='utf-8').splitlines() if line.strip()]
lines = list(dict.fromkeys(lines))
assert len(lines) >= 5, 'write at least five different Q/A lines'
assert all(' | A:' in line and line.startswith('Q:') for line in lines)
train = [line for i, line in enumerate(lines) if i % 5 != 0]
val = [line for i, line in enumerate(lines) if i % 5 == 0]
for name, rows in [('train', train), ('val', val)]:
text = '\n'.join(row.replace(' | A:', '\nA:') for row in rows) + '\n'
Path('data/' + name + '.txt').write_text(text, encoding='utf-8')
print(name, len(rows))
Check your understanding
Run and . You should see settings.json and prepare.py. The sample produces 12 training records and 3 validation records. Add your own Q/A line and explain which file must be rebuilt.
If you get stuck
usually means the wrong folder or filename. Run ~/xslm and ls data. means Python differs: use four spaces. Finished reference files are in ~/xslm-reference for comparison; they do not fill your project automatically.
BPE tokenizer
A gives each text piece a number. Our character-based starts with individual characters, counts adjacent pairs and repeatedly merges the most frequent pair. The order of merges matters: encoding must replay that order.
This small teaching keeps separate and reserves 0 for unknown characters. It is not a full byte-level production tokenizer. It saves both the and to tokenizer.json. A trained with one vocabulary cannot safely use a different one.
Type and try
Type tokenizer.py, run it and inspect the . Follow merge with a pencil: when two IDs match, the new ID and advance two positions; otherwise advance one. then decode a sentence.
cd ~/xslm
nano tokenizer.py
python3 tokenizer.py
python3 -m json.tool data/tokenizer.json
Practice in the terminal
tokenizer.pyComplete source to type
merge combines one pair without overlapping. train learns the ordered rules. encode replays them; decode joins vocabulary pieces. The final assertion checks reversibility.
import json
from collections import Counter
from pathlib import Path
def merge(ids, pair, new_id):
result, i = [], 0
while i < len(ids):
if tuple(ids[i:i + 2]) == tuple(pair):
result.append(new_id)
i += 2
else:
result.append(ids[i])
i += 1
return result
class BPE:
def __init__(self, vocab, merges):
self.vocab, self.merges = vocab, merges
self.chars = {c: i for i, c in enumerate(vocab) if len(c) == 1}
@classmethod
def train(cls, text, count):
vocab = ['\ufffd'] + sorted(set(text) - {'\ufffd'})
ids = [vocab.index(c) for c in text]
merges = []
for _ in range(count):
pairs = Counter((a, b) for a, b in zip(ids, ids[1:])
if '\n' not in vocab[a] + vocab[b])
if not pairs:
break
pair, frequency = pairs.most_common(1)[0]
if frequency < 2:
break
new_id = len(vocab)
vocab.append(vocab[pair[0]] + vocab[pair[1]])
merges.append([list(pair), new_id])
ids = merge(ids, pair, new_id)
return cls(vocab, merges)
def encode(self, text):
ids = [self.chars.get(c, 0) for c in text]
for pair, new_id in self.merges:
ids = merge(ids, pair, new_id)
return ids
def decode(self, ids):
return ''.join(self.vocab[i] for i in ids)
def state(self):
return {'vocab': self.vocab, 'merges': self.merges}
if __name__ == '__main__':
settings = json.loads(Path('settings.json').read_text())
text = Path('data/train.txt').read_text(encoding='utf-8')
tokenizer = BPE.train(text, settings['merges'])
Path('data/tokenizer.json').write_text(
json.dumps(tokenizer.state(), ensure_ascii=False), encoding='utf-8')
assert tokenizer.decode(tokenizer.encode(text)) == text
print('vocab:', len(tokenizer.vocab), 'merges:', len(tokenizer.merges))
print('roundtrip: OK', tokenizer.encode('Q: hi\nA:'))
Check your understanding
: OK means the entire training text survives encode → decode. Change the merge count in settings.json and compare counts. Test an unseen character: it becomes �, not a newly learned token.
If you get stuck
No merges can be valid when no pair repeats. If you change text or merge rules, rebuild the tokenizer and start new model ; keep old checkpoints in an exported .
GPT architecture
looks up a of numbers for each . A records where it appears. asks what to look for, describes each earlier position, and value carries its information. combine those values.
The uses one causal attention head and one block. keys and values contain only the current and earlier positions, so the model cannot peek ahead. preserve the input; a two-layer changes features. stabilizes their scale; the output head scores tokens. For readable , this uses , parameter-free normalization and no .
Type and try
Write engine.py first and run its check; then write model.py. In Value, each operation remembers its derivative. backward visits the graph in reverse order and adds contributions, so shared learn from every use. Trace token → position → attention → MLP → head in forward.
cd ~/xslm
nano engine.py
nano model.py
python3 engine.py
python3 -c "from model import GPT; m=GPT(10, 8, 12); print(len(m.forward([1, 2])), len(m.forward([1, 2])[0]))"
Practice in the terminal
engine.pyComplete source to type
is a number; grad is its effect on the ; parents store local derivatives. Addition distributes a ; multiplication scales it by the other input. converts scores to .
import math
class Value:
def __init__(self, data, parents=()):
self.data = float(data)
self.grad = 0.0
self.parents = parents
def __add__(self, other):
other = other if isinstance(other, Value) else Value(other)
return Value(self.data + other.data, ((self, 1), (other, 1)))
def __mul__(self, other):
other = other if isinstance(other, Value) else Value(other)
return Value(self.data * other.data,
((self, other.data), (other, self.data)))
def __pow__(self, power):
return Value(self.data ** power,
((self, power * self.data ** (power - 1)),))
def __neg__(self):
return self * -1
def __sub__(self, other):
return self + (-other)
def __truediv__(self, other):
return self * (other ** -1)
__radd__ = __add__
__rmul__ = __mul__
def exp(self):
result = math.exp(self.data)
return Value(result, ((self, result),))
def log(self):
return Value(math.log(self.data), ((self, 1 / self.data),))
def relu(self):
return Value(max(0, self.data), ((self, float(self.data > 0)),))
def backward(self):
order, seen, stack = [], set(), [(self, False)]
while stack:
node, expanded = stack.pop()
if expanded:
order.append(node)
elif node not in seen:
seen.add(node)
stack.append((node, True))
stack.extend((parent, False) for parent, _ in node.parents)
for node in order:
node.grad = 0.0
self.grad = 1.0
for node in reversed(order):
for parent, derivative in node.parents:
parent.grad += derivative * node.grad
def softmax(values):
peak = max(v.data for v in values)
exps = [(v - peak).exp() for v in values]
total = sum(exps)
return [v / total for v in exps]
def linear(x, weights):
return [sum(a * b for a, b in zip(x, row)) for row in weights]
def norm(x):
mean = sum(x) / len(x)
variance = sum((v - mean) ** 2 for v in x) / len(x)
return [(v - mean) * (variance + 1e-5) ** -0.5 for v in x]
if __name__ == '__main__':
x = Value(3)
y = x * x + 2 * x
y.backward()
print('y =', y.data, 'dy/dx =', x.grad)
assert y.data == 15 and x.grad == 8
model.pyComplete source to type
Read shapes first, then forward. Every Value in weights is trainable. store vectors and . loss compares each position with its next-token target. state and load serialize numbers, not .
import random
from engine import Value, linear, norm, softmax
class GPT:
def __init__(self, vocab_size, width, context, seed=7):
self.width, self.context = width, context
rng = random.Random(seed)
shapes = {
'token': (vocab_size, width), 'position': (context, width),
'query': (width, width), 'key': (width, width),
'value': (width, width), 'project': (width, width),
'expand': (2 * width, width), 'contract': (width, 2 * width),
'head': (vocab_size, width),
}
self.weights = {
name: [[Value(rng.gauss(0, 0.1)) for _ in range(cols)]
for _ in range(rows)] for name, (rows, cols) in shapes.items()
}
self.parameters = [v for matrix in self.weights.values()
for row in matrix for v in row]
def forward(self, ids):
assert 0 < len(ids) <= self.context
w, keys, values, output = self.weights, [], [], []
for position, token in enumerate(ids):
x = [a + b for a, b in zip(w['token'][token], w['position'][position])]
h = norm(x)
q = linear(h, w['query'])
keys.append(linear(h, w['key']))
values.append(linear(h, w['value']))
scores = [sum(a * b for a, b in zip(q, k)) / self.width ** 0.5
for k in keys]
attention = softmax(scores)
mixed = [sum(p * v[i] for p, v in zip(attention, values))
for i in range(self.width)]
x = [a + b for a, b in zip(x, linear(mixed, w['project']))]
hidden = [v.relu() for v in linear(norm(x), w['expand'])]
x = [a + b for a, b in zip(x, linear(hidden, w['contract']))]
output.append(linear(norm(x), w['head']))
return output
def loss(self, inputs, targets):
logits = self.forward(inputs)
losses = [-softmax(row)[target].log()
for row, target in zip(logits, targets)]
return sum(losses) / len(losses)
def state(self):
return {name: [[v.data for v in row] for row in matrix]
for name, matrix in self.weights.items()}
def load(self, state):
assert set(state) == set(self.weights), 'checkpoint keys'
for name, matrix in self.weights.items():
assert len(state[name]) == len(matrix), 'checkpoint rows'
for row, saved in zip(matrix, state[name]):
assert len(saved) == len(row), 'checkpoint columns'
for value, number in zip(row, saved):
value.data = float(number)
Check your understanding
engine.py must print y = 15.0 and dy/dx = 8.0. The model shape check prints 2 10: two positions, ten vocabulary scores each. Explain why appending a future token must not change the earlier scores.
If you get stuck
Keep matrix rows and zip inputs aligned. A count-based is not a substitute for these layers. This model is deliberately tiny; all embedding, attention, MLP and head weights still learn through .
Training & checkpoints
For [a,b,c], inputs [a,b] target [b,c]. is the of the correct next token. A smaller means the assigned it more ; it does not guarantee a truthful answer.
Each iteration chooses a short window, clears old gradients, runs forward and backward, clips the norm and applies . A checkpoint stores , settings, , step and random state after each completed update. The initial and final values use the same first held-out window: a small diagnostic, not a complete evaluation.
Type and try
Type train.py and run the short . Watch real step and loss values. Run five more steps and check resume. Start small because speed varies. Repeat training to explore changes; the default run only checks that the pipeline works.
cd ~/xslm
nano train.py
python3 train.py --steps 20
python3 train.py --steps 5
Practice in the terminal
train.pyComplete source to type
The prevents resuming on silently changed data. Saving through a and preserves the previous complete checkpoint if writing is interrupted. SGD has no state to save.
import argparse
import hashlib
import json
import math
import os
import random
from pathlib import Path
from model import GPT
from tokenizer import BPE
def main():
cfg = json.loads(Path('settings.json').read_text())
parser = argparse.ArgumentParser()
parser.add_argument('--steps', type=int, default=cfg['steps'])
args = parser.parse_args()
assert args.steps > 0, '--steps must be positive'
token_state = json.loads(Path('data/tokenizer.json').read_text())
tokenizer = BPE(**token_state)
train_text = Path('data/train.txt').read_text(encoding='utf-8')
val_text = Path('data/val.txt').read_text(encoding='utf-8')
fingerprint = hashlib.sha256((train_text + '\0' + val_text).encode()).hexdigest()
train, val = tokenizer.encode(train_text), tokenizer.encode(val_text)
model = GPT(len(tokenizer.vocab), cfg['width'], cfg['context'], cfg['seed'])
rng, step = random.Random(cfg['seed']), 0
checkpoint = Path('checkpoints/model.json')
if checkpoint.exists():
saved = json.loads(checkpoint.read_text())
assert saved['settings'] == cfg and saved['tokenizer'] == token_state, 'changed settings/tokenizer: start a new checkpoint'
assert saved['data_hash'] == fingerprint, 'changed data: start a new checkpoint'
model.load(saved['weights'])
step = saved['step']
rng.setstate((saved['random'][0], tuple(saved['random'][1]), saved['random'][2]))
n = model.context
assert len(train) > n and len(val) > n, 'add more text'
print('parameters:', len(model.parameters), 'resume:', step, flush=True)
print('validation:', round(model.loss(val[:n], val[1:n + 1]).data, 4), flush=True)
checkpoint.parent.mkdir(exist_ok=True)
for _ in range(args.steps):
start = rng.randrange(len(train) - n)
window = train[start:start + n + 1]
for p in model.parameters:
p.grad = 0
loss = model.loss(window[:-1], window[1:])
loss.backward()
size = math.sqrt(sum(p.grad * p.grad for p in model.parameters))
scale = min(1.0, 1.0 / (size + 1e-8))
for p in model.parameters:
p.data -= cfg['learning_rate'] * p.grad * scale
step += 1
state = {'settings': cfg, 'tokenizer': token_state, 'data_hash': fingerprint,
'weights': model.state(), 'step': step, 'random': rng.getstate()}
temporary = checkpoint.with_suffix('.tmp')
temporary.write_text(json.dumps(state, ensure_ascii=False), encoding='utf-8')
os.replace(temporary, checkpoint)
print('step:', step, 'loss:', round(loss.data, 4), flush=True)
print('validation:', round(model.loss(val[:n], val[1:n + 1]).data, 4), flush=True)
print('saved:', checkpoint, flush=True)
if __name__ == '__main__':
main()
Check your understanding
Find checkpoints/model.json. The second run continues the previous step count. Compare validation before and after. Loss may rise on different random windows; test a longer run before drawing conclusions.
If you get stuck
stops training; the last completed update remains saved. Changed settings, tokenizer or data deliberately reject the old checkpoint. Export first, then move model.json to another name to start again. A already running keeps its old weights until restarted.
Text generation
Generation repeatedly feeds the latest to and appends one predicted . 0 picks the highest score. Positive temperatures rescale scores before random : higher values spread more broadly. keeps only the strongest candidates; 0 keeps all.
A seed repeats the random choices. It does not make answers correct. Generation stops at a or the requested token limit. Only the newly generated text is returned. Keep the Q:/A: format used during .
Type and try
Write generate.py. Compare the three while keeping the same checkpoint. Change only temperature first, then top_k and seed. The checkpoint supplies its own and dimensions.
cd ~/xslm
nano generate.py
python3 generate.py --temperature 0 --top-k 1
python3 generate.py --temperature 0.8 --top-k 8 --seed 7
python3 generate.py --temperature 1.2 --top-k 0 --seed 8
Practice in the terminal
generate.pyComplete source to type
load rebuilds the architecture and restores numbers. generate crops context, scores candidates and samples repeatedly. A makes comparisons .
import argparse
import json
import math
import random
from pathlib import Path
from model import GPT
from tokenizer import BPE
def load():
saved = json.loads(Path('checkpoints/model.json').read_text())
cfg = saved['settings']
tokenizer = BPE(**saved['tokenizer'])
model = GPT(len(tokenizer.vocab), cfg['width'], cfg['context'], cfg['seed'])
model.load(saved['weights'])
return model, tokenizer, cfg
def generate(model, tokenizer, prompt, tokens=24, temperature=0.8, top_k=8, seed=7):
if not prompt or tokens < 1 or temperature < 0 or not math.isfinite(temperature) or top_k < 0:
raise ValueError('invalid sampling parameters')
ids, result, rng = tokenizer.encode(prompt), [], random.Random(seed)
for _ in range(tokens):
logits = model.forward(ids[-model.context:])[-1]
ranked = sorted(range(len(logits)), key=lambda i: logits[i].data, reverse=True)
if temperature == 0:
token = ranked[0]
else:
candidates = ranked[:top_k] if top_k else ranked
peak = logits[candidates[0]].data
weights = [math.exp((logits[i].data - peak) / temperature) for i in candidates]
token = rng.choices(candidates, weights=weights)[0]
ids.append(token)
if tokenizer.vocab[token] == '\n':
break
result.append(token)
return tokenizer.decode(result)
if __name__ == '__main__':
model, tokenizer, cfg = load()
parser = argparse.ArgumentParser()
parser.add_argument('--prompt', default='Q: hi\nA:')
parser.add_argument('--tokens', type=int, default=cfg['tokens'])
parser.add_argument('--temperature', type=float, default=0.8)
parser.add_argument('--top-k', type=int, default=8)
parser.add_argument('--seed', type=int, default=cfg['seed'])
args = parser.parse_args()
assert 1 <= args.tokens <= cfg['max_tokens']
print(generate(model, tokenizer, args.prompt, args.tokens,
args.temperature, args.top_k, args.seed))
Check your understanding
The same checkpoint, , seed and should the same text. Explain why loading differs from training them and why an empty answer can occur when newline is selected first.
If you get stuck
Strange or empty output is expected from a tiny, briefly trained model. Check format, unknown characters and training . More sampling randomness is not a fix for insufficient training.
Runner & API
The loads the checkpoint once, accepts and calls generate. handles requests sequentially. checks the process; /generate returns generated text. This is a teaching , not a public service.
is the ’s loopback address. Run here in the ; your address bar points to a different computer. and & leave the server in the background while this tab runs. Reloading or closing the tab ends it. This environment does not require .
Type and try
Write runner.py and start it with the shown. Save its immediately. Check runner.log, then request /health and /generate. If the first health check is early, wait for the startup line and try again.
cd ~/xslm
nano runner.py
nohup python3 -u runner.py > runner.log 2>&1 &
echo $! > runner.pid
cat runner.log
curl -s http://127.0.0.1:8765/health
curl -s http://127.0.0.1:8765/generate -H 'Content-Type: application/json' -d '{"prompt":"Q: hi\nA:","tokens":8,"temperature":0.8}'
Practice in the terminal
runner.pyComplete source to type
One instance is shared by requests. Validate the body, generate, then return JSON. Binding to the guest loopback keeps this exercise to your .
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
from generate import load, generate
def main():
model, tokenizer, cfg = load()
class Handler(BaseHTTPRequestHandler):
def reply(self, status, body):
data = json.dumps(body, ensure_ascii=False).encode('utf-8')
self.send_response(status)
self.send_header('Content-Type', 'application/json; charset=utf-8')
self.send_header('Content-Length', str(len(data)))
self.end_headers()
self.wfile.write(data)
def do_GET(self):
if self.path == '/health':
self.reply(200, {'status': 'ok', 'model': 'XSLM'})
else:
self.reply(404, {'error': 'not found'})
def do_POST(self):
if self.path != '/generate':
return self.reply(404, {'error': 'not found'})
try:
length = int(self.headers.get('Content-Length', '0'))
if not 0 < length <= 4096:
raise ValueError('request size')
data = json.loads(self.rfile.read(length))
prompt = data['prompt']
tokens = data.get('tokens', cfg['tokens'])
temperature = data.get('temperature', 0.8)
top_k = data.get('top_k', 8)
seed = data.get('seed', cfg['seed'])
if not isinstance(prompt, str) or not 0 < len(prompt) <= 512:
raise ValueError('prompt')
if type(tokens) is not int or not 1 <= tokens <= cfg['max_tokens']:
raise ValueError('tokens')
if type(top_k) is not int or top_k < 0 or type(seed) is not int:
raise ValueError('top_k/seed')
if type(temperature) not in (int, float):
raise ValueError('temperature')
result = generate(model, tokenizer, prompt, tokens, temperature, top_k, seed)
self.reply(200, {'text': result, 'model': 'XSLM'})
except (ValueError, KeyError, TypeError):
self.reply(400, {'error': 'invalid request'})
address = ('127.0.0.1', cfg['port'])
print('XSLM Runner:', address, flush=True)
HTTPServer(address, Handler).serve_forever()
if __name__ == '__main__':
main()
Check your understanding
Expect status ok from health and a JSON text field from generation. Send an empty JSON object: the should return 400 instead of crashing. The response must come from your checkpoint, not a canned answer.
If you get stuck
: inspect runner.log and confirm a checkpoint exists. : keep the existing Runner or stop its recorded PID before restarting. After new , restart the Runner to load them.
XChat
is the interface, is the execution service, and is the learned . xchat.py reads a question, adds the format, sends an request and displays the text. It does not import the model or generate an answer itself.
This first runs inside the . Each question is independent: it does not yet send . A useful extension is to add history, while respecting the model’s small . Fluency is not the graduation requirement; an actual request through your own trained model is.
Type and try
Leave the Runner running. Type xchat.py and launch it. At You>, type hi and press Enter. Type /exit to return to . Only then run the final to stop the Runner. Export your complete project.
cd ~/xslm
nano xchat.py
python3 xchat.py
hi
/exit
kill "$(cat runner.pid)"
Practice in the terminal
xchat.pyComplete source to type
input reads a line; Request creates the ; reads the real response. Connection errors are reported instead of replaced with a fake answer.
import json
from pathlib import Path
from urllib.request import Request, urlopen
from urllib.error import URLError
def ask(question):
cfg = json.loads(Path('settings.json').read_text())
body = {'prompt': 'Q: ' + question + '\nA:', 'tokens': cfg['tokens']}
request = Request('http://127.0.0.1:' + str(cfg['port']) + '/generate',
data=json.dumps(body).encode(),
headers={'Content-Type': 'application/json'})
with urlopen(request, timeout=120) as response:
return json.load(response)['text']
if __name__ == '__main__':
print('XChat -> Runner -> XSLM | /exit')
while True:
try:
question = input('You> ').strip()
if question == '/exit':
break
if question:
print('XSLM>', ask(question))
except (EOFError, KeyboardInterrupt):
break
except (URLError, TimeoutError) as error:
print('Runner:', error)
Check your understanding
Show the chain XChat → HTTP Runner → loaded → generated text. Stop the Runner and try another chat: it must report a connection error. Restart it and verify chat works again. Explain each file to someone else.
If you get stuck
Do not type the kill command inside XChat; it would be sent as a question. Exit first. A successful response with poor text still demonstrates the pipeline; improve and training as your next experiment.
Loading…
Choose a pending booking to cancel. Each cancellation requires confirmation.
Your :