The guide
Notebooks
Evcxr, the two rules a cell must follow, and the card every type draws itself as.
Topos runs in Evcxr, the Rust
Jupyter kernel and REPL. There is no wrapper API. The evcxr
feature adds rich cell output; everything else is the ordinary
crate.
cargo install --locked evcxr_jupyter
evcxr_jupyter --installFirst cell:
:dep topos = { version = "0.13", features = ["evcxr"] }
use topos::*;A ~/.config/evcxr/init.evcxr saves repeating that. Turn the
compile cache on — every cell is a real crate:
:cache 500
:dep topos = { version = "0.13", features = ["evcxr"] }Two rules#
Evcxr compiles each cell as its own crate and keeps the variables between them.
A persisted variable cannot borrow another one. A Value
borrows the tape, so it dies with the cell. Symbol is the
cross-cell name. End a recording cell with .symbol() and come
back through Tape::resolve. See Names.
let tape: Tape<f64> = Tape::new();
let w: Symbol = tape.parameter(0.0).symbol();
let x: Symbol = tape.input(0.0).symbol();
let y: Symbol = tape.input(0.0).symbol();let loss: Symbol = {
let (w, x, y) = (tape.resolve(w), tape.resolve(x), tape.resolve(y));
((w * x - y) * (w * x - y)).symbol()
};A persisted variable needs an explicit type. Evcxr infers a
cell from that cell alone. Annotate every binding that must
survive. This is Evcxr, not topos — it cannot infer
let v = vec![1.0_f64]; either.
Give each binding its own let. A destructured tuple does not
survive the cell, even with a type on the tuple: Evcxr cannot
name a type that came from a dependency.
Train#
into_network consumes the tape. Evcxr tracks the move: tape
ends where network begins. Training is a data loop over
caller-owned parameters. See Spec and
state.
let network: Network<f64> = tape.into_network();
let mut parameters: Parameters<f64> = network.parameters();let samples: Vec<(f64, f64)> = vec![(1.0, 2.0), (2.0, 4.0), (3.0, 6.0)];
for step in 0..300 {
let (sx, sy) = samples[step % 3];
let gradients = network
.forward(¶meters, [(x, sx.into()), (y, sy.into())])
.backward(loss)
.parameters(¶meters);
parameters = parameters.step(&gradients, |p, g| {
p.clone() - g.clone() * Tensor::from(0.02)
});
}parameters.of(w) // trained
network.parameters().of(w) // initials, materialized freshTo record more, into_tape consumes the network. Symbols still
resolve. Parameters::carried keeps trained slots and fills new
ones from their initials.
let tape: Tape<f64> = network.into_tape();
let cube: Symbol = { let w = tape.resolve(w); (w * w * w).symbol() };
let network: Network<f64> = tape.into_network();
let parameters: Parameters<f64> = parameters.carried(&network);Cell output#
0 Parameter [2, 2] 1 Input [1, 2] 2 MatMul 1, 0 [1, 2] 3 Tanh 2 [1, 2] 4 Sum 3 [] network: 5 nodes, 1 parameter, 1 input
Network at the end of a cell: the spec dump, exactly as describe prints it, in the card the notebook shows. Every card on this page is the HTML to_html emitted, embedded as is.0 Parameter [8, 16] freed after 5 1 Parameter [16] freed after 6 2 Parameter [16, 4] freed after 24 3 Parameter [4] freed after 10 4 Input [32, 8] freed after 47 5 MatMul 4, 0 [32, 16] freed after 7 6 BroadcastAlong 1 axis=0 extent=32 [32, 16] freed after 7 7 Add 5, 6 [32, 16] freed after 8 8 Tanh 7 [32, 16] freed after 33 9 MatMul 8, 2 [32, 4] freed after 11 10 BroadcastAlong 3 axis=0 extent=32 [32, 4] freed after 11 11 Add 9, 10 [32, 4] freed after 12 12 Sum 11 [] kept 13 Leaf [] freed after 14 14 Broadcast 13 shape=[32, 4] [32, 4] freed after 16 15 Leaf [32, 4] freed after 16 16 Add 15, 14 [32, 4] freed after 20 17 Leaf [32, 4] freed after 18 18 Add 17, 16 [32, 4] freed after 27 19 Leaf [32, 4] freed after 20 20 Add 19, 16 [32, 4] freed after 21 21 SumAlong 20 axis=0 [4] freed after 23 22 Leaf [4] freed after 23 23 Add 22, 21 [4] kept 24 Permute 2 order=[1, 0] [4, 16] freed after 25 25 MatMul 18, 24 [32, 16] freed after 29 26 Permute 8 order=[1, 0] [16, 32] freed after 27 27 MatMul 26, 18 [16, 4] freed after 31 28 Leaf [32, 16] freed after 29 29 Add 28, 25 [32, 16] freed after 35 30 Leaf [16, 4] freed after 31 31 Add 30, 27 [16, 4] kept 32 Leaf [32, 16] freed after 34 33 Mul 8, 8 [32, 16] freed after 34 34 Sub 32, 33 [32, 16] freed after 35 35 Mul 29, 34 [32, 16] freed after 37 36 Leaf [32, 16] freed after 37 37 Add 36, 35 [32, 16] freed after 41 38 Leaf [32, 16] freed after 39 39 Add 38, 37 [32, 16] freed after 48 40 Leaf [32, 16] freed after 41 41 Add 40, 37 [32, 16] freed after 42 42 SumAlong 41 axis=0 [16] freed after 44 43 Leaf [16] freed after 44 44 Add 43, 42 [16] kept 47 Permute 4 order=[1, 0] [8, 32] freed after 48 48 MatMul 47, 39 [8, 16] freed after 52 51 Leaf [8, 16] freed after 52 52 Add 51, 48 [8, 16] kept plan: forward; 49 of 53 nodes evaluated, 5 readable live volume: peak 2373 elements at node 29, retain-all 11922
live volume (elements)
── live
2500 ┤ ⡄ ⡤⢤ ⢠
│ ⡸⢱ ⢰⠁ ⢇ ⡎⡆
2000 ┤ ⡀ ⡤⠒⢤⠃⠸⡀ ⡎ ⠘⡄ ⢀ ⣀⣰⠁⢇
│ ⢠⢳ ⢰⠁ ⡇ ⡰⠁ ⢣ ⡎⡆⢠⠃ ⢸
1500 ┤ ⡜ ⢇ ⡎ ⢱⠴⠁ ⠘⡼ ⠸⡜ ⠈⡆
│ ⢠⠃ ⠈⢆ ⢀⡠⡀ ⣀⢄⡀⡠⠤⠔⠑⡄ ⡜ ⠁ ⠁ ⢣
1000 ┤ ⢠⠃ ⠈⠖⠁ ⠱⡀ ⡠⠊ ⠈ ⠈⠒⠤⠤⠚ ⢇ ⡰⠑⢢
│ ⢀⠇ ⠈⠊ ⠈⠉ ⠈⡆
500 ┤ ⡜ ⢇ ⡀
│ ⢀⣀⡰⠁ ⠘⠉
0 ┤⠒⠒⠁
└┬────────────┬────────────┬────────────┬────────────┬────────────┬
0 10 20 30 40 50
scheduled nodePlan: the schedule, then the live volume along it.| -0.1707 | -0.7486 | 0.6209 | 0.1285 | -0.0737 | -0.3882 |
| -0.0496 | -0.2663 | -0.5666 | -0.1346 | -0.6142 | 0.7124 |
| 0.6476 | 0.5753 | 0.5639 | 0.0748 | 0.5881 | -0.269 |
| 0.1845 | 0.3986 | 0.2704 | -0.6093 | -0.241 | -0.1181 |
Tensor small enough to print exactly, shaded by value. ── norm
18 ┤ ⣼
│ ⢰⠁⡇ ⡄
15 ┤ ⡎ ⢣ ⢰⢣
│ ⡸ ⢸ ⡜⢸
12 ┤ ⢠⠃ ⠈⡆ ⢀⠇ ⡇
│ ⡜ ⢇ ⢸ ⢇
│ ⣀⣀⠴⠁ ⢸ ⡇ ⢸
9 ┤ ⢀⠔⠉ ⠘⡄ ⢰⠁ ⠈⡆
│⢀⠔⠁ ⡇ ⡜ ⢇
6 ┤⠁ ⢱⢀⠇ ⢸ ⣀
│ ⠸⣸ ⠘⡄ ⢀⡠⠤⠒⠒⠒⠒⠒⠒⠒⠒⠒⠉⠉ ⠉⠒⠤⣀⣀⣀⣀⣀⣀⣀⣀⡀
3 ┤ ⠇ ⡇ ⢀⡠⠔⠊⠁ ⠈⢆
│ ⢸⡠⠔⠒⠉⠉⠉⠉⠉⠉⠉⠉⠁ ⠱⡀
0 ┤ ⠈
└┬────────┬─────────┬────────┬────────┬─────────┬────────┬────────┬──
0.0 2.5 5.0 7.5 10.0 12.5 15.0 17.5
nodeField of gradients: one Euclidean norm per node, along the tape. A vanishing or exploding region is a shape, not a number to hunt for.0 [3, 4] 1 [4] 2 [4, 2] 3 [2] 4 [] 0.01
Parameters: each slot's shape, and the value when it is a scalar.#0 → #32 #1 → #24
Adjoints: the target and each wrt → gradient pair.roots #5 #28 observes #3 memory forward-only
Entry: roots, observes, memory posture, numerics.With evcxr, the last expression in a cell draws itself instead
of dumping Debug:
| Ends with | Shows |
|---|---|
Value / Tensor | shape, extremes, payload — table when small, chart when large |
Tape / Network | the spec dump (describe); long dumps elide the middle |
Parameters | each slot's shape, and the value when it is a scalar |
Plan | the schedule, and live volume along it |
Field | one Euclidean norm per node, along the tape |
Run | the same profile for a forward pass |
Adjoints | each wrt → gradient pair |
Entry | roots, observes, memory posture, numerics |
Symbol | what it is, and how to read through it |
Each card is a to_html string plus evcxr_display, so output
is ordinary cargo test, and the terminal REPL gets
text/plain. Charts use malevich.
Rough edges#
- A shape mistake panics the cell. The session keeps its variables; only that cell's work is lost.
- A cell is a real compile. A small one is a fraction of a second; generics can take one to two. The cache helps. It is not Python-snappy.
topos