Files
SuperTensor/references/api.md
T
dela de917a2fbd Harden \stgroup and tighten the callout budget (review follow-up)
- Bracket ink is part of the fit: \st@facebody drops -inkw/-inke extreme
  coordinates and \stface/\ststack register them with the enclosing
  group/col/row fit, so a group outline can no longer be crossed by a
  member's bracket arms
- \stlink inside \stgroup or \stcol is now a package error: sub-flow
  members never terminate a pending connector, so the arrow was dropped
  silently while the label still rendered
- \stgroup requires role= (explicit role=neutral for mixed groups) and
  must bind at least two members or one \stcol partition; a lone stack
  or face inside a group is a dirty-build warning
- lint: default budget is one \stcallout per figure; the
  allow-multiple-callouts directive relaxes it to one per band
- build.sh: clean-build hint no longer names hue budget (lint owns it)
- tests/group-callout.tex reworked: multi-member group with a bracketed
  member as a regression probe, single callout; new negative fixtures
  group-link, group-norole, group-single, callout-budget
- api.md, checklist.md, style.md, layout.md, SKILL.md updated to match
2026-08-05 17:03:14 +08:00

12 KiB
Raw Blame History

supertensor.sty API

\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.

The build first runs scripts/lint.py. It rejects ledger changes, untracked absolute objects, repeated anonymous dimensions, raw TikZ rectangles, a formula placed before the last row, an unclosed \stgroup, a callout anchored to anything other than a band, more than one callout in the figure, and more than four active hue families per row. Intentional galleries/tests may put % supertensor-lint: allow-absolute, allow-missing-formula near the top; do not add an exemption to a deliverable merely to make it pass.

Ledgers

\stsetrole{q}{stTeal}      % role -> color. Macros take a ROLE, never a color.
\stdim{T}{6}               % symbolic axis -> physical edge length in cells
\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. Redeclaring an axis or role with the same value is harmless; changing its value emits a warning and keeps the original mapping.

Lengths: \stunit (one cell, 4.6 mm) and \sttilegap (white gutter, 0.5 mm).

Flow layout

This is the default. Leave the coordinate argument empty and the object is placed by a cursor. Hand-written offsets are the main source of layout bugs in these figures: every gap becomes a tuned magic number, so a label that grows by two characters silently lands on the next tensor, and two stages started from two different x share no rail.

\ststage{SA}{stage heading}      % on the left rail, below the previous band
\strow{rowA}{T}                  % open a band, declared height T
  \stface[role=q]{Q}{}{T}{d}     % empty coord = place at the cursor
  \stglyph{mA}{$\times$}         % operator glyph; reserves its own width
  \stface[role=w]{W}{}{d}{d}
  \stlink{lA}{softmax}           % connector whose LABEL is a flow object
  \stface[role=s]{S}{}{T}{d}
\strowend                        % fit the band, arm the caption lanes
\stcaption{Q}{$\mathbf Q$}{$T\times d$}
macro does
\ststage{name}{text} stage heading on the left rail, below all ink so far
\strow{name}{height} open a band; height is an axis name or an integer
\strowend fit the band into name, then \stlane it
\stcol{name}{height} … \stcolend vertical sub-flow filling one slot of the band
\stgroup[role=]{name} … \stgroupend outline naming the objects inside it as one composite
\stglyph{name}{$\times$} operator glyph (not \stop — plain TeX owns that name)
\stcomm{name}{All-Reduce} collective node
\stnode[style]{name}{text} any node, placed and measured by the cursor
\stlink{name}{label} connector; empty label reserves \stlinklen of bare arrow
\stgap{4mm} / \stvgap{4mm} one-off extra space, horizontal / vertical
\stbbox{all} everything drawn so far, as one node, for \stmeaningbox
\sttopformula{F}{math} the formula line, centered on what was actually drawn
\sttrack{node} fold a hand-placed node into the bbox and the vertical cursor
\stleftrail{x} / \stlayoutreset move the rail / start over

Gaps are declared once: \stgutter (6 mm, between objects in a band), \strowgap (3.5 mm, above a band), \stblockgap (9 mm, above a stage heading), \stlinklen (10 mm).

Four properties follow by construction, and each of them is a bug class removed:

  • The gap belongs to the object that follows it, and the first object in a band gets none — so every band starts flush on the same rail, and gap=0pt means exactly adjacent, which is how shards are made to tile their parent.
  • Every object reserves its own width, including a stack's offset sheets and a bracket's overhang. right=6mm of X reserves nothing, so the next face is free to land on top.
  • A connector's label is a flow object, so a label can never be wider than its arrow.
  • A band declares its height, so an object that does not fit is a build failure (Package supertensor Warning), not something the reader discovers.

Call \sttopformula after the bands. A formula placed first can only be centered on a figure whose width is not yet known — that is how the top line ends up visibly off-center.

\stcol is what a split along the contracted axis looks like:

\stcol{W2}{dff}                              % declared total height
  \stface[role=r1]{W2a}{}{dffl}{d}
  \stface[role=r2, gap=0pt]{W2b}{}{dffl}{d}  % tiles W2a exactly
