mod chart;
mod corpus;
use std::time::Instant;
use topos::{Dropout, Module, RmsNorm, Shape, Tape, Tensor, Value, concat, cross_entropy, init};
use chart::loss_chart;
use corpus::{VOCABULARY_LEN, draw, from_token, load_names, shuffle, training_samples};
const CONTEXT_LEN: usize = 8;
const EMBED_DIM: usize = 32;
const HEAD_COUNT: usize = 2;
const HEAD_DIM: usize = EMBED_DIM / HEAD_COUNT;
const HIDDEN_LEN: usize = 4 * EMBED_DIM;
const BATCH_LEN: usize = 32;
const PACKED_LEN: usize = BATCH_LEN * CONTEXT_LEN;
const KEEP: f64 = 0.9;
struct Head<'tape> {
query: Value<'tape, f32>,
key: Value<'tape, f32>,
value: Value<'tape, f32>,
}
struct Model<'tape> {
embeddings: Value<'tape, f32>,
positions: Value<'tape, f32>,
heads: Vec<Head<'tape>>,
projection: Value<'tape, f32>,
attention_norm: RmsNorm<f32>,
hidden_weights: Value<'tape, f32>,
output_weights: Value<'tape, f32>,
hidden_norm: RmsNorm<f32>,
final_norm: RmsNorm<f32>,
logit_weights: Value<'tape, f32>,
logit_bias: Value<'tape, f32>,
scale: Value<'tape, f32>,
}
impl<'tape> Model<'tape> {
fn new(tape: &'tape Tape<f32>) -> Self {
let mut weights = init::xavier(7);
let ones = Tensor::filled([EMBED_DIM], 1.0);
let epsilon = Tensor::filled([], 1e-5);
Self {
embeddings: tape.parameter(init::normal(8, 1.0)(&Shape::new([
VOCABULARY_LEN,
EMBED_DIM,
]))),
positions: tape.parameter(init::normal(9, 1.0)(&Shape::new([CONTEXT_LEN, EMBED_DIM]))),
heads: (0..HEAD_COUNT)
.map(|_| Head {
query: tape.parameter(weights(&Shape::new([EMBED_DIM, HEAD_DIM]))),
key: tape.parameter(weights(&Shape::new([EMBED_DIM, HEAD_DIM]))),
value: tape.parameter(weights(&Shape::new([EMBED_DIM, HEAD_DIM]))),
})
.collect(),
projection: tape.parameter(weights(&Shape::new([EMBED_DIM, EMBED_DIM]))),
attention_norm: RmsNorm::new(tape, ones.clone(), epsilon.clone()),
hidden_weights: tape.parameter(weights(&Shape::new([EMBED_DIM, HIDDEN_LEN]))),
output_weights: tape.parameter(weights(&Shape::new([HIDDEN_LEN, EMBED_DIM]))),
hidden_norm: RmsNorm::new(tape, ones.clone(), epsilon.clone()),
final_norm: RmsNorm::new(tape, ones, epsilon),
logit_weights: tape.parameter(weights(&Shape::new([EMBED_DIM, VOCABULARY_LEN]))),
logit_bias: tape.parameter(weights(&Shape::new([VOCABULARY_LEN]))),
scale: tape.leaf(Tensor::filled([], 1.0 / (HEAD_DIM as f32).sqrt())),
}
}
fn states(
&self,
_tape: &'tape Tape<f32>,
tokens: Value<'tape, f32>,
positions: Value<'tape, f32>,
mask: Value<'tape, f32>,
dropouts: &[Dropout<f32>; 2],
) -> Value<'tape, f32> {
let stream = self.embeddings.gather(tokens) + self.positions.gather(positions);
let normalized = self.attention_norm.express(stream);
let heads: Vec<Value<'tape, f32>> = self
.heads
.iter()
.map(|head| {
let query = normalized.matmul(head.query);
let key = normalized.matmul(head.key);
let scores = query.matmul(key.transpose());
let scaled = scores * self.scale.broadcast_like(scores);
let weights = (scaled + mask).softmax(1);
weights.matmul(normalized.matmul(head.value))
})
.collect();
let stream = stream + dropouts[0].express(concat(&heads, 1).matmul(self.projection));
let normalized = self.hidden_norm.express(stream);
let stream = stream
+ dropouts[1].express(
normalized
.matmul(self.hidden_weights)
.relu()
.matmul(self.output_weights),
);
self.final_norm.express(stream)
}
fn logits(
&self,
states: Value<'tape, f32>,
extraction: Value<'tape, f32>,
) -> Value<'tape, f32> {
let product = states.gather(extraction).matmul(self.logit_weights);
product + self.logit_bias.broadcast_along_like(0, product)
}
}
fn block_causal_mask(samples: usize) -> Tensor<f32> {
let rows = samples * CONTEXT_LEN;
let mut elements = Vec::with_capacity(rows * rows);
for query in 0..rows {
for key in 0..rows {
let same_sample = query / CONTEXT_LEN == key / CONTEXT_LEN;
let causal = key % CONTEXT_LEN <= query % CONTEXT_LEN;
elements.push(if same_sample && causal {
0.0
} else {
f32::NEG_INFINITY
});
}
}
Tensor::new([rows, rows], elements)
}
fn main() {
let names = load_names();
let mut samples = training_samples::<CONTEXT_LEN>(&names);
let mut shuffle_state: u64 = 9;
shuffle(&mut samples, &mut shuffle_state);
println!("loaded {} names, {} samples", names.len(), samples.len());
let tape = Tape::new();
let model = Model::new(&tape);
let tokens = tape.input(Tensor::selection(vec![0; PACKED_LEN], VOCABULARY_LEN, 1.0));
let positions = tape.leaf(Tensor::selection(
(0..PACKED_LEN)
.map(|row| row % CONTEXT_LEN)
.collect::<Vec<_>>(),
CONTEXT_LEN,
1.0,
));
let mask = tape.leaf(block_causal_mask(BATCH_LEN));
let extraction = tape.leaf(Tensor::selection(
(0..BATCH_LEN)
.map(|sample| sample * CONTEXT_LEN + CONTEXT_LEN - 1)
.collect::<Vec<_>>(),
PACKED_LEN,
1.0,
));
let targets = tape.input(Tensor::selection(vec![0; BATCH_LEN], VOCABULARY_LEN, 1.0));
let dropouts = [
Dropout::new(&tape, [PACKED_LEN, EMBED_DIM]),
Dropout::new(&tape, [PACKED_LEN, EMBED_DIM]),
];
let states = model.states(&tape, tokens, positions, mask, &dropouts);
let loss = cross_entropy(model.logits(states, extraction), targets);
let sample_tokens = tape.input(Tensor::selection(vec![0; CONTEXT_LEN], VOCABULARY_LEN, 1.0));
let sample_positions = tape.leaf(Tensor::selection(
(0..CONTEXT_LEN).collect::<Vec<_>>(),
CONTEXT_LEN,
1.0,
));
let sample_mask = tape.leaf(block_causal_mask(1));
let sample_extraction = tape.leaf(Tensor::selection(vec![CONTEXT_LEN - 1], CONTEXT_LEN, 1.0));
let sample_dropouts = [
Dropout::new(&tape, [CONTEXT_LEN, EMBED_DIM]),
Dropout::new(&tape, [CONTEXT_LEN, EMBED_DIM]),
];
let sample_states = model.states(
&tape,
sample_tokens,
sample_positions,
sample_mask,
&sample_dropouts,
);
let sample_probabilities = model.logits(sample_states, sample_extraction).softmax(1);
let (tokens, targets, loss, sample_tokens, sample_probabilities) = (
tokens.symbol(),
targets.symbol(),
loss.symbol(),
sample_tokens.symbol(),
sample_probabilities.symbol(),
);
let recorded_nodes = tape.len();
let network = tape.into_network();
let mut parameters = network.parameters();
let training_plan = network.entry([loss]).backward().lower();
let sampling_plan = network.entry([sample_probabilities]).lower();
let mut dropout_masks = init::dropout::<f32>(13, KEEP);
let mask_shape = Shape::new([PACKED_LEN, EMBED_DIM]);
let fast = Tensor::new([], [0.1]);
let slow = Tensor::new([], [0.01]);
let mut window_loss = 0.0;
let mut losses = Vec::new();
let training = Instant::now();
for step in 0..5000 {
let start = (step * BATCH_LEN) % (samples.len() - BATCH_LEN);
let batch = &samples[start..start + BATCH_LEN];
let batch_tokens: Vec<usize> = batch
.iter()
.flat_map(|(context, _)| context.iter().copied())
.collect();
let batch_targets: Vec<usize> = batch.iter().map(|&(_, next)| next).collect();
let run = training_plan.forward(
¶meters,
[
(tokens, Tensor::selection(batch_tokens, VOCABULARY_LEN, 1.0)),
(
targets,
Tensor::selection(batch_targets, VOCABULARY_LEN, 1.0),
),
(dropouts[0].mask(), dropout_masks(&mask_shape)),
(dropouts[1].mask(), dropout_masks(&mask_shape)),
],
);
let batch_loss = run.of(loss).scalar();
losses.push(batch_loss);
if step == 0 {
println!(
"step 0: minibatch loss = {batch_loss:.4} (a uniform model costs ln 27 ~ 3.30)"
);
}
window_loss += batch_loss;
if (step + 1) % 500 == 0 {
println!(
"steps {:4}..{:4}: mean minibatch loss = {:.4}",
step + 1 - 500,
step + 1,
window_loss / 500.0
);
window_loss = 0.0;
}
let gradients = run.backward(loss).parameters(¶meters);
let learning_rate = if step < 4000 { &fast } else { &slow };
parameters = parameters.step(&gradients, |parameter, gradient| {
parameter.clone() - gradient.clone() * learning_rate.broadcast_like(gradient)
});
}
println!(
"trained {} steps in {:.3}s",
losses.len(),
training.elapsed().as_secs_f64()
);
assert_eq!(network.len(), recorded_nodes);
println!("the tape held {recorded_nodes} nodes through every step");
println!("{}", loss_chart("transformer training", &losses));
println!("sampled names:");
let mut state: u64 = 7;
for _ in 0..10 {
let mut window = [0usize; CONTEXT_LEN];
let mut name = String::new();
loop {
let run = sampling_plan.forward(
¶meters,
[(
sample_tokens,
Tensor::selection(window.to_vec(), VOCABULARY_LEN, 1.0),
)],
);
let row = run.of(sample_probabilities).to_vec();
let token = draw(&row, &mut state);
if token == 0 {
break;
}
name.push(from_token(token));
window.rotate_left(1);
window[CONTEXT_LEN - 1] = token;
}
println!(" {name}");
}
}