Python package
linnet-lang is a NumPy-only core that runs the compiler and exposes the program as typed objects, plus a backend per framework behind an extra.
Install
uv add "linnet-lang[torch]" # linnet.torch
uv add "linnet-lang[jax]" # linnet.jax
uv add "linnet-lang[flax]" # linnet.jax.load_nnx
uv add "linnet-lang[onnx]" # linnet.onnx (add onnxruntime or onnxruntime-gpu to run models)
uv add "linnet-lang[nest]" # linnet.nestThe wheels carry the linnet compiler. The package runs LINNET_BIN if it is set, then the wheel's compiler, then the one on PATH. In a checkout, uv sync --all-extras in python/linnet installs every backend and test dependency.
Modules
| Module | Contents |
|---|---|
linnet | load_program, Program, compile_plan, read_arrays, read_bindings |
linnet.ir, linnet.diagram | typed program, diagrams |
linnet.nest | Nest model zoo |
linnet.torch, linnet.jax, linnet.onnx | backends for PyTorch, JAX and Flax, ONNX |
linnet.triton, linnet.hf, linnet.gguf | integrations: Triton Inference Server, Transformers (vLLM, SGLang, TGI), GGUF and Ollama |
linnet.packing | examples packed into fixed-size batches as numpy arrays, for either framework |
linnet.jax.train | training in JAX |
linnet.train | supervised fine-tuning over packed sequences; linnet.train.grpo, reinforcement learning; linnet.train.dpo, preferences |
Typed program
from linnet import load_program
program = load_program("examples/01-llama/src/lib.linnet", std_root="stdlib")
print(program)llama::Model<Vocab: Dim, H: Dim, Heads: Dim, KvHeads: Dim, Inner: Dim, Layers: Dim, Batch: Dim, MaxSeq: Dim, T: Float = bf16>
sub embedding: Embedding<Vocab, H, T>
sub layers: [DecoderLayer<H, Heads, KvHeads, Inner, Batch, MaxSeq, T>; Layers]
sub norm: RmsNorm<H, T>
sub lm_head: Linear<H, Vocab, T>
pub entry forward<B: Dim, S: Dim>(tokens: Tensor[B, S; i32]) -> Tensor[B, S, Vocab; T]
pub entry decode(token: Tensor[Batch, 1; i32], pos: i32) -> Tensor[Batch, Vocab; T]
...load_program reads linnet plan output into frozen linnet.ir dataclasses; nothing executes. Dimensions stay symbolic:
from linnet import ir
entry = program.entry("forward")
ir.format_signature(entry)
# 'pub entry forward<B: Dim, S: Dim>(tokens: Tensor[B, S; i32]) -> Tensor[B, S, Vocab; T]'
weight = next(e for e in program.manifest if e.path.endswith("k_proj.weight"))
ir.format_shape(weight.shape) # 'KvHeads * (H / Heads), H'
weight.repeat # (DimSymbol(id=21, name='Layers'),)
for op in entry.body.walk(): # every operation, nested regions included
if op.kind == "call":
print(op.attrs["callee"], [ir.format_type(r.type) for r in op.results])| Class | Fields or cases |
|---|---|
Program | root, manifest, blocks, functions, constants |
Function | generics, params, results, states, body (a Region of Ops) |
Dim | int, DimSymbol, PackSize, DimExpr(op, args) with add, mul, floordiv, mod, min, max |
Unit | a Dim or a Pack (*S) |
DType | a name such as "bf16", or DTypeVar |
Type | ScalarType, TensorType, TupleType, OptionalType, ArrayType, NamedType (block, struct, enum), ShapeType, UnitType |
ir.substitute(type, ir.call_substitution(op)) rewrites a callee's types in the caller's generics. format_dim, format_shape, format_type and format_signature print Linnet syntax.
ir.bind_generics(program.root.generics, {"H": 64, ...}) gives the generics values (defaults fill the rest), and the Bindings it returns evaluate dimensions, shapes and dtypes: bindings.shape(weight.shape). Every backend runs from the same Program; compile_plan is load_program with the backends' options (functions=True for the module-level entries).
Diagrams
python -m linnet.diagram examples/01-llama/src/lib.linnet --std stdlib \
--entry forward --expand 1 -o forward.svglinnet.diagram draws an entry as a dataflow graph, with each edge's inferred tensor type (T[B, S, H]).
| Option | Effect |
|---|---|
--expand N | inline sub-block calls N levels deep; 0 shows blocks only |
--params | a node per parameter read |
--no-arithmetic | elementwise operations folded into edges |
--format svg|tikz|dot | inferred from -o (.svg, .tex, .dot) |
--theme light|dark | SVG colours |
diagram.build(program, "forward", expand=1) returns a Graph for to_svg, to_tikz or to_dot. TikZ output needs \usetikzlibrary{fit, arrows.meta}; DOT needs Graphviz.