\stcolend

A column that consumes a height other than the one it declares is drawn off-center, so that is a warning too.

Absolute placement still works everywhere — pass a coordinate instead of {}. Mix freely, but wrap hand-placed nodes in \sttrack so the cursor knows about them.

Groups

\stgroup … \stgroupend is the second sub-flow. It draws a thin rounded outline in the role hue around whatever is placed between them, naming those objects as one composite:

\stgroup[role=q]{qg}                   % keys: role= (REQUIRED, hue), pad= (default \stgrouppad)
  \stface[role=q]{q1}{}{T}{dh}         % "these three heads are q"
  \stface[role=q, gap=2mm]{q2}{}{T}{dh}
  \stface[role=q, gap=2mm]{q3}{}{T}{dh}
\stgroupend
\stcaption{qg}{$\mathbf q$}{$T\times h\times d_h$}

The members go inside the block, and three properties follow from that:

  • a group can only wrap adjacent objects — one that reached across the band would swallow whatever sat between its members;
  • the group, not its last member, terminates a pending \stlink and sources the next one, so an arrow lands on the outline instead of ending inside it and crossing a border that is not its endpoint. For the same reason \stlink inside a group is a package error: a member never terminates a connector, so the arrow would be dropped silently;
  • the padding is reserved on both sides, so the neighbour cannot land tangent to it. The fit includes decoration ink too: a bracket=true member's arms stay inside the outline.

A group must bind at least two members, or one \stcol partition — around a single face or stack the outline is decoration, and the package warns. role= is required; a genuinely mixed group passes role=neutral explicitly. \strowend fits the group, so \stcaption{qg}{...} hangs from the caption lane below the outline, not below the member. A group may contain a \stcol; it may not sit inside one, and it may not nest. An unclosed group is a lint error.

Callouts

\stcallout{name}{text width}{band}{title}{body} hangs a side note card off the right edge of a finished band, top-aligned with it. Call it after \strowend:

\strowend
\stcaption{g}{$\mathbf g$}{$B\times T\times h\times d_k$}
\stcallout{n1}{5.2cm}{rowB}{下界化换来了什么}{Kimi Linear 的 $g$ 无下界……}

Arg 3 must be a \strow band name, never a single face — a card beside one operand reads as a step in the computation (layout.md). The budget is one callout per figure: a card on every band is a dashboard, and the cards are unreadable at thumbnail size anyway. Both rules are lint errors; % supertensor-lint: allow-multiple-callouts relaxes the budget to one per band for a figure that genuinely needs it. Inside an open band a callout is a package error.

A callout pushes the vertical cursor below its own bottom edge. A card taller than its band therefore opens visible white space rather than colliding with the next stage — which is the signal that its text belongs in \stmeaningbox instead.

Faces

\stface[keys]{name}{}{rows}{cols}                    % flow
\stface[keys]{name}{(coord)}{rows}{cols}             % absolute
\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. A non-empty (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
gap= \stgutter flow only: space before this object. gap=0pt = exactly adjacent

\stindexface entries are row-major, rows*cols of them; . leaves a cell blank. It deliberately has no lightness ramp — see semantics.md. The package validates the entry count, pattern name, level range, and every pattern=data row's count, width and 0–3 domain; any mismatch makes build.sh fail.

\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

\strowend calls \stlane for you, so in flow mode captions just follow the band:

\strowend
\stcaption{A}{$\mathbf A$}{$T\times d$}     % symbol lane, shape lane
\stcaptiontop{A}{\stshapefont{token}}       % occasional label above a face

Arming the lane by hand (absolute placement):

\node[inner sep=0pt, fit=(A)(B)(C)] (rowA) {};
\stlane{rowA}
\stcaption{A}{$\mathbf A$}{$T\times d$}
\stnolane

\stcaption defines name-sym and name-shape nodes; anchor the next stage heading against name-shape.south.

Operators, connectors, nodes

Prefer \stglyph / \stcomm / \stlink (above). The raw forms are for absolute placement:

\node[st op, right=6mm of A] (m) {$\times$};   % reserves nothing -- see layout.md
\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

\stbbox{all}                    % flow: everything drawn so far
\stmeaningbox{mb}{16.6cm}{all}{axes text}{objects text}{mechanism text}
\stsignature{因果多头注意力(掩码 + 拼接投影)}{mb} % optional

Arg 2 is the total box width; arg 3 is the node it hangs below. \stbbox already contains every caption and top label; if you build the fit by hand, include them yourself or the box will overlap them. An empty {} row is dropped.

\stsignature renders only its subject; it has no author or handle mechanism.

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.
  • Neither the TikZ path parser nor the calc library expands a macro sitting where it expects (. Every cursor-computed coordinate therefore reaches the parser as literal text, via \edef ... \noexpand. Same class of trap: pgfmath cannot digest \stresolve's \ifcsname, so a ledger lookup must be pre-resolved with \edef before it is measured.
  • \sttopformula puts a group around its argument, which breaks TikZ's own \\. For more than one line, wrap the math in amsmath's gathered.