Apsi
Browse the docs · Apsi 2.0.0
You are reading the documentation of Apsi 2.0.0. The current version is Apsi 2.4.4 — what changed.

Docs 2.0.0 · 5 min read

Building AI with Apsi

Apsi does not force a ready-made architecture on you. Every network is an ordinary function that takes a "parameter tree" (record / list / struct), and grad differentiates with respect to every parameter in it. Whatever architecture you invent, you can write it down directly.

1. Build any architecture with use layers

Every layer is a pair of pure functions — init(seed) creates its parameters, layer(params, x) applies it — and layers compose into graphs. Models are ordinary values: grad, optimizers, save_params and fit work on them. This section is an overview; LAYERS.md is the complete reference (every layer with its shapes and parameters, error messages, recipes).

use layers
use nn

let model = sequential([linear(2, 64), activation("gelu"), dropout_layer(0.1), linear(64, 3)])
var params = model.init(seed=0)
let logits = model(params, X)                       # model(params, X, train=true) turns dropout on
let r = fit(model, params, X, y, loss="cross_entropy", opt=adam(lr=0.01), epochs=50, batch_size=64)
summary(model, r.params)                            # layer table with parameter counts
Kind Layers
dense linear(in, out), dense(in, out, act), bilinear, embedding(vocab, d), positional_encoding(len, d, learned)
convolution conv2d_layer(c_in, c_out, k, stride, padding="same"), max_pool(k), avg_pool(k), global_avg_pool(), flatten_layer()
recurrent lstm_layer(in, hidden, return_sequences), gru_layer(…), last_step()
attention multi_head_attention(d, heads, causal) (self- or cross-attention), transformer_block(d, heads, ff, causal), transformer_encoder(d, heads, layers)
graph neural networks gcn(in, out), sage(in, out, aggregate), gat(in, out, heads), gin(in, out), graph_pool("mean"|"sum"|"max") + the graph graph(edges=…, n=…, undirected, batch) and the differentiable ops segment_sum/mean/max/softmax, gather
normalisation layer_norm_layer(d), batch_norm_layer(d), rms_norm_layer(d), dropout_layer(p), scale(d)
activations activation("relu" | "gelu" | "silu" | "swish" | "mish" | "elu" | "leaky_relu" | "tanh" | "sigmoid" | "softplus" | "softmax") or activation(any_function)
composition sequential([...]), residual(inner, pre_norm), parallel(branches, combine="concat"|"add"|"mul"|"mean"|"list"), dag(nodes, inputs, output) — any directed acyclic graph, several inputs and outputs, add(), multiply(), concatenate(), lambda_layer(fn), select(i), identity(), reshape_layer(...)

Any graph of layers — each node is [name, layer, input(s)]:

let net = dag([
    ["wide", linear(2, 3), "x"],
    ["deep", mlp([2, 32, 32, 3]), "x"],
    ["both", add(), ["wide", "deep"]],
], inputs=["x"], output="both")

Graph neural networks — pass the graph when applying or fitting; mask limits the loss to labelled nodes:

let g = graph(edges=[[0, 1], [1, 2], [2, 0]], n=3, undirected=true)
let gnn = gat_net(8, 16, classes, heads=4)
let r = fit(gnn, gnn.init(seed=0), features, labels, loss="cross_entropy", graph=g, mask=train_mask, epochs=200)

Model zoo — mlp(sizes), resnet_mlp(in, out, width, blocks), cnn(channels, classes, image), lstm_classifier(in, hidden, classes), transformer_lm(vocab, d, heads, layers, context) (GPT-style), transformer_classifier(vocab, classes, …), autoencoder(in, latent), vae(in, latent) with vae_loss, gcn_net, gat_net, gin_net (graph classification), siamese(encoder, combine, head).

Training — fit(model, params, X, y, loss="mse"|"cross_entropy"|"mae"|"bce"|"vae"|fn, opt, epochs, batch_size, clip, schedule, graph, mask, val=(Xv, yv)) returns record(params, history, val_history); predict, summary, count_params. Full example: examples/28_any_architecture.apsi.

On GPUs with PyTorch — the same model, trained by PyTorch on CUDA / ROCm / Apple MPS / Intel XPU with mixed precision and torch.compile; the result is a normal Apsi parameter tree again (details: BACKENDS.md):

let r = fit(model, params, X, y, loss="cross_entropy", epochs=20, batch_size=256,
            backend="torch", device="cuda", precision="bf16", compile=true)     # run with --allow-python
let m = torch_module(model, r.params)        # a real torch.nn.Module: state_dict, DDP, ONNX export
check_torch()                                # every layer compared with NumPy (outputs and gradients)

2. Design your own, function by function

use nn

