Skip to content

The shape algebra

Every tensor dimension in TensorCAD is a multivariate polynomial with exact rational coefficients over named symbols. Not an integer. This page argues why that is worth the machinery.

The problem with integers

Resolve D to 4096 and H to 32 early, and a shape is a tuple of numbers. Then two things go wrong.

A mismatch becomes uninformative. 4096 ≠ 4032 tells you two numbers differ. It does not tell you that one path computes H × dh and the other computes D, and that they diverged because dh was set independently.

And a shape can no longer mean anything at runtime. Batch size and sequence length are not known when you are drawing. Substitute a placeholder and every downstream check is about the placeholder.

What a polynomial buys

B and T stay indeterminate all the way through. The design symbols — D, H, dh, F — are substituted. Two shapes are compatible when their difference is the zero polynomial under that partial environment.

So B T (H dh) and B T D are the same shape when H × dh = D, and when they are not, the difference is H·dh − D — an expression naming exactly what disagrees. That is what the canvas shows you.

Division is an obligation, not a rounding

D / H succeeds only when H divides every term of D exactly. When it cannot be proven, the algebra refuses rather than rounding, and the refusal becomes a design-rule finding.

This is the check that catches D = 1024, H = 7 at the keystroke instead of at model.to(device).

Rationals, exactly

Coefficients are rationals, so 1.3 * 8/3 * D — how Llama writes a feed-forward width — stays 52/15 · D rather than drifting through binary floating point. ceil_mult(1.3*8/3*D, 1024) then folds to a number, because rounding functions need their arguments determined and design parameters always are.

One deliberate wart: rat() scales by ten until numerator and denominator are whole, so 1/3 becomes 333333333333/1000000000000. It is not exact. The Go port reproduces it exactly rather than fixing it, because a shape label that read differently in the two engines would be a migration bug you could only find by eye. It gets fixed in both at once, or not at all.

Why this makes scaleDesign possible

Because a parameter is stored as the expression the author wrote — not the number it evaluated to — halving D moves everything derived from it. The feed-forward width is still ceil_mult(1.3*8/3*D, 1024); it just evaluates to something else.

This is also why a numeric parameter field must never become a stepper or a slider. Doing so overwrites ceil_mult(1.3*8/3*D, 1024) with 14336 and destroys the design intent. Parameters are edited as text with the evaluated number shown beside them.

Where it lives

packages/core-go/shapes/symexpr.go is the polynomial; expr.go parses the expressions; pattern.ts parses shape patterns; infer.ts walks a graph propagating shapes and collecting the disagreements.