Core¶
Expr represents a calculation whose inputs are not yet known. Function gives that calculation
named inputs and outputs so that you can evaluate it, differentiate it, or generate C code.
The functions guide shows how to use both.
The first sections cover modelling. Verification, rewriting, and text rendering are lower-level interfaces for inspecting or extending the compiler.
Expressions¶
Expr
dataclass
¶
One node of the expression graph: an op, its argument nodes and the resulting TensorType.
Nodes are immutable and interned, so building the same expression twice returns the same
object. Create leaves with sym and const, then combine them with the operators, methods
and builders below.
support
property
¶
Flat row-major indices of the entries that may be nonzero, or None when any may be.
Every other entry is zero for all finite input values. A product with a constant zero counts as zero. The result is a set or a sorted array of indices, computed on first access and cached.
scalar ¶
Return this expression with a hint to expand its procedure into straight-line scalar code.
The hint overrides the automatic size limits and applies to the entry point too. Procedures
expand only when scalar substitution preserves each store's rounding and conversion. A
block hint in the same function wins. See the lowering page of How it works for the policy.
block ¶
Return this expression with a hint to keep its procedure as loops over buffers.
The procedure is not expanded into scalar code and survives as its own C procedure. Every other optimization still runs on it.
values ¶
Return the stored values in row-major coordinate order, as sc.values does.
to_dense ¶
Place stored values at their coordinates, filling other entries with zero.
sum ¶
Sum over the chosen axes, or every axis when axis is None.
Negative axes count from the end. An empty tuple leaves every entry unchanged.
keepdims retains the reduced axes with extent one. Empty domains sum to zero.
The result keeps the operand's dtype.
mean ¶
Average over the chosen axes, counting structural zeros in sparse inputs.
Axes and keepdims follow sum. An empty reduction domain returns NaN.
sym(name, shape=None, *, dtype=dtypes.float64, diff=True)
staticmethod
¶
Create a named symbolic input, the leaf every graph is built from.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
the input's name, which becomes its name on any |
required |
shape
|
int | tuple[int, ...] | None
|
an |
None
|
dtype
|
DType | str
|
the element type, |
float64
|
diff
|
bool
|
whether derivatives with respect to this input are meaningful. Setting it to
|
True
|
const(value, *, dtype=None, pattern=None)
staticmethod
¶
Wrap an array or scalar as a constant node.
A NumPy array or scalar keeps its dtype, and anything else becomes float64, unless
dtype says otherwise. Constants are never differentiable: a derivative with respect to one
is structurally zero.
A SciPy sparse array becomes a sparse constant with its pattern. Duplicate entries are summed
and explicitly stored zeros stay stored. pattern instead makes value the stored values
of a sparse constant, in the pattern's coordinate order.
ExprOp ¶
Bases: StrEnum
The expression dialect's operation set.
A StrEnum, so an op is a proper enum value and still prints and serializes as its name.
OP_INFO carries the arity and the differentiability of each.
OpInfo
dataclass
¶
What the compiler knows about one ExprOp: its arity (None for variadic) and whether it
has derivative rules. OP_INFO holds one per op.
pattern_rule gives the stored pattern of an elementwise op or MATMUL with a sparse
operand. An elementwise op without one refuses a sparse operand. Movement ops and reductions
compute the pattern from the stored coordinates instead. support_rule names how Expr.support follows the op, None when
any entry may be nonzero.
Functions¶
Function
dataclass
¶
A Python body and declarations, traced once for each binding of its shape holes.
Calling with symbolic leaves embeds the selected instance in a graph. Numerical leaves
evaluate its compiled code. Each call takes one positional argument per declared parameter.
instantiate returns the concrete graph for inspection or ahead-of-time compilation.
instantiate ¶
instantiate(shapes: tuple[int | tuple[int, ...] | TensorType, ...] | None = None) -> ConcreteFunction[SymbolicInputs, NumericalInputs, SymbolicOutputs, NumericalOutputs]
Bind input shape holes in flat leaf order and return the cached graph instance.
A fully declared function needs no arguments. Bare functions must be bound by calling them.
TensorType bindings must agree with the declaration's dtype and differentiability.
symbolic_call ¶
Embed a symbolic call, including the zero-input case.
numerical_call ¶
Bind shapes, compile the instance, and evaluate its numerical outputs.
factory ¶
factory(name: str, inputs: Sequence[str], outputs: Sequence[str | DerivSpec], aux: Any = None) -> Function
Derive named outputs once per binding, with one parameter per selected input leaf.
Shape templates must include every open source input in inputs. Fixed source inputs
may be omitted when the requested outputs do not depend on them. Seeds use fwd:<input>
and weights use lam:<output>, as in ConcreteFunction.factory.
sparsity ¶
Return a sparse output's stored pattern without numerical evaluation or compilation.
Pass the same arguments as a call to select its binding. Fully specified functions also
accept no arguments. of selects an output by name and is required for multiple outputs.
Dense outputs return None. A batched sparse output returns the pattern its matrices share.
recompile ¶
Drop a fully specified function's compiled handle so its next numerical call uses another cache path.
solver_stats ¶
Return the latest statistics for a solver in the fully specified graph.
Function.instantiate returns a concrete instance, which holds the resolved shapes and
expression graph for inspection and export.
ConcreteFunction
dataclass
¶
A named expression graph: named inputs, named outputs, and the computation between them.
ConcreteFunction is the unit of composition, differentiation and compilation. Calling fn(inputs)
with Expr leaves puts one call node in a larger graph. Lowering may still inline a small
callee when it expands a procedure into scalar code. fn.factory(...) derives a new
ConcreteFunction carrying the requested derivatives. Calling fn(inputs) with array leaves lowers
it, renders C, compiles and caches a shared library, and calls it through the pointer entry that
every generated function shares.
Each call takes one argument per parameter in the input tree. A group remains a tuple-valued
parameter. Empty calls evaluate numerically; symbolic_call() embeds a zero-input graph.
A single-output result is a bare leaf.
Input and output names matter beyond display. Derivatives are requested by name, and the generated C symbols are built from them.
A ConcreteFunction is immutable. Use @scaly.function(...) to build one from a Python body,
or ConcreteFunction.build from expressions already in hand.
results
property
¶
Everything one call computes: the outputs, then the residuals of a custom reverse rule.
result_names
property
¶
The output names, then the first of res:0, res:1, ... that no output takes, one per residual.
nodes
cached
property
¶
Every expression node the results reach, each after its operands.
input_shapes
property
¶
The input leaf shapes in C-signature order.
output_shapes
property
¶
The output leaf shapes in C-signature order.
build
classmethod
¶
build(name: str, input_tree: Tree[Any, Any], inputs: Sequence[Expr], output_tree: Tree[Any, Any], outputs: Sequence[Expr], *, output_coloring_widths: Sequence[int | None] | None = None, device: DeviceSpec | str | None = None, descriptor: Any = None, role: Role | None = None) -> ConcreteFunction[Any, Any, Any, Any]
Build a function from its trees and the expressions at their leaves.
inputs are the INPUT expressions of input_tree's leaves and outputs the
expressions of output_tree's leaves, both in flat leaf order. Every input the outputs
depend on must be among inputs.
input_map ¶
The symbolic input leaves keyed by their declared names, in declaration order.
output_map ¶
The output expressions keyed by their declared names, in declaration order.
symbolic_call ¶
Embed a call using one symbolic argument per declared parameter.
numerical_call ¶
Compile and evaluate one numerical argument per declared parameter.
recompile ¶
Drop the compiled handle so the next numerical call uses another cache path.
solver_stats ¶
Return the latest stats for a solver reached by this compiled function.
factory ¶
factory(name: str, inputs: Sequence[str], outputs: Sequence[str | DerivSpec], aux: Mapping[str, Sequence[str]] | None = None) -> ConcreteFunction
Build a new ConcreteFunction whose outputs mix this function's outputs and derivatives of them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
the new function's name, which also names its generated C symbols. |
required |
inputs
|
Sequence[str]
|
the input names, in order. Besides the declared inputs, |
required |
outputs
|
Sequence[str | DerivSpec]
|
output names and |
required |
aux
|
Mapping[str, Sequence[str]] | None
|
extra outputs usable by name in |
None
|
Returns:
| Type | Description |
|---|---|
ConcreteFunction
|
A |
Raises:
| Type | Description |
|---|---|
ValueError
|
if a name in |
Types¶
TensorType
dataclass
¶
The shape and dtype of an expression, and whether it is differentiable (diff).
A pattern makes the tensor sparse: it lists the coordinates stored in each matrix over the
last two axes, and every other entry is zero. Leading axes are batch axes sharing the pattern.
DType
dataclass
¶
Interned dtype descriptor.
See dtypes for the canonical instances. A DType compares equal to its string name,
so dtypes.float64 == "float64" holds.
Source code in src/scaly/ir/types.py
dtypes ¶
Canonical interned dtype instances. Mirrors the small tinygrad-style registry.
SparsityPattern
dataclass
¶
The structurally nonzero entries of a matrix, as coordinate lists rows and cols.
The entry order is the order of the values stored alongside the pattern. It is not necessarily
row- or column-major. to_csr and to_csc return the permutation into either order.
canonicalize ¶
Return the shared row-major pattern and the permutation that reorders its values.
values_canonical[k] = values[permutation[k]]. Stored zeros keep their coordinates. The
pattern is what TensorType(shape, pattern=...) takes.
to_csr ¶
(row_ptr, col_ind, val_perm): val_perm[k] is the COO position of CSR slot k, so
values_csr[k] = values[val_perm[k]] pairs a compact COO-ordered value buffer with the CSR
indices. The COO order is arbitrary, for example piece by piece for a mapped sparse Jacobian.
to_csc ¶
(col_ptr, row_ind, val_perm), the compressed-column counterpart of to_csr.
CanonicalPattern
dataclass
¶
The stored coordinates of a sparse matrix, in row-major order.
Equal coordinates give the same object, so is compares two patterns. Build one with
SparsityPattern.canonicalize. The digest identifies the shape and coordinates.
as_dtype ¶
Coerce a dtype name, a DType or None into a canonical DType.
None means float64, scaly's default.
Source code in src/scaly/ir/types.py
Builders¶
einsum ¶
Contract labelled axes, with NumPy's string subscript and broadcasting rules.
A label repeated within one operand selects a diagonal, and ... stands for
broadcast leading axes. Operands contract pairwise from left to right as matrix
products, so their order decides the size of each intermediate. Sparse operands
follow the restrictions of these operations, including the refusal of batched
sparse matrix products.
diag ¶
Build a diagonal matrix from a vector, or extract a matrix's diagonal.
k selects the diagonal above the main one when positive, below when negative.
Only rank-one and rank-two inputs are accepted, following numpy.diag.
matmul ¶
Matrix product with NumPy's matmul rules.
The last two axes of each operand multiply as matrices and leading axes broadcast. A vector on the left acts as one row and a vector on the right as one column, and that axis is removed from the result.
dot ¶
Scalar product of two expressions with the same number of entries, whatever their shapes.
sumsqr ¶
reduce_max ¶
Maximum over axes, propagating NaN and splitting derivatives equally at ties.
Axes and keepdims follow Expr.sum. Structural zeros participate in sparse
reductions. An empty reduction domain raises ValueError during construction.
Source code in src/scaly/ir/expr.py
reduce_min ¶
Minimum over axes, with the same shape, NaN and derivative rules as reduce_max.
argmax ¶
Index of the first maximum or NaN, as nondifferentiable int64 values.
Floating operands, a single integer axis or None, and keepdims follow
NumPy. With axis=None indices address the flattened logical array. Structural
zeros participate. An empty reduction domain raises ValueError.
Function output leaves currently require float64. Cast indices to float64 to
return them numerically, or use the integer expression inside the function.
Source code in src/scaly/ir/expr.py
argmin ¶
Index of the first minimum or NaN, with the same rules as argmax.
norm_inf ¶
norm_1 ¶
norm_2 ¶
stack ¶
Join equally shaped expressions along a new axis, adding one to the rank.
concat ¶
Join expressions along an existing axis. All other dimensions must agree.
split ¶
Split x along axis, into sections equal parts or at the given boundaries.
Source code in src/scaly/ir/expr.py
vec ¶
gather ¶
gather(x: Any, indices: Any, *, axis: int | None = None, mode: str = 'promise_in_bounds', fill_value: float = 0.0) -> Expr
Read x at int64 indices, preserving the index shape.
With axis=None, indices address the flattened source. Otherwise, the
chosen axis is replaced by the index shape, as with NumPy's take.
fill returns fill_value outside the source. promise_in_bounds
requires every index to lie within the source.
scatter ¶
scatter(base: Any, indices: Any, values: Any, *, axis: int | None = None, combine: str = 'add', mode: str = 'promise_in_bounds') -> Expr
Update a tensor at flat int64 indices, resolving duplicates in index order.
combine is set, add, max or min. fill discards
out-of-range updates. axis=None addresses the flattened base. With an
axis, values have the shape a gather at the same indices would return.
segment_sum ¶
Sum flat values by segment id into a vector of length n.
Segment ids are int64 values in [0, n). Empty segments are zero, and
repeated ids accumulate in input order.
segment_max ¶
Take the maximum in each segment, using fill_value for empty segments.
Out-of-range segment ids are discarded. NaN propagates within a segment. Derivatives split equally between tied values, including the initial fill.
take ¶
Read entries along axis, inserting the shape of the int64 indices.
Negative indices are out of range. Under fill they return fill_value.
dynamic_slice ¶
dynamic_slice(x: Any, start: Any, size: int, *, axis: int = -1, mode: str = 'fill', fill_value: float = 0.0) -> Expr
Read a fixed-size interval with an int64 start that may vary at runtime.
Positions past either end of the selected axis return fill_value under
fill, with the start left unchanged.
index_add ¶
Add updates along axis, summing duplicate indices in input order.
index_set ¶
Replace entries along axis. The last update to each entry wins.
atan2 ¶
minimum ¶
maximum ¶
where ¶
Choose elementwise: x where the bool condition holds, y elsewhere.
The three broadcast together and x and y share a dtype. The generated C computes both
branches, so a branch may be NaN or infinite where it is not chosen, and the derivative ignores
the branch not chosen too: where(x > 0, sqrt(x), 0) has derivative zero at x = 0,
including when a Function or vmap computes the square root. The condition has no derivative.
Custom derivative rules receive numerical seeds without condition flags and must return finite
results on inactive entries. In reverse mode, a select inside a mapped Function also guards
derivatives of the map's arguments. A selected zero tangent guards later partials, including across
ordinary and mapped calls:
sqrt(where(x > 0, x, 0)) has derivative zero at x = -1 in both modes.
Dense matrix products drop the condition in reverse mode: for a 2-by-2 matrix of ones M,
where(x > 0, sqrt(x) @ M, 0).sum() at x = [0, 0] gives a zero forward product
with seed [1, 1], but gradient [NaN, NaN].
Source code in src/scaly/ir/expr.py
cast ¶
Convert x elementwise to dtype, as NumPy's astype does.
A float becomes an integer by dropping its fraction, and a float outside the integer's range,
NaN included, has no defined result. A cast to bool is x != 0, so NaN is true. A cast
keeps its derivative only from one float dtype to another.
uint8 is not supported for expressions and raises ValueError.
Source code in src/scaly/ir/expr.py
equal ¶
Elementwise x == y as a bool expression. The == operator compares expressions by identity.
not_equal ¶
isfinite ¶
Elementwise test that a floating value is neither infinite nor NaN.
The generated C refuses to compile under -ffast-math, which assumes no value is either.
Source code in src/scaly/ir/expr.py
print(fmt, *values) ¶
Return the first of values, printing them all each time generated code computes it.
fmt has one {} for each value, and {{ and }} for literal braces. Each print ends
its line. A float64 prints with 17 significant digits and a float32 with 9, enough to
read each back exactly, an integer in full and a bool as True or False, and a tensor
prints flat as [a, b, ...]. The print is part of the returned expression, so a
function's outputs have to use it. A print they do not use raises ValueError when Scaly runs
the function body to build its expression graph.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fmt
|
str
|
the text to print, with a |
required |
values
|
Any
|
the expressions to print. The first is returned. |
()
|
vmap ¶
vmap(callee: Function[SI, NI, SO, NO] | ConcreteFunction[SI, NI, SO, NO], length: int) -> Function[SI, NI, SO, NO]
Map independent calls over a leading axis, preserving parameter and output trees.
Each output leaf has shape (length, *callee_shape). Use broadcast for a whole
shared tensor and window for rank-1 overlapping slices of a declared width.
Affine views are read from their base in place. Other views preserve their element order.
Sparse matrices¶
These builders convert between sparse and dense tensors. The sparse matrices guide shows them in use.
pack ¶
pack(value: Any, pattern: CanonicalPattern | SparsityPattern, *, shape: tuple[int, ...] | None = None) -> Expr
Build a sparse tensor from its stored values, including explicit zeros.
A SparsityPattern takes the values in its own coordinate order, a canonical pattern in
row-major order. Leading axes of value are batch axes. shape defaults to those axes
followed by the pattern's shape.
Source code in src/scaly/ir/expr.py
values ¶
Return a sparse tensor's stored values, in row-major coordinate order, as a dense tensor.
The result has the storage shape. A dense tensor is returned unchanged.
Source code in src/scaly/ir/expr.py
to_dense ¶
Return a dense tensor with zeros outside the pattern. A dense tensor is returned unchanged.
Source code in src/scaly/ir/expr.py
from_dense ¶
Keep the entries of a dense tensor inside pattern as a sparse tensor and drop the others.
Source code in src/scaly/ir/expr.py
sparsify ¶
Store only the entries that Expr.support says may be nonzero.
Batch axes share one pattern, covering the entries of every batch entry. A sparse tensor is returned unchanged.
Source code in src/scaly/ir/expr.py
PyTorch weights¶
load_torch_state_dict ¶
Load a simple PyTorch state_dict zip checkpoint without importing torch.
Supports the modern torch.save(state_dict, ...) zip layout: data.pkl
stores tensor metadata and data/<n> stores raw CPU storage bytes. This is
intentionally narrow. Unsupported pickle globals fail loudly instead of being
materialized.
Source code in src/scaly/utils/torch_state_dict.py
Verification and rewriting¶
verify_expr ¶
Topologically walk the DAG below root and raise on the first violation.
spec defaults to spec_expr, the full expression-dialect contract.
Source code in src/scaly/ir/expr_spec.py
Spec ¶
Bases: Generic[Node, Op]
A verifier: a set of Rules, indexed by op so checking a node touches only its own.
Rules with op=None apply to every node. Both dialects build their specs from this class.
Source code in src/scaly/ir/spec.py
Rule
dataclass
¶
Bases: Generic[Node, Op]
One verifier check: check returns an error message for a bad node, or None.
The rule applies to nodes whose op is op, or to every node when op is None.
Source code in src/scaly/ir/spec.py
VerifyError ¶
Bases: Exception
Raised by verify_expr and verify_program at the first node failing a rule.
The message names the node, its op and the rule it broke. Both dialects raise this one type.
Pattern
dataclass
¶
One rewrite: nodes with op op (any op when None) that satisfy predicate become replacement(node).
Source code in src/scaly/ir/match.py
PatternMatcher ¶
An op-indexed set of rewrite Patterns.
Indexing by op means a node is only tested against patterns that could match it, which is what keeps rewriting cheap as the pattern set grows.
Source code in src/scaly/ir/match.py
rewrite ¶
rewrite(root: Node, patterns: Iterable[Pattern[Node]] | PatternMatcher[Node], *, rebuild: Callable[[Node, tuple[Node, ...]], Node] | None = None, fixpoint: bool = True, revisit: bool = False, max_steps: int = 1000000, memo: dict[Node, Node] | None = None) -> Node
Apply patterns across a graph, bottom up, and return the rewritten root.
Iterative over an explicit stack, so depth in the graph never becomes depth on the Python
stack. Each node is visited once with its already-rewritten children memoized by identity, so
shared subgraphs stay shared. fixpoint retries the matcher on a node until nothing fires.
revisit instead walks into a replacement's subgraph so nested rewrites collapse in one
pass. max_steps bounds the total number of replacements. rebuild defaults to the
expression adapter rebuild_expr. Program adapters may reuse memo across roots with
identical patterns, rebuild, and options. Expression nodes do not support caller memoization
because their lowering hints depend on traversal provenance.
Source code in src/scaly/ir/match.py
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | |
simplify ¶
Apply algebraic identities and constant folding until the graph stops changing.
Covers the shared arithmetic identities of passes/arith.py (neutral elements, zero
annihilation, x - x, negation normalization, small constant powers), folding of all-constant
subgraphs, identity reshape, transpose and gather, slice of slice, slice of stack, A.T @ v as
v @ A (and v @ A.T as A @ v), and a matmul with an all-ones vector as sums.
Also folds elementwise and matrix products with no possible nonzero terms, and sums,
gathers, scatters, stacks and concatenations of known zeros.
Source code in src/scaly/passes/expr.py
cse ¶
cse_many ¶
Common-subexpression elimination across several outputs at once.
Sharing is found between the outputs as well as inside each, which is the point of asking
for several derivatives from one Function.factory call.
Source code in src/scaly/passes/expr.py
Text¶
format_expr ¶
Render a graph as a topological dump with stable %0, %1, ... names.
For the stable assembly form meant for diffs and tests, use render_expr_assembly.
Source code in src/scaly/ir/expr.py
render_expr_assembly ¶
render_expr_assembly(obj: Function | ConcreteFunction | Expr | Iterable[Expr], *, name: str | None = None) -> str
Render the expression Expr dialect as a compact SSA assembly listing.
When given a ConcreteFunction, the listing is a expr.module containing every transitive expression
callee body before the requested function. This mirrors what C rendering eventually needs.
Source code in src/scaly/ir/text.py
render_program_assembly ¶
Render Program IR as structured assembly with explicit control-flow constructs.