supertensor: shape-aware tensor figure toolkit

Extracted from the tensor-formula-viz skill and rebuilt around the idea that
the geometry rules should be enforced by construction rather than restated as
prose an agent has to remember.

- assets/supertensor.sty: faces, stacks, index faces, shared caption lanes,
  meaning box, signature. Macros take a declared axis and a declared role, so
  equal shapes get equal edges, a x a is square, a transpose swaps the face,
  and contracted axes share an edge length -- without any manual alignment.
- scripts/preflight.sh: decide the TikZ/CJK path before drawing.
- scripts/build.sh: compile and fail on silent corruption (missing CJK glyphs,
  overfull boxes, undeclared roles), then export pdf/svg/png/thumb.
- scripts/test.sh: build every figure as a regression test for the package.
- examples/: three golden figures (TP-FFN, causal MHA, MoE top-k gather) plus
  an anti-pattern gallery of figures that compile cleanly and still lie.
- SKILL.md + references/: lean entry point, details loaded on demand.
This commit is contained in:
dela
2026-08-05 12:17:33 +08:00
commit 7a22bef9e3
20 changed files with 1661 additions and 0 deletions
+55
View File
@@ -0,0 +1,55 @@
# Anti-patterns
Every figure below compiles cleanly. `build.sh` is happy with all of them. They are still
wrong, because the compiler checks TeX syntax and not whether the picture is true.
Render `examples/antipatterns.tex` and look at
`examples/build/antipatterns.png` once before your first figure.
## 1. The transpose that only changed its label
A face captioned `Kᵀ` that is still `T × d_h`. The reader looks for the contracted axis,
finds two faces of the same height, and concludes the contraction runs along the wrong
dimension. **Fix:** swap the arguments — `\ststack{KT}{...}{dh}{T}{3}`. See `geometry.md` §3.
## 2. Shards that do not tile their parent
Two shards drawn with a gap, or stretched to fill a parent whose other shards were elided.
Both assert a width that the tensor does not have. **Fix:** place each shard from the
previous one's edge (`($(W1a.east)+(2*\stunit,0)$)`), and draw an ellipsis for anything
omitted. See `geometry.md` §5–6.
## 3. An index drawn as a heatmap
Expert ids or token positions rendered with a lightness ramp. The ramp is a magnitude
channel, so it says `expert 3 > expert 0`, which is meaningless. **Fix:** `\stindexface`.
See `semantics.md`.
Same family: a Boolean mask drawn with graded cells (it has one level, not three), and a
score matrix drawn as flat blocks (it has magnitude, and hiding it wastes the figure).
## 4. One pale level everywhere
A whole tensor in `role!10`. At full size it looks tasteful; at thumbnail size — which is
how it will be seen on a slide — it is a blank rectangle. **Fix:** three separated levels,
`role!30 / role!55 / role!80`. Contrast comes from lightness, not saturation.
See `style.md`.
## Not in the gallery, but just as common
- **Periodic texture.** A polynomial hash reduced mod 3 repeats every 3 rows, and the eye
reads the resulting stripe as real structure. `pattern=dense` avoids it; if you write
your own filler, check that rows 1, 2, 4, 5 of a tall face are not identical.
- **A label wider than its connector.** The white underlay then covers the target tensor.
Shorten the label or widen the gap — never let it sit on a face. See `layout.md`.
- **A new hue for a regrouped view of the same data.** `X` and the per-expert buffers
gathered out of `X` are the same object in a different order; a second hue claims they
are different tensors.
- **Captions hanging at different depths** because the faces in a row have different
heights. Use `\stlane`.
- **A floating commentary card between two operands.** If it is not a real operation, it
belongs in the stage subtitle or the bottom box.
- **A meaning box that repeats the shapes.** The shapes are already under every block. The
box is for what the axes *mean* and what the operation *does*.
- **Solving crowding by shrinking type.** The type hierarchy is a hard floor; move the
stage to another row instead.
+103
View File
@@ -0,0 +1,103 @@
# supertensor.sty API
```tex
\documentclass[border=10pt]{standalone}
\usepackage[cjk]{supertensor} % cjk: ctex + fandol (XeLaTeX). en: English rail labels.
```
Build with `./scripts/build.sh fig.tex` — it puts `assets/` on `TEXINPUTS`, so the package
does not need to be installed into your texmf tree.
## Ledgers
```tex
\stsetrole{q}{stTeal} % role -> color. Macros take a ROLE, never a color.
\stdim{T}{6} % symbolic axis -> physical edge length in cells
\stsetauthor{...} % default 五道口纳什
\stsetlabels{A}{O}{M} % override the three meaning-box rail labels
\stsetrail{3.2em} % width of the bold label rail
```
Colors: `stTeal stOrange stCoral stViolet stGray stInk`. An unknown role falls back to gray
**and emits a package warning**, which `build.sh` turns into a failed build.
Lengths: `\stunit` (one cell, 4.6 mm) and `\sttilegap` (white gutter, 0.5 mm).
## Faces
```tex
\stface[keys]{name}{(coord)}{rows}{cols}
\ststack[keys]{name}{(coord)}{rows}{cols}{sheets}
\stindexface[keys]{name}{(coord)}{rows}{cols}{entries}
```
`rows`/`cols` accept a declared axis name or a raw integer. `(coord)` must include its own
parentheses — `{(0,0)}`, `{($(A.east)+(1.5,0)$)}`. `name` becomes a TikZ node you can
anchor against; `\ststack` also defines `name-front`.
Keys:
| key | default | meaning |
|---|---|---|
| `role=` | `neutral` | hue, via `\stsetrole` |
| `pattern=` | `dense` | `solid dense diag band lower upper causal empty data` |
| `data=` | — | with `pattern=data`: comma-separated rows, one digit per cell, `0`–`3` = level |
| `level=` | `2` | level for `pattern=solid` and the flat level of a mask |
| `bracket=` | `false` | thin neutral matrix brackets |
| `border=` | `true` | outer `black!60` border |
| `tiles=` | `true` | `false` = one flat filled rectangle |
`\stindexface` entries are row-major, `rows*cols` of them; `.` leaves a cell blank.
It deliberately has no lightness ramp — see `semantics.md`.
```tex
\stface[role=w, pattern=data, level=3,
data={3300,0330,0033,3003,3030,0303}]{D}{(0,0)}{T}{E}
\stindexface[role=idx]{I}{(b)}{T}{k}{0,1, 1,2, 2,3, 3,0, 0,2, 1,3}
```
## Captions
```tex
\node[inner sep=0pt, fit=(A)(B)(C)] (rowA) {};
\stlane{rowA}
\stcaption{A}{$\mathbf A$}{$T\times d$} % symbol lane, shape lane
\stnolane
\stcaptiontop{A}{\stshapefont{token}} % occasional label above a face
```
`\stcaption` defines `name-sym` and `name-shape` nodes; anchor the next stage heading
against `name-shape.south`.
## Operators, connectors, nodes
```tex
\node[st op, right=6mm of A] (m) {$\times$};
\node[st comm, right=9mm of P] (ar) {All-Reduce};
\starrow{P.east}{ar.west}
\starrowlabel{M.east}{A.west}{softmax}
```
Styles: `st sym st shape st stage st op st note st arrow st comm st brace`.
Text helpers: `\stformula \ststagelabel \stoperator \stsymfont \stshapefont \stprose`.
Connectors route on the background layer automatically.
## Bottom
```tex
\node[inner sep=0pt, fit=(F)(rowA)(rowB)(Y-shape)] (all) {};
\stmeaningbox{mb}{16.6cm}{all}{axes text}{objects text}{mechanism text}
\stsignature{因果多头注意力(掩码 + 拼接投影)}{mb}
```
Arg 2 is the total box width; arg 3 is the node it hangs below — include every caption and
top label in that `fit` or the box will overlap them. An empty `{}` row is dropped.
## Gotchas
- `\ststack` never re-enters `\stface`; if you extend the package, do not pass
`role=\st@role` back through pgfkeys — it defines the macro in terms of itself and hangs.
- A coordinate expression inside `fit=` needs braces: `fit={(a) ($(b)+(1,0)$)}`.
- `\foreach {\macro,...,1}` cannot infer its direction from an unexpanded macro.
- `\strole` is expandable on purpose (it is used inside `\edef`); the warning lives in
`\stcheckrole`.
+59
View File
@@ -0,0 +1,59 @@
# Pre-delivery checklist
A clean `build.sh` proves only that TeX was happy. Nothing below is checked by the compiler.
Work through it against the rendered PNG. Any mandatory violation means redraw, not patch.
## 1. Math (before looking at the picture)
- [ ] Every shape recomputed independently from the source formula or code.
- [ ] Block multiplication, broadcasting, reductions and sharding algebra verified.
- [ ] Axis identities that the figure asserts actually hold in the numbers drawn
(`d = h·d_h`, `Σ_e n_e = T·k`, shards summing to the parent).
## 2. Semantics ledger
- [ ] Every discrete or overloaded symbol has one type, domain and range.
- [ ] Score, index and mask are three distinct objects in three distinct grammars.
- [ ] Producer-to-consumer chain closed: scores → indices → gather/mask → values.
- [ ] Shared selectors drawn once, with the reuse axis marked.
## 3. Geometry ledger
- [ ] Equal shapes have identical faces everywhere.
- [ ] `a×a` is square; every transpose physically swaps the face.
- [ ] Both occurrences of each contracted axis have the same edge length.
- [ ] Shards tile their parent exactly; concat reverses split; elisions use an ellipsis.
## 4. Full-size visual audit
Open the PNG at 100 %.
- [ ] No forbidden intersection, tangency, clipping or occlusion — including stack offset
sheets, brackets, arrow labels and the meaning box.
- [ ] Every connector's white label underlay covers only its own connector.
- [ ] No connector crosses a box that is not its endpoint.
- [ ] Symbols and shapes sit on two flat lanes per row; stage headings share a left rail.
- [ ] Top zone compact (≤2 formula lines, no shape underbraces).
- [ ] Bottom box: one column, ≤3 rows, no overflow, no shrunken type.
- [ ] Signature outside the box, one line, names what the figure actually shows, not clipped
and not visually dominant.
- [ ] Structural support exact: known zeros unfilled, masks and diagonals exactly right.
## 5. Thumbnail audit
Open `*-thumb.png` (360 px).
- [ ] ≤4 active hue families per row plus gray; each role keeps one hue across stages.
- [ ] Base colors still muted, not saturated.
- [ ] Three visibly separated lightness levels where values vary; nothing is uniform pastel.
- [ ] Borders neutral, not tensor-colored.
- [ ] Dense texture reads as noise, not as stripes, checkerboard or a symmetric motif.
- [ ] The main structural claim of the figure is still legible at this size.
## 6. Delivery
- [ ] PNG preview shown.
- [ ] Short mechanism explanation in prose.
- [ ] `.tex` source and vector artifact (PDF/SVG) linked.
- [ ] Any degraded path stated explicitly — no CJK font, no `pdftocairo`, fallback renderer,
inferred shapes or conventions.
+77
View File
@@ -0,0 +1,77 @@
# Geometry invariants
A figure that is drawn to the wrong geometry is not a stylistic problem — it teaches the
reader a false fact about the computation. These invariants are mandatory. Most of them
are automatic if you declare the geometry ledger and never pass a raw number.
## The ledger
```tex
\stdim{T}{6} % sequence length -> 6 cells, everywhere in the figure
\stdim{d}{9} % model width -> 9 cells, everywhere
\stdim{dh}{3} % head width -> 3 cells, and 3*dh = d holds visually
```
`\stface{...}{T}{dh}` resolves the names through the ledger, so **one symbolic axis maps
to exactly one physical edge length across the whole figure**. Equal shapes therefore form
an equivalence class automatically: `Q` and `V` at `T×d_h` come out identical without you
lining anything up by hand.
Raw integers are accepted (`\stface{A}{(0,0)}{4}{4}`) but they opt out of the guarantee.
Use them only for a face whose axis appears nowhere else.
## The rules
1. **Face orientation.** A matrix face `a×b` is height `a`, width `b`. Always. For batched
or stacked tensors, the *last two* axes make the face; leading axes become depth
(`\ststack`) or repeated panels — never a wider rectangle.
2. **Squares.** `a×a` renders as a square. Automatic when both arguments resolve to the
same declared axis.
3. **Transpose.** Draw `K^T` by physically swapping height and width:
`\ststack{KT}{...}{dh}{T}{3}` against `\ststack{K}{...}{T}{dh}{3}`. Relabelling a face
`K^T` while leaving its shape alone is invalid — it is the single most common lie in
attention figures.
4. **Contraction.** In `(m×k)(k×n)`, both occurrences of `k` get the same edge length.
With the ledger this is free: pass the same axis name to the width of the left face and
the height of the right one. The same applies to `einsum` and attention axes.
5. **Partition.** Explicit shards tile their parent exactly along the split axis, equal
shards are equal in size, and concatenation reverses the split. Place shards from the
previous face's edge so no gap can creep in:
```tex
\stface[role=r1]{W1a}{(...)}{d}{dffl}
\stface[role=r2]{W1b}{($(W1a.east)+(2*\stunit,0)$)}{d}{dffl}
```
The offset is `half-width of the next face` in `\stunit`, so the two faces are exactly
adjacent. Size concatenated parts from their *declared* shapes — Q/K/V are equal
segments only when their output shapes are equal.
6. **Elision.** If intermediate shards are omitted, draw an ellipsis. Never stretch the
visible shards to impersonate the full parent.
7. **Axis changes.** Geometry may change only at an explicit reshape, flatten, transpose,
split, or concat operator, and the axis identity must be stated — e.g. `(h/p)·d_h = d/p`.
Do not silently reuse one generic rectangle on both sides of an axis change.
8. **Illustrative counts.** Cell counts need not equal real dimensions. Choosing `d=9`
to stand for 4096 is fine. It never waives rules 1–7: the *ratios* you draw are read as
facts. If `d = h·d_h`, pick numbers where that arithmetic actually holds.
## Sharded matmul
When a matmul is sharded, expand the block algebra in the top formula as well as in the
middle row, otherwise the reader cannot check the figure:
```
XW = [XW^(1) | ... | XW^(p)] = [H^(1) | ... | H^(p)]
[H^(1) | ... | H^(p)] [W^(1); ...; W^(p)] = Σ_r H^(r) W^(r) = Σ_r P^(r)
```
Column sharding splits the *output* axis (shards sit side by side); row sharding splits the
*contracted* axis (shards stack vertically and require a reduction). `examples/tp-ffn-allreduce.tex`
draws both in one figure.
## Distinguish global from local
Per-rank shapes and global shapes are different objects. Label them differently
(`d_ff/p` vs `d_ff`) and, when both appear, say in the **Axes** row which one the figure
is drawing.
+89
View File
@@ -0,0 +1,89 @@
# Layout invariants
Everything on the canvas is a bounding box: tensors, full offset stacks, brackets,
operators, arrow labels, annotations, symbols, shape labels, stage headings, the meaning
box, the signature. **Tangency counts as collision.**
## Gutters
Define one base gutter `g ≥ 1 em`. Unrelated boxes stay at least `g` apart; stage bands at
least `1.5g`. In practice: `right=5mm–9mm` between an operator and its operands, `7mm–9mm`
between the last caption of one row and the next stage heading.
Overlap is allowed only inside one declared composite:
- tiles inside their own face,
- shards tiling a parent,
- outline sheets in one `\ststack`,
- a bracket around its own tensor,
- a connector endpoint touching its source/target border.
Every other intersection or occlusion is forbidden.
## Lanes
Reserve separate vertical lanes and never put anything else in them:
```
stage heading
(connector annotations)
tensor / operator row
symbols <- \stcaption arg 2
shapes <- \stcaption arg 3
```
Faces of different heights would otherwise hang their captions at different depths. Fix it
with a shared baseline:
```tex
\node[inner sep=0pt, fit=(Q)(KT)(S)] (rowA) {};
\stlane{rowA}
\stcaption{Q}{$\mathbf Q^{(i)}$}{$h\times T\times d_h$}
...
\stnolane
```
Every `\stcaption` between `\stlane` and `\stnolane` hangs from the bottom of `rowA`, so
symbols and shapes form two flat lanes.
Stage headings share one left rail. Anchor each heading below the previous row but at the
previous *heading's* x, not at the previous row's content:
```tex
\coordinate (cy) at ($(I-shape.south)+(0,-9mm)$);
\node[st stage, anchor=north west] (SC) at (SB.west |- cy) {...};
```
Explanatory prose belongs in the stage subtitle, the bottom box, or above its own
connector. Never drop a floating commentary card between two operands unless it is a real
operation node (`st comm`).
## Layers
The package declares three: `stbg` (connectors), `main` (tensors, operators), `stfg`
(text). `\starrow` and `\starrowlabel` route on `stbg` automatically, so a connector can
never cover a face. Two consequences you still own:
- A connector may not cross a box that is not one of its endpoints. Move the row, don't
route over.
- A label's white underlay may cover only its own connector — never a tensor, never
another label. If the label is wider than the arrow, shorten the label or widen the gap.
This is the single most common failure after a first draft.
## Stacks
`\ststack` includes its offset sheets in the bounding box, so neighbours can be spaced
against the real extent. Back sheets are outline-only and must carry no semantic content
of their own. If individual slices need to be read, use separate panels instead of overlap.
## When it does not fit
In this order:
1. shorten or remove secondary annotation,
2. widen the natural crop,
3. increase row spacing,
4. move the whole stage to another row.
Never solve crowding by shrinking below the type hierarchy, closing the gutter, or covering
another object.
+67
View File
@@ -0,0 +1,67 @@
# Symbol semantics
Shape is not meaning. Two tensors of shape `T×k` can be a score matrix, a list of token
positions, or a Boolean support, and drawing all three the same way is the fastest way to
mislead a reader who is trying to follow the mechanism.
## Classify every non-obvious symbol
For each symbol record: **semantic kind**, **dtype/domain**, **what one entry means**, and
its **range** when meaningful. Kinds worth separating:
| kind | example | domain |
|---|---|---|
| value / activation | `X`, `H` | ℝ |
| score / logit | `S = QKᵀ/√d_h` | ℝ |
| probability | `A = softmax(S)` | [0,1], rows sum to 1 |
| index / coordinate | `I = TopKIndices(G)` | {0,…,E−1} |
| rank / order | selected-slot axis `r` | {1,…,k} |
| count | `n_e` tokens per expert | ℕ |
| id | token id, device id | opaque |
| mask / support | `D`, causal `M` | {0,1} or {0,−∞} |
| permutation | gather order | bijection |
| shape parameter | `p`, `h` | ℕ, not drawn as a tensor |
## One block, one object
Never merge a score, an index list and a mask under a label like `M/S`. Each conversion
gets an explicit operator and arrow. For selection or routing, close the entire chain:
```
continuous scores → discrete indices/ids → gather / scatter / mask / route → selected values
```
`TopKValues` and `TopKIndices` are different tensors; if both are used, show both.
## Three grammars, deliberately different
| object | grammar | package |
|---|---|---|
| value / score / probability | magnitude — three separated lightness levels | `\stface[pattern=dense]` |
| index / id | discrete symbols in outlined cells, **no** lightness ramp | `\stindexface{...}{entries}` |
| mask / support | one flat level, exact structure, zeros unfilled | `\stface[pattern=causal, level=3]` or `pattern=data` |
`examples/moe-topk-gather.tex` puts all three in one figure on purpose. The reason indices
get no ramp: a ramp invites the reader to compare `expert 3 > expert 0` as if the number
were a size.
## Notation duties
- Define index notation and range at first use:
`S_t = (s_{t,1},…,s_{t,k})`, `s_{t,r} ∈ {0,…,t}`.
- Distinguish the *source-position* axis `s` from the *selected-slot* axis `r`, and say
whether ordering, duplicates, padding or variable cardinality matter.
- Show the address mapping once: `G[b,t,r,:] = X[b, S[b,t,r], :]`.
- If a mask is shown alongside the index tensor, state `M[b,t,s] = 1[s ∈ S_{b,t}]` — do not
let the figure imply they are the same object.
- When one selector is shared across heads, ranks or branches, draw it **once** and mark
the broadcast/reuse axis. A per-head copy of a shared mask is a false claim about memory
and about the computation (`examples/mha-causal.tex`: `M` is a single face while `A` is a
three-sheet stack).
## Colors carry semantics too
One tensor role keeps one hue for the whole figure — that is what `\stsetrole` is for. A
gathered, resharded or regrouped view of the same data keeps the *same* role color; a new
hue means a new object. Derived tensors may reuse their parent's family rather than
spending a hue (`V → O → Y` in the MHA example are all violet).
+94
View File
@@ -0,0 +1,94 @@
# House style
The package ships these defaults; this file explains what you must still decide and what
you must not undo.
## Canvas
White background, natural `standalone` crop. No forced 16:9. No title by default. The top
holds at most two compact formula lines: the primary chain and, only if essential, one
companion definition.
Each stage is one horizontal algebraic row with a shared visual baseline. Use the fewest
stages that preserve the primary path. No unrelated branches, no dashboard panels.
## Type hierarchy
| element | size | macro / style |
|---|---|---|
| formula | `\large` | `\stformula` |
| stage label | `\small\bfseries`, muted | `st stage` |
| operator | `\Large` | `st op` |
| symbol | `\small` | `\stcaption` arg 2 |
| shape | `\scriptsize`, muted | `\stcaption` arg 3 |
| bottom prose | `\small` | `\stmeaningbox` |
| signature | `\scriptsize`, low contrast | `\stsignature` |
Never shrink below this to make something fit — see `layout.md`.
## Faces
Separate tiles with a small white gutter and 0.5–1 pt corner rounding (`\st@tile` does
this). Thin neutral brackets, `black!55`–`black!70` outer borders. No saturated
tensor-colored outlines, no continuous spreadsheet grid.
**Encode support before magnitude.** Every known zero stays white/unfilled; every shown
nonzero gets color. A diagonal matrix must read instantly as colored diagonal cells on a
white field. `pattern=diag/band/lower/upper/causal/data` fill exactly the structural
support.
## Color
Palette (already defined): `stTeal #4F8FA5`, `stOrange #EE995B`, `stCoral #C95B5B`,
`stViolet #8A74B5`, `stGray #85898F`. Muted, mid-chroma, paper-like. Do not add saturated
primaries.
- One semantic color per tensor role, held across every stage: `\stsetrole{q}{stTeal}`.
Macros take a **role**, never a color.
- At most four active hue families per algebraic row, plus neutral gray. Vary lightness or
reuse the related input/output family before spending a new hue.
- If sign matters: hue for sign, intensity for magnitude.
- Contrast comes from **lightness separation, not saturation**. Levels are `role!30`,
`role!55`, `role!80` (`\stlevelpct`); white is reserved for zero/absence. Do not render
a whole tensor in `role!5`–`role!15` pastel.
- Illustrative dense tensors use two or three **non-periodic** levels. `pattern=dense`
composes coprime moduli to avoid this; a polynomial hash mod 3 makes rows 1, 2, 4, 5
identical and the eye reads that stripe as structure in the data. No checkerboards, no
regular stripes, no symmetric motifs unless they encode real structure.
## Bottom box
One full-width, low-contrast box, one reading column, three fixed-label rows:
- **Axes** — what each dimension means.
- **Objects** — semantic kind / domain / range (see `semantics.md`).
- **Mechanism** — at most two essential mappings, contractions, broadcasts or boundaries.
Narrow bold label rail, left-aligned ragged-right `\small` content, 8–10 pt inner padding,
0.4–0.6 em row gaps. Rail labels localize with the package option (`zh` default, `en`).
Keep each row compact: prefer symbol semantics over numeric configuration. When it is too
long, **remove content** — never add cards, columns or smaller type. Pass `{}` to omit a row.
## Signature
One centered line below the box, outside it, low-contrast gray, `\scriptsize` or smaller:
`\stsignature{<subject>}{<fit node>}` renders `<subject>@五道口纳什`. The subject must name
what this figure actually visualizes. Keep it on one line, with a small but visible gap.
## Never
Charts or metric insets not present in the primary formula. Decorative pills, banners,
shadows, repeated separators, explanatory cards.
## Reference image
`assets/kimi-matrix-style-reference.png` — inspect it with an image viewer before drawing
when entry-level matrix blocks are central or Kimi-like styling is requested. Use it only
to calibrate tile spacing, rounding, neutral brackets, restrained hue, lightness separation
and structural whitespace. Do not copy its content, and do not embed it in the output.
## Language
Chinese figures use concise Chinese labels with standard English terms where helpful
(`softmax`, `All-Reduce`, `gather` stay in English). Switch the whole figure at once —
`\usepackage[en]{supertensor}` plus English stage headings and box text — never mix.