# a home-made architecture: gated residual blocks
fn block(p, x):
    let gate = sigmoid(x @ p.g.W + p.g.b)
    let h = gelu(layer_norm(x) @ p.f.W + p.f.b)
    return x + gate * h

fn model(p, x):
    var h = x @ p.inp.W
    for layer in p.blocks:
        h = block(layer, h)
    return h @ p.out.W

let params = record(inp=dense(16, 64), blocks=[record(g=dense(64, 64), f=dense(64, 64)) for _ in 0..4],
                    out=dense(64, 1))
let loss, grads = value_and_grad(fn(p) => mse(model(p, X), y))(params)

Building blocks:

Area Available
Ops @, einsum, conv2d, max-pool, attention, layer/batch/RMS norm, dropout, embeddings, causal mask, LSTM, GRU
Activations relu, gelu, sigmoid, tanh, softplus, softmax, log-softmax — and any function you write
Losses mse, cross-entropy, or your own (including physics losses that use higher-order autodiff)
Optimizers SGD (momentum), Adam, AdamW, RMSprop, Adagrad, L-BFGS (minimize), gradient clipping, LR schedules
Training the ready-made train() or your own loop (value_and_grad + opt.step)
Data use data (streaming, parallel, prefetch, SSD cache)
Precision f64/f32/f16/bf16/fp8/fp6/fp4/int8/int4, quantization-aware training (round_to)

3. Use less RAM / VRAM

Technique How Effect
Memory-mapped loading load_safetensors(path) any file size opens instantly; only the parts you use are read
Load in a small format load_safetensors(path, dtype=int8) / fp8_e4m3 / int4 / fp4_e2m1 4–8× smaller than f32 (16× smaller than f64)
Compress an existing model quantize_model(params, int8), then compute with qlinear(x, W, b) example 26: 67.5 KB → 18.6 KB with the same accuracy (99.3 %)
Train to tolerate compression round_to(W, fp8_e4m3) inside the forward pass gradients pass through (straight-through estimator)
Data larger than RAM open_tensor / load_tensor(mmap=true) + from_tensors(batch=…) stream from SSD

4. Use pretrained models from other ecosystems

Install the library with pip as usual and call it from Apsi (run with --allow-python):

use models
let classify = hf_pipeline("sentiment-analysis")          # pip install transformers torch
print(classify("Apsi is great"))

let net = onnx_model("resnet50.onnx")                       # pip install onnxruntime(-gpu)
let logits = net(image_batch)                               # picks TensorRT / CUDA / DirectML / CPU for you

let weights = torch_load("checkpoint.pt")                   # pip install torch (safe weights_only mode)
let hf_weights = load_safetensors("model.safetensors", dtype=int8)   # no extra library needed

Existing PyTorch/numpy code can be translated with apsi convert model.py: nn.Module models, losses, optimizers, data loaders and training loops become native Apsi that runs without Python or PyTorch, with the same outputs and gradients as PyTorch for the same weights. load_torch_weights(model, params, state_dict) imports pretrained weights (see PYTHON.md). Any other Python library: pyimport("module"), then to_torch / from_torch to move tensors back and forth (after use layers, torch_module(model, params) turns a whole Apsi model into a torch.nn.Module). apsi pkg install ai installs ONNX Runtime, PyTorch and Transformers into Apsi's own runtime.

5. Serve it as an API

use web
let app = server()
@app.post("/predict")
fn predict(req) = {"y": model(params, tensor(req.json()["x"]))}
app.run(port=8080)

Complete examples: examples/23_web_api.apsi, 26_model_serving.apsi and 27_pretrained_models.apsi. Server security is covered in SERVER.md.

6. Save and share models

save_model writes an Apsi checkpoint (.apsim): a standard safetensors file that also keeps units, compressed tensors and the parameter tree, checks a SHA-256 when loading, can be signed, and splits huge models into shards. save_gguf writes GGUF for llama.cpp, Ollama and LM Studio, with Q8_0 and Q4_0 bit-identical to ggml. apsi model convert converts between safetensors, GGUF and .apsim. Full reference: MODEL-FILES.md.

use models
save_model("ckpt/model.apsim", params, metadata=record(step=step), sign=true)
let params = load_model("ckpt/model.apsim", key=true)
save_gguf("model-q8.gguf", params, dtype="q8_0")

Model file safety

  • load_safetensors never runs code from the file (unlike pickle) and checks that every offset stays inside the file
  • load_model and load_gguf check every header size and offset against the file before mapping it, and load_model checks the SHA-256 (and the signature when you give a key)
  • torch_load uses weights_only=True, so code embedded in a .pt file is not executed
  • the on-disk cache is signed with HMAC

Found a mistake? Report it · Markdown source