topos

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 --install

First 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(&parameters, [(x, sx.into()), (y, sy.into())])
        .backward(loss)
        .parameters(&parameters);
    parameters = parameters.step(&gradients, |p, g| {
        p.clone() - g.clone() * Tensor::from(0.02)
    });
}
parameters.of(w)             // trained
network.parameters().of(w)   // initials, materialized fresh

To 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#

network: 5 nodes, 1 parameter, 1 input
   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
Plate 1. A 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.
plan
   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 node
Plate 2. A Plan: the schedule, then the live volume along it.
tensor · [4, 6] · f32 · min -0.7486 max 0.7124 mean 0.0215
-0.1707-0.74860.62090.1285-0.0737-0.3882
-0.0496-0.2663-0.5666-0.1346-0.61420.7124
0.64760.57530.56390.07480.5881-0.269
0.18450.39860.2704-0.6093-0.241-0.1181
Plate 3. A Tensor small enough to print exactly, shaded by value.
field · 19 nodes · max norm 17.9448 · mean norm 5.5597
                                ── norm
18 ┤           ⣼
   │          ⢰⠁⡇      ⡄
15 ┤          ⡎ ⢣     ⢰⢣
   │         ⡸  ⢸     ⡜⢸
12 ┤        ⢠⠃  ⠈⡆   ⢀⠇ ⡇
   │        ⡜    ⢇   ⢸  ⢇
   │     ⣀⣀⠴⠁    ⢸   ⡇  ⢸
 9 ┤  ⢀⠔⠉        ⠘⡄ ⢰⠁  ⠈⡆
   │⢀⠔⠁           ⡇ ⡜    ⢇
 6 ┤⠁             ⢱⢀⠇    ⢸                              ⣀
   │              ⠸⣸     ⠘⡄               ⢀⡠⠤⠒⠒⠒⠒⠒⠒⠒⠒⠒⠉⠉ ⠉⠒⠤⣀⣀⣀⣀⣀⣀⣀⣀⡀
 3 ┤               ⠇      ⡇           ⢀⡠⠔⠊⠁                         ⠈⢆
   │                      ⢸⡠⠔⠒⠉⠉⠉⠉⠉⠉⠉⠉⠁                               ⠱⡀
 0 ┤                                                                   ⠈
   └┬────────┬─────────┬────────┬────────┬─────────┬────────┬────────┬──
   0.0      2.5       5.0      7.5     10.0      12.5     15.0     17.5
                                    node
Plate 4. A Field of gradients: one Euclidean norm per node, along the tape. A vanishing or exploding region is a shape, not a number to hunt for.
parameters · 5 slots
   0  [3, 4]
   1  [4]
   2  [4, 2]
   3  [2]
   4  []  0.01
Plate 5. Parameters: each slot's shape, and the value when it is a scalar.
adjoints · 2 pairs · target #7
#0  →  #32
#1  →  #24
Plate 6. Adjoints: the target and each wrt → gradient pair.
entry · 2 roots · 1 observes · Exact
roots     #5  #28
observes  #3
memory    forward-only
Plate 7. An Entry: roots, observes, memory posture, numerics.

With evcxr, the last expression in a cell draws itself instead of dumping Debug:

Ends withShows
Value / Tensorshape, extremes, payload — table when small, chart when large
Tape / Networkthe spec dump (describe); long dumps elide the middle
Parameterseach slot's shape, and the value when it is a scalar
Planthe schedule, and live volume along it
Fieldone Euclidean norm per node, along the tape
Runthe same profile for a forward pass
Adjointseach wrt → gradient pair
Entryroots, observes, memory posture, numerics
Symbolwhat 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.