#[allow(dead_code)]
mod corpus;
use std::io::{Read, Write};
use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
use std::time::Instant;
use topos::{
Module, RmsNorm, Shape, Symbol, Tape, Tensor, Value, concat, cross_entropy, init, stack,
};
use corpus::{VOCABULARY_LEN, 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 TIMED_STEPS: usize = 300;
const WARMUP_STEPS: usize = 20;
fn parameter_payloads() -> Vec<Tensor<f32>> {
let mut weights = init::xavier(7);
let ones = Tensor::filled([EMBED_DIM], 1.0);
let mut payloads = vec![
init::normal(8, 1.0)(&Shape::new([VOCABULARY_LEN, EMBED_DIM])),
init::normal(9, 1.0)(&Shape::new([CONTEXT_LEN, EMBED_DIM])),
];
for _ in 0..HEAD_COUNT {
payloads.push(weights(&Shape::new([EMBED_DIM, HEAD_DIM])));
payloads.push(weights(&Shape::new([EMBED_DIM, HEAD_DIM])));
payloads.push(weights(&Shape::new([EMBED_DIM, HEAD_DIM])));
}
payloads.push(weights(&Shape::new([EMBED_DIM, EMBED_DIM])));
payloads.push(ones.clone());
payloads.push(weights(&Shape::new([EMBED_DIM, HIDDEN_LEN])));
payloads.push(weights(&Shape::new([HIDDEN_LEN, EMBED_DIM])));
payloads.push(ones.clone());
payloads.push(ones);
payloads.push(weights(&Shape::new([EMBED_DIM, VOCABULARY_LEN])));
payloads.push(weights(&Shape::new([VOCABULARY_LEN])));
payloads
}
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)
}
struct Formulation {
module: String,
forward_nodes: usize,
total_nodes: usize,
oracle_loss: f32,
}
fn recorded(
payloads: &[Tensor<f32>],
feeds: &(Tensor<f32>, Tensor<f32>),
batched: bool,
) -> Formulation {
let tape = Tape::new();
let mut supply = payloads.iter().cloned();
let mut next = move || supply.next().expect("payloads cover the model");
let embeddings = tape.parameter(next());
let positions_table = tape.parameter(next());
let heads: Vec<[Value<'_, f32>; 3]> = (0..HEAD_COUNT)
.map(|_| {
[
tape.parameter(next()),
tape.parameter(next()),
tape.parameter(next()),
]
})
.collect();
let projection = tape.parameter(next());
let attention_norm = RmsNorm::new(&tape, next(), Tensor::filled([], 1e-5));
let hidden_weights = tape.parameter(next());
let output_weights = tape.parameter(next());
let hidden_norm = RmsNorm::new(&tape, next(), Tensor::filled([], 1e-5));
let final_norm = RmsNorm::new(&tape, next(), Tensor::filled([], 1e-5));
let logit_weights = tape.parameter(next());
let logit_bias = tape.parameter(next());
let scale = tape.leaf(Tensor::filled([], 1.0 / (HEAD_DIM as f32).sqrt()));
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 stream = embeddings.gather(tokens) + positions_table.gather(positions);
let normalized = attention_norm.express(stream);
let attended = if batched {
let project = |slot: usize| {
let slices: Vec<_> = heads
.iter()
.map(|head| normalized.matmul(head[slot]))
.collect();
stack(&slices, 0)
};
let scores = project(0).matmul(project(1).permute([0, 2, 1]));
let scaled = scores * scale.broadcast_like(scores);
let weights = (scaled + mask.broadcast_along_like(0, scaled)).softmax(2);
let context = weights.matmul(project(2));
context.permute([1, 0, 2]).reshape([PACKED_LEN, EMBED_DIM])
} else {
let outputs: Vec<_> = heads
.iter()
.map(|head| {
let scores = normalized
.matmul(head[0])
.matmul(normalized.matmul(head[1]).transpose());
let scaled = scores * scale.broadcast_like(scores);
let weights = (scaled + mask).softmax(1);
weights.matmul(normalized.matmul(head[2]))
})
.collect();
concat(&outputs, 1)
};
let stream = stream + attended.matmul(projection);
let normalized = hidden_norm.express(stream);
let stream = stream
+ normalized
.matmul(hidden_weights)
.relu()
.matmul(output_weights);
let states = final_norm.express(stream);
let product = states.gather(extraction).matmul(logit_weights);
let loss = cross_entropy(
product + logit_bias.broadcast_along_like(0, product),
targets,
);
let forward_nodes = tape.len();
let wrt: Vec<Symbol> = std::iter::once(embeddings)
.chain([positions_table])
.chain(heads.iter().flatten().copied())
.chain([
projection,
hidden_weights,
output_weights,
logit_weights,
logit_bias,
])
.map(|value| value.symbol())
.collect();
let adjoints = tape.differentiate(loss, wrt);
let (tokens, targets, loss) = (tokens.symbol(), targets.symbol(), loss.symbol());
let network = tape.into_network();
let parameters = network.parameters();
let plan = network.entry(adjoints.roots()).lower();
let run = plan.forward(
¶meters,
[(tokens, feeds.0.clone()), (targets, feeds.1.clone())],
);
Formulation {
module: plan.emit_stablehlo().expect("the joint step emits"),
forward_nodes,
total_nodes: network.len(),
oracle_loss: run.of(loss).scalar(),
}
}
fn timed(module: &str, name: &str, request: &[u8], response_len: usize) -> (f32, f64) {
let directory = std::env::temp_dir().join("topos-attention-grading");
std::fs::create_dir_all(&directory).expect("the staging directory creates");
let module_path = directory.join(format!("{name}.mlir"));
let static_path = directory.join("static.bin");
let manifest_path = directory.join("manifest.json");
std::fs::write(&module_path, module).expect("the module writes");
std::fs::write(&static_path, []).expect("the empty static file writes");
let mut shapes: Vec<String> = parameter_payloads()
.iter()
.map(|payload| {
let extents: Vec<String> = payload
.shape()
.axes()
.iter()
.map(usize::to_string)
.collect();
format!("[{}]", extents.join(", "))
})
.collect();
shapes.push(format!("[{PACKED_LEN}, {VOCABULARY_LEN}]"));
shapes.push(format!("[{BATCH_LEN}, {VOCABULARY_LEN}]"));
std::fs::write(
&manifest_path,
format!("{{\"dynamic\": [{}]}}", shapes.join(", ")),
)
.expect("the manifest writes");
let python = std::env::var("TOPOS_XLA_PYTHON").unwrap_or_else(|_| "python3".to_string());
let mut command: Vec<String> = python.split_whitespace().map(str::to_string).collect();
command.push("tools/serve-stablehlo-xla.py".to_string());
let mut child: Child = Command::new(&command[0])
.args(&command[1..])
.arg(&module_path)
.arg(&static_path)
.arg(&manifest_path)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("the serving process starts; is `jax` installed for it?");
let mut requests: ChildStdin = child.stdin.take().expect("the server's input pipes");
let mut responses: ChildStdout = child.stdout.take().expect("the server's output pipes");
let mut step = |request: &[u8]| -> Vec<f32> {
requests.write_all(request).expect("the request writes");
requests.flush().expect("the request flushes");
let mut response = vec![0u8; 4 * response_len];
responses
.read_exact(&mut response)
.expect("the server answers; see its standard error");
response
.chunks_exact(4)
.map(|chunk| f32::from_le_bytes(chunk.try_into().expect("four bytes")))
.collect()
};
let first = step(request)[0];
for _ in 0..WARMUP_STEPS {
step(request);
}
let clock = Instant::now();
for _ in 0..TIMED_STEPS {
step(request);
}
let seconds = clock.elapsed().as_secs_f64();
let _ = child.kill();
let _ = child.wait();
(first, seconds)
}
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);
let batch = &samples[..BATCH_LEN];
let tokens: Vec<usize> = batch
.iter()
.flat_map(|(context, _)| context.iter().copied())
.collect();
let targets: Vec<usize> = batch.iter().map(|&(_, next)| next).collect();
let feeds = (
Tensor::selection(tokens, VOCABULARY_LEN, 1.0_f32),
Tensor::selection(targets, VOCABULARY_LEN, 1.0_f32),
);
let payloads = parameter_payloads();
let request: Vec<u8> = payloads
.iter()
.flat_map(|payload| payload.to_vec())
.chain(feeds.0.to_vec())
.chain(feeds.1.to_vec())
.flat_map(f32::to_le_bytes)
.collect();
for (name, batched) in [("head-loop", false), ("batched", true)] {
let formulation = recorded(&payloads, &feeds, batched);
let response_len = 1 + payloads
.iter()
.enumerate()
.filter(|&(index, _)| !is_norm_gain(index))
.map(|(_, payload)| payload.shape().volume())
.sum::<usize>();
let dots = formulation.module.matches("dot_general").count();
let (first, seconds) = timed(&formulation.module, name, &request, response_len);
let drift = (f64::from(formulation.oracle_loss) - f64::from(first)).abs()
/ f64::from(formulation.oracle_loss);
if std::env::var("TOPOS_ENVELOPE").as_deref() == Ok("report") {
if drift >= 1e-3 {
println!("{name}: RED ROW — left the oracle envelope, drift {drift:.1e}");
}
} else {
assert!(drift < 1e-3, "{name} left the oracle envelope: {drift:e}");
}
println!(
"{name:9} tape {} -> {} nodes, {} module lines, {dots} dot_general; \
first loss {first:.6} (oracle drift {drift:.1e}); \
{:.3} ms/step over {TIMED_STEPS} steps",
formulation.forward_nodes,
formulation.total_nodes,
formulation.module.lines().count(),
seconds * 1000.0 / TIMED_STEPS as f64,
);
}
}
fn is_norm_gain(index: usize) -> bool {
let after_heads = 2 + 3 * HEAD_COUNT;
index == after_heads + 1 || index == after_heads + 4 || index == after_heads + 5
}