diff --git a/README.md b/README.md index f270bce..0913726 100644 --- a/README.md +++ b/README.md @@ -37,14 +37,14 @@ A minimal figure: \stdim{d}{4} \begin{document}\begin{tikzpicture} - \stface[role=act, bracket=true]{X}{(0,0)}{T}{d} - \node[st op, right=6mm of X] (m) {$\times$}; - \stface[role=w]{W}{($(m)+(1.4,0)$)}{d}{d} - \node[inner sep=0pt, fit=(X)(W)] (row) {}; - \stlane{row} + \ststage{S1}{one band, placed by cursor} + \strow{row}{T} % band height, declared once + \stface[role=act, bracket=true]{X}{}{T}{d} % empty coord = at the cursor + \stglyph{m}{$\times$} + \stface[role=w]{W}{}{d}{d} + \strowend \stcaption{X}{$\mathbf X$}{$T\times d$} \stcaption{W}{$\mathbf W$}{$d\times d$} - \stnolane \end{tikzpicture}\end{document} ``` @@ -52,13 +52,19 @@ Because `T` and `d` come from the ledger, the contracted axis is automatically o length in both operands, `d×d` is automatically square, and any other face of shape `T×d` in the figure is automatically identical to `X`. +Because the coordinates are empty, each object reserves its own width and the gap between +them is `\stgutter`, declared once. Nothing here is a tuned offset, so growing a label can +only push its neighbours apart — it can never land on top of one. And the band declares its +height, so an object that does not fit is a failed build rather than something the reader +discovers. + See `references/api.md` for the full macro list. ## Examples | file | shows | |---|---| -| `tp-ffn-allreduce.tex` | column-then-row sharding, exact tiling, one hue per TP rank, a collective as a real node | +| `tp-ffn-allreduce.tex` | column-then-row sharding, exact tiling, one hue per TP rank, a collective as a real node, a `\stcol` split along the contracted axis | | `mha-causal.tex` | leading axes as stack depth, a physically swapped `Kᵀ`, a mask in a different grammar from the scores it gates | | `moe-topk-gather.tex` | scores → indices → Boolean support → gather, with all three cell grammars side by side | | `antipatterns.tex` | four figures that compile cleanly and still teach something false | @@ -77,6 +83,10 @@ Two LaTeX warnings produce a figure that is quietly wrong rather than visibly br greps for both and exits non-zero. A `Package supertensor Warning` — an undeclared role falling back to gray — is treated the same way. +The flow layout adds two of its own: an object that overflows its band, and a `\stcol` +whose contents do not add up to the height it declared (which means it is drawn +off-center). Both are things a reader would have to notice for you. + A clean build still proves nothing about collisions, hue budget or whether the math is right. That is what `references/checklist.md` is for. diff --git a/SKILL.md b/SKILL.md index 7ffbd70..af6df73 100644 --- a/SKILL.md +++ b/SKILL.md @@ -29,8 +29,10 @@ A figure built from raw TikZ has to re-earn every invariant by hand and usually - *geometry* — one `\stdim{axis}{cells}` per symbolic axis, one `\stsetrole{role}{color}` per tensor role. Declaring these makes the invariants automatic. See `references/geometry.md`. -4. **Pick the smallest grammar** that exposes the mechanism (see below), then reserve - stage lanes and draw. See `references/layout.md` and `references/api.md`. +4. **Pick the smallest grammar** that exposes the mechanism (see below), then draw with the + flow layout — `\ststage` / `\strow` … `\strowend`, empty coordinate arguments, gaps + declared once. Reach for an absolute coordinate only when no band can express the + placement. See `references/api.md` and `references/layout.md`. 5. **Build and audit.** `./scripts/build.sh fig.tex`. A clean build only proves TeX was happy; then run the visual audit in `references/checklist.md` against the PNG at full size and at thumbnail size. Redraw on any mandatory-invariant violation. @@ -47,7 +49,8 @@ shape or convention you inferred. | sharding, device ownership, channel groups | adjacent faces tiling a parent | two `\stface` calls, one role each | | leading axes (`B`, `h`) | depth | `\ststack{...}{sheets}` | | discrete choices (indices, token ids, expert ids) | symbols in cells, no ramp | `\stindexface` | -| data movement, collectives, non-linear ops | arrows and nodes | `\starrow`, `\starrowlabel`, `st comm` | +| data movement, collectives, non-linear ops | arrows and nodes | `\stlink`, `\stcomm` | +| a split along the contracted axis | a vertical pair inside one band slot | `\stcol` … `\stcolend` | Combine grammars only when each one adds information. Known zeros stay unfilled; masks, diagonals, sparsity and partitions must encode their exact structure. @@ -65,7 +68,8 @@ reference file with the full statement and the failure it prevents. from continuous score to discrete index to gathered value. - **Layout** (`references/layout.md`) — everything is a bounding box; tangency counts as collision. Reserved lanes for stage heading / tensors / symbols / shapes. Connectors on - the background layer, text on the foreground layer. + the background layer, text on the foreground layer. Place by cursor, not by hand-tuned + coordinate: a magic-number offset is only valid for the content it was tuned against. - **Style** (`references/style.md`) — muted palette, one hue per role, ≤4 active hues per row plus gray, three separated lightness levels, non-periodic texture, no decoration. diff --git a/assets/supertensor.sty b/assets/supertensor.sty index 1f03805..4e4e7bd 100644 --- a/assets/supertensor.sty +++ b/assets/supertensor.sty @@ -92,6 +92,247 @@ line width=0.4pt}, } +% --------------------------------------------------------- flow layout ------ +% Hand-placed absolute coordinates are the main source of layout bugs in these +% figures. Every gap ends up 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. The cursor below removes both by construction: +% gaps are declared once, positions are derived from the objects actually +% drawn. Absolute placement still works -- pass a coordinate instead of `{}'. +% +% \ststage{S1}{stage heading} % anchored on the left rail +% \strow{R1}{T} % band of height T units +% \stface[role=q]{Q}{}{T}{d} % empty coord = place at the cursor +% \stglyph{m}{$\times$} +% \stface[role=w]{W}{}{d}{d} +% \stcol{C}{2*d units} % a vertical pair in one band slot +% \stface[role=r1]{Ca}{}{d}{d} +% \stface[role=r2, gap=0pt]{Cb}{}{d}{d} +% \stcolend +% \strowend % fits the band, arms \stlane +\newlength{\stgutter}\setlength{\stgutter}{6mm} % between objects in a band +\newlength{\strowgap}\setlength{\strowgap}{3.5mm} % above a band +\newlength{\stblockgap}\setlength{\stblockgap}{9mm} % above a stage heading +\newlength{\stlinklen}\setlength{\stlinklen}{10mm} % bare connector reservation +\newlength{\st@railx}\newlength{\st@cx}\newlength{\st@cy} +\newlength{\st@ycur}\newlength{\st@bandh}\newlength{\st@tmpx}\newlength{\st@tmpy} +% Column sub-flow state. Named st@v* rather than st@col*: \st@cols and \st@col +% are already the column count and the resolved hue of a face. +\newlength{\st@vx}\newlength{\st@vy}\newlength{\st@vw}\newlength{\st@vtop} +\newlength{\st@vh} +\newif\ifst@inrow +\newif\ifst@incol +\newif\ifst@first + +\newcommand{\stlayoutreset}{% + \global\st@railx=0pt \global\st@ycur=0pt + \global\st@cx=0pt \global\st@cy=0pt \global\st@bandh=0pt + \global\st@inrowfalse \global\st@incolfalse + \gdef\st@rowlist{}\gdef\st@alllist{}\gdef\st@rowname{}% + \gdef\st@collist{}\gdef\st@colname{}% + \gdef\st@lastnode{}\gdef\st@linklabel{}\gdef\st@linkprev{}} +\stlayoutreset +\newcommand{\stleftrail}[1]{\global\st@railx=\dimexpr#1\relax} +\newcommand{\stvgap}[1]{\global\advance\st@ycur by -\dimexpr#1\relax} + +% The vertical cursor tracks the lowest ink so far, so the next band never has +% to be positioned by eye. +\newcommand{\st@lower}[1]{% + \pgfextracty{\st@tmpy}{\pgfpointanchor{#1}{south}}% + \ifdim\st@tmpy<\st@ycur \global\st@ycur=\st@tmpy \fi} +\newcommand{\st@regall}[1]{\xdef\st@alllist{\st@alllist(#1)}} +% Inside a column the items belong to the column, not to the band: they must not +% terminate a pending connector, or the arrow would land on the first sheet of +% the stack instead of on the column as a whole. +\newcommand{\st@regrow}[1]{% + \ifst@incol + \xdef\st@collist{\st@collist(#1)}\st@regall{#1}% + \else + \xdef\st@rowlist{\st@rowlist(#1)}\st@regall{#1}% + \st@drawpendinglink{#1}% + \gdef\st@lastnode{#1}% + \fi} + +\newcommand{\st@needrow}[1]{% + \ifst@inrow\else + \PackageError{supertensor}{\string#1\space needs an open \string\strow}% + {Flow placement only works between \string\strow\space and + \string\strowend. Pass an explicit coordinate instead.}% + \fi} + +% The gap belongs to the object that follows it, and the first object in a band +% gets none -- that is what puts every band's left edge on the same rail. +\newcommand{\st@leadgap}[1]{% + \ifst@first + \global\st@firstfalse + \else + \ifst@incol\global\advance\st@vy by -\dimexpr#1\relax + \else\global\advance\st@cx by \dimexpr#1\relax\fi + \fi} + +% Reserve #1 of horizontal space, hand back the center coordinate for it. +\newcommand{\st@flow}[1]{% + \dimen0=\dimexpr#1\relax + \dimen2=\st@cx \advance\dimen2 by 0.5\dimen0 + \edef\st@pos{(\the\dimen2,\the\st@cy)}% + \global\advance\st@cx by \dimen0} + +% Reserve #2 of vertical space inside a column, hand back the center for it. +% Items are centered on the column's own axis, so a narrow shard sits under a +% wide one without an eyeballed offset. +\newcommand{\st@vflow}[2]{% + \dimen0=\dimexpr#1\relax \dimen4=\dimexpr#2\relax + \dimen2=\st@vx \advance\dimen2 by 0.5\dimen0 + \dimen6=\st@vy \advance\dimen6 by -0.5\dimen4 + \edef\st@pos{(\the\dimen2,\the\dimen6)}% + \global\advance\st@vy by -\dimen4 + \ifdim\dimen0>\st@vw \global\st@vw=\dimen0 \fi} + +% A band declares its height, so an object that does not fit is a build +% failure rather than something the reader discovers. +\newcommand{\st@checkh}[2]{% + \dimen0=\dimexpr#2\relax \advance\dimen0 by -\st@bandh + \ifdim\dimen0>4mm + \PackageWarning{supertensor}{Object `#1' overflows its band by + \the\dimen0. Raise the height declared in \string\strow\space or give it + its own band}% + \fi} + +\newcommand{\ststage}[2]{% + \dimen0=\st@ycur \advance\dimen0 by -\stblockgap + \node[st stage, anchor=north west] (#1) at (\the\st@railx,\the\dimen0) {#2}; + \st@regall{#1}\st@lower{#1}} + +% \strow{name}{height in axis units or a declared axis} +\newcommand{\strow}[2]{% + \gdef\st@rowlist{}\gdef\st@rowname{#1}\gdef\st@lastnode{}% + \gdef\st@linklabel{}\gdef\st@linkprev{}% + \global\st@inrowtrue\global\st@firsttrue + % Resolve through the ledger first: pgfmath's parser cannot digest + % \stresolve's \ifcsname on its own. + \edef\st@bandu{\stresolve{#2}}% + \pgfmathsetlengthmacro{\st@bh}{\st@bandu*\stunit}% + \global\st@bandh=\st@bh + \global\st@cx=\st@railx + \dimen0=\st@ycur \advance\dimen0 by -\strowgap \advance\dimen0 by -0.5\st@bandh + \global\st@cy=\dimen0} + +\newcommand{\strowend}{% + \ifdefempty{\st@rowlist}% + {\PackageWarning{supertensor}{Empty \string\strow\space `\st@rowname'}}% + {\edef\st@do{\noexpand\node[inner sep=0pt, outer sep=0pt, + fit={\st@rowlist}] (\st@rowname) {};}\st@do + \stlane{\st@rowname}\st@regall{\st@rowname}\st@lower{\st@rowname}}% + \global\st@inrowfalse} + +% \stcol{name}{height} ... \stcolend -- a vertical sub-flow occupying one slot +% of the band. This is what a split along a shared axis looks like: two shards +% stacked with gap=0pt tile their parent exactly, which no pair of hand-tuned +% y offsets can guarantee. +\newcommand{\stcol}[2]{% + \st@needrow{\stcol}% + \ifst@incol + \PackageError{supertensor}{Nested \string\stcol}% + {Close the open column with \string\stcolend\space first.}% + \fi + \st@leadgap{\stgutter}% + \gdef\st@collist{}\gdef\st@colname{#1}% + \global\st@vx=\st@cx \global\st@vw=0pt + \edef\st@colu{\stresolve{#2}}% + \pgfmathsetlengthmacro{\st@ch}{\st@colu*\stunit}% + \global\st@vh=\st@ch + \dimen0=\st@cy \advance\dimen0 by 0.5\st@vh + \global\st@vtop=\dimen0 \global\st@vy=\dimen0 + \global\st@incoltrue\global\st@firsttrue} + +\newcommand{\stcolend}{% + \ifdefempty{\st@collist}% + {\PackageWarning{supertensor}{Empty \string\stcol\space `\st@colname'}% + \global\st@incolfalse}% + {\dimen0=\st@vtop \advance\dimen0 by -\st@vy % height actually consumed + \global\st@incolfalse + \edef\st@do{\noexpand\node[inner sep=0pt, outer sep=0pt, + fit={\st@collist}] (\st@colname) {};}\st@do + \global\advance\st@cx by \st@vw + \global\st@firstfalse + % The column is centered on the band using its DECLARED height, so a + % mismatch is not just an overflow -- it is a column drawn off-center. + \dimen2=\dimen0 \advance\dimen2 by -\st@vh + \ifdim\dimen2<0pt \dimen2=-\dimen2 \fi + \ifdim\dimen2>2mm + \PackageWarning{supertensor}{Column `\st@colname' consumes + \the\dimen0\space but declares \the\st@vh; it is drawn off-center. + Fix the height in \string\stcol}% + \fi + \st@checkh{\st@colname}{\the\dimen0}% + \st@regrow{\st@colname}}} + +% A node in the flow: an operator glyph, a collective, a note. It reserves its +% own width, which is the whole point -- `right=6mm of X' reserves nothing, so +% the next face is free to land on top of it. +\newcommand{\stnode}[3][]{% + \st@needrow{\stnode}\st@leadgap{\stgutter}% + \ifst@incol + \node[#1, anchor=north west] (#2) at (\the\st@vx,\the\st@vy) {#3}; + \pgfextractx{\st@tmpx}{\pgfpointanchor{#2}{east}}% + \advance\st@tmpx by -\st@vx + \ifdim\st@tmpx>\st@vw \global\st@vw=\st@tmpx \fi + \pgfextracty{\st@tmpy}{\pgfpointanchor{#2}{south}}% + \global\st@vy=\st@tmpy + \else + \node[#1, anchor=west] (#2) at (\the\st@cx,\the\st@cy) {#3}; + \pgfextractx{\st@tmpx}{\pgfpointanchor{#2}{east}}% + \global\st@cx=\st@tmpx + \fi + \st@regrow{#2}} +% Not \stop: that name is already taken by plain TeX. +\newcommand{\stglyph}[2]{\stnode[st op]{#1}{#2}} +\newcommand{\stcomm}[2]{\stnode[st comm]{#1}{#2}} +\newcommand{\stgap}[1]{\global\advance\st@cx by \dimexpr#1\relax} + +% \stlink{name}{label} -- a connector whose LABEL is a flow object. The label +% reserves its own width and the arrows are drawn to the neighbours once the +% right-hand one exists, so a label can never be wider than its connector. +\newcommand{\stlink}[2]{% + \st@needrow{\stlink}% + \xdef\st@linkprev{\st@lastnode}% + \ifblank{#2}% + {\gdef\st@linklabel{}\stgap{\stlinklen}}% + {\st@leadgap{\stgutter}% + \node[st note, anchor=west, inner sep=1pt] (#1) at (\the\st@cx,\the\st@cy) {#2}; + \pgfextractx{\st@tmpx}{\pgfpointanchor{#1}{east}}% + \global\st@cx=\st@tmpx + \gdef\st@linklabel{#1}\st@regall{#1}}} +\newcommand{\st@drawpendinglink}[1]{% + \ifdefempty{\st@linkprev}{}{% + \begin{pgfonlayer}{stbg} + \ifdefempty{\st@linklabel}% + {\draw[st arrow] (\st@linkprev.east) -- (#1.west);}% + {\draw[st arrow] (\st@linkprev.east) -- (\st@linklabel.west); + \draw[st arrow] (\st@linklabel.east) -- (#1.west);}% + \end{pgfonlayer} + \gdef\st@linkprev{}\gdef\st@linklabel{}}} + +% Everything drawn through the flow, as one node -- for the meaning box anchor. +\newcommand{\stbbox}[1]{% + \edef\st@do{\noexpand\node[inner sep=0pt, outer sep=0pt, + fit={\st@alllist}] (#1) {};}\st@do} + +% Escape hatch: fold a hand-placed node into the bounding box and push the +% vertical cursor below it, so the next band still knows where the ink ends. +\newcommand{\sttrack}[1]{\st@regall{#1}\st@lower{#1}} + +% \sttopformula{name}{math} -- the formula line, centered on everything drawn +% so far. Call it AFTER the bands: a formula placed first can only be centered +% on a figure whose width is not known yet, which is how the top line ends up +% visibly off-center. +\newcommand{\sttopformula}[2]{% + \stbbox{st@bbt}% + % For more than one line, wrap the math in amsmath's `gathered': TikZ's own + % \\ handling does not survive the group \stformula puts around the content. + \node[above=6mm of st@bbt, anchor=south] (#1) {\stformula{#2}}; + \st@regall{#1}} + % ------------------------------------------------------------- faces -------- \newif\ifst@bracket \newif\ifst@border @@ -102,10 +343,13 @@ pattern/.store in=\st@pattern, data/.store in=\st@data, level/.store in=\st@level, + gap/.store in=\st@gap, bracket/.is if=st@bracket, border/.is if=st@border, tiles/.is if=st@tiles, - role=neutral, pattern=dense, data={}, level=2, + % gap= is the space BEFORE this object, replacing the standing gutter. + % gap=0pt is how shards are made to tile their parent exactly. + role=neutral, pattern=dense, data={}, level=2, gap=\stgutter, bracket=false, border=true, tiles=true, } @@ -153,17 +397,36 @@ % `role=\st@role' back through pgfkeys would define \st@role in terms of % itself and hang the run. \newcommand{\st@facecore}[2]{% - \node[inner sep=0pt, outer sep=0pt, minimum width=\st@w, minimum height=\st@h] - (#1) at #2 {}; + \st@basenode{#1}{#2}% \st@facebody{#1}} +% Shared entry for cursor placement: reserve the width, check the band. +\newcommand{\st@flowbegin}[3]{% + \st@needrow{\stface}\st@leadgap{\st@gap}% + \ifst@incol \st@vflow{#2}{#3}\else \st@flow{#2}\st@checkh{#1}{#3}\fi} + +% Neither the TikZ path parser nor calc expands a macro sitting where it +% expects `('. Everything the cursor computes therefore goes through \edef and +% reaches the parser as literal text. +\newcommand{\st@coordat}[2]{\coordinate (#1) at #2;} +\newcommand{\st@basenode}[2]{% + \node[inner sep=0pt, outer sep=0pt, minimum width=\st@w, minimum height=\st@h] + (#1) at #2 {};} + % \stface[keys]{name}{center coord}{rows}{cols} % rows/cols accept a declared axis name or a raw integer. Height <- rows, % width <- cols, always: a matrix face a x b never renders sideways. +% An empty coord places the face at the row cursor. The bracket decoration is +% drawn outside the node box, so its 3.4mm on each side is reserved too. \newcommand{\stface}[5][]{% \begingroup \st@setup{#1}{#4}{#5}% - \st@facecore{#2}{#3}% + \ifblank{#3}{% + \ifst@bracket\pgfmathsetlengthmacro{\st@tw}{\st@w+6.8mm}\else\let\st@tw\st@w\fi + \st@flowbegin{#2}{\st@tw}{\st@h}% + \edef\st@do{\noexpand\st@facecore{#2}{\st@pos}}\st@do + \st@regrow{#2}% + }{\st@facecore{#2}{#3}}% \endgroup} % \stindexface[keys]{name}{center coord}{rows}{cols}{entries} @@ -175,8 +438,11 @@ \newcommand{\stindexface}[6][]{% \begingroup \st@setup{#1}{#4}{#5}% - \node[inner sep=0pt, outer sep=0pt, minimum width=\st@w, minimum height=\st@h] - (#2) at #3 {}; + \ifblank{#3}% + {\ifst@bracket\pgfmathsetlengthmacro{\st@tw}{\st@w+6.8mm}\else\let\st@tw\st@w\fi + \st@flowbegin{#2}{\st@tw}{\st@h}% + \edef\st@do{\noexpand\st@basenode{#2}{\st@pos}}\st@do}% + {\st@basenode{#2}{#3}}% \foreach \st@e [count=\st@z from 0] in {#6} {% \pgfmathtruncatemacro{\st@ii}{div(\st@z,\st@cols)+1}% \pgfmathtruncatemacro{\st@jj}{mod(\st@z,\st@cols)+1}% @@ -196,6 +462,7 @@ \draw[draw=black!60, line width=0.5pt, rounded corners=1pt] (#2.south west) rectangle (#2.north east); \fi + \ifblank{#3}{\st@regrow{#2}}{}% \endgroup} \newcommand{\st@facebody}[1]{% @@ -250,9 +517,17 @@ \st@setup{#1}{#4}{#5}% \pgfmathtruncatemacro{\st@back}{#6-1}% \pgfmathsetlengthmacro{\st@dx}{1.3mm}% - \coordinate (#2-c) at ($#3+(-0.5*\st@back*\st@dx,-0.5*\st@back*\st@dx)$); - \node[inner sep=0pt, outer sep=0pt, minimum width=\st@w, minimum height=\st@h] - (#2-front) at (#2-c) {}; + % The offset sheets are part of the object: reserve their extent too, or the + % neighbour is spaced against the front sheet and lands on the back ones. + \ifblank{#3}% + {\pgfmathsetlengthmacro{\st@tw}{\st@w+\st@back*\st@dx}% + \pgfmathsetlengthmacro{\st@th}{\st@h+\st@back*\st@dx}% + \ifst@bracket\pgfmathsetlengthmacro{\st@tw}{\st@tw+6.8mm}\fi + \st@flowbegin{#2}{\st@tw}{\st@th}% + \edef\st@do{\noexpand\st@coordat{#2-c}% + {($\st@pos+(-0.5*\st@back*\st@dx,-0.5*\st@back*\st@dx)$)}}\st@do}% + {\coordinate (#2-c) at ($#3+(-0.5*\st@back*\st@dx,-0.5*\st@back*\st@dx)$);}% + \st@basenode{#2-front}{(#2-c)}% \ifnum\st@back>0 % Ascending loop, descending index: `{\macro,...,1}' cannot infer its % direction from an unexpanded macro and runs away. @@ -269,6 +544,7 @@ \node[inner sep=0pt, outer sep=0pt, fit={(#2-front) ($(#2-front.north east)+(\st@back*\st@dx,\st@back*\st@dx)$)}] (#2) {}; + \ifblank{#3}{\st@regrow{#2}}{}% \endgroup} % ------------------------------------------------- symbol / shape captions --- @@ -286,7 +562,8 @@ {\node[st sym, below=1.6mm of #1] (#1-sym) {#2};}% {\node[st sym, anchor=north] at ($(#1.center |- \st@lane.south)+(0,-1.6mm)$) (#1-sym) {#2};}% - \node[st shape, below=0.6mm of #1-sym] (#1-shape) {#3};} + \node[st shape, below=0.6mm of #1-sym] (#1-shape) {#3};% + \st@regall{#1-shape}\st@lower{#1-shape}} \newcommand{\stcaptiontop}[2]{% \node[st sym, above=1.6mm of #1] (#1-top) {#2};} diff --git a/examples/mha-causal.tex b/examples/mha-causal.tex index 34d3505..5f84f26 100644 --- a/examples/mha-causal.tex +++ b/examples/mha-causal.tex @@ -2,6 +2,7 @@ % Shows: leading axes as stack depth, a transpose that physically swaps the % face, equal edge length on the contracted axis, and a Boolean mask drawn in % a different grammar from the scores it gates. +% Layout is entirely by cursor: no absolute coordinate appears below. % ../scripts/build.sh mha-causal.tex \documentclass[border=10pt]{standalone} \usepackage[cjk]{supertensor} @@ -19,79 +20,66 @@ \begin{document} \begin{tikzpicture} -% ================================================================= formula == -\node (F) at (0,0) {\stformula{$\displaystyle - \mathbf A^{(i)}=\mathrm{softmax}\!\left( - \frac{\mathbf Q^{(i)}\mathbf K^{(i)\top}}{\sqrt{d_h}}+\mathbf M\right),\qquad - \mathbf O^{(i)}=\mathbf A^{(i)}\mathbf V^{(i)}$}}; - % ============================================================ stage A row === -\node[st stage, below=7mm of F] (SA) {每头打分:沿 $d_h$ 收缩}; -\coordinate (a) at ($(SA)+(-3.9,-1.9)$); - -\ststack[role=q, bracket=true]{Q}{(a)}{T}{dh}{3} -\node[st op, right=6mm of Q] (mA) {$\times$}; -% K^T: the face is physically swapped, not relabelled. Its height equals Q's -% width -- that is the contracted axis d_h, drawn at one edge length. -\ststack[role=k, bracket=true]{KT}{($(mA)+(1.9,0)$)}{dh}{T}{3} -\node[st op, right=6mm of KT] (eA) {$=$}; -\ststack[role=s]{S}{($(eA)+(2.0,0)$)}{T}{T}{3} - -\node[inner sep=0pt, fit=(Q)(KT)(S)] (rowA) {}; -\stlane{rowA} +\ststage{SA}{每头打分:沿 $d_h$ 收缩} +\strow{rowA}{T} + \ststack[role=q, bracket=true]{Q}{}{T}{dh}{3} + \stglyph{mA}{$\times$} + % K^T: the face is physically swapped, not relabelled. Its height equals Q's + % width -- that is the contracted axis d_h, drawn at one edge length. + \ststack[role=k, bracket=true]{KT}{}{dh}{T}{3} + \stglyph{eA}{$=$} + \ststack[role=s]{S}{}{T}{T}{3} +\strowend \stcaption{Q}{$\mathbf Q^{(i)}$}{$h\times T\times d_h$} \stcaption{KT}{$\mathbf K^{(i)\top}$}{$h\times d_h\times T$} \stcaption{S}{$\mathbf S^{(i)}$}{$h\times T\times T$} -\stnolane % ============================================================ stage B row === -\node[st stage, below=9mm of Q-shape.south west, anchor=north west] (SB) - {因果掩码与加权求和}; -\coordinate (b) at ($(SB)+(0.6,-2.0)$); - -% The mask is a Boolean support, not a magnitude: one flat level, exact -% triangle, no stack -- it is shared by every head. -\stface[role=w, pattern=data, - data={300000,330000,333000,333300,333330,333333}]{M}{(b)}{T}{T} -\ststack[role=s, pattern=causal]{A}{($(M.east)+(3.75,0)$)}{T}{T}{3} -\starrowlabel{M.east}{A.west}{softmax} -\node[st op, right=6mm of A] (mB) {$\times$}; -\ststack[role=v, bracket=true]{V}{($(mB)+(1.4,0)$)}{T}{dh}{3} -\node[st op, right=6mm of V] (eB) {$=$}; -\ststack[role=v]{O}{($(eB)+(1.4,0)$)}{T}{dh}{3} - -\node[inner sep=0pt, fit=(M)(A)(V)(O)] (rowB) {}; -\stlane{rowB} +\ststage{SB}{因果掩码与加权求和} +\strow{rowB}{T} + % The mask is a Boolean support, not a magnitude: one flat level, exact + % triangle, no stack -- it is shared by every head. + \stface[role=w, pattern=data, + data={300000,330000,333000,333300,333330,333333}]{M}{}{T}{T} + \stlink{lB}{softmax} + \ststack[role=s, pattern=causal]{A}{}{T}{T}{3} + \stglyph{mB}{$\times$} + \ststack[role=v, bracket=true]{V}{}{T}{dh}{3} + \stglyph{eB}{$=$} + \ststack[role=v]{O}{}{T}{dh}{3} +\strowend \stcaption{M}{$\mathbf M$}{$T\times T$} \stcaption{A}{$\mathbf A^{(i)}$}{$h\times T\times T$} \stcaption{V}{$\mathbf V^{(i)}$}{$h\times T\times d_h$} \stcaption{O}{$\mathbf O^{(i)}$}{$h\times T\times d_h$} -\stnolane % ============================================================ stage C row === -\node[st stage, below=9mm of M-shape.south west, anchor=north west] (SC) - {沿 $d_h$ 拼接后投影}; -\coordinate (c) at ($(SC)+(1.2,-2.0)$); - -% Concatenation reverses the split: three h-shards of width d_h tile a face of -% width d exactly. -\stface[role=v]{C1}{(c)}{T}{dh} -\stface[role=v]{C2}{($(C1.east)+(1.5*\stunit,0)$)}{T}{dh} -\stface[role=v]{C3}{($(C2.east)+(1.5*\stunit,0)$)}{T}{dh} -\node[st op, right=6mm of C3] (mC) {$\times$}; -\stface[role=w]{WO}{($(mC)+(2.5,0)$)}{d}{d} -\node[st op, right=6mm of WO] (eC) {$=$}; -\stface[role=v, bracket=true]{Y}{($(eC)+(2.5,0)$)}{T}{d} - -\node[inner sep=0pt, fit=(C1)(WO)(Y)] (rowC) {}; -\stlane{rowC} +\ststage{SC}{沿 $d_h$ 拼接后投影} +\strow{rowC}{d} % the d x d projection is the tallest object here + % Concatenation reverses the split: gap=0pt makes the three h-shards of width + % d_h tile a face of width d exactly, with no eyeballed offset. + \stface[role=v]{C1}{}{T}{dh} + \stface[role=v, gap=0pt]{C2}{}{T}{dh} + \stface[role=v, gap=0pt]{C3}{}{T}{dh} + \stglyph{mC}{$\times$} + \stface[role=w]{WO}{}{d}{d} + \stglyph{eC}{$=$} + \stface[role=v, bracket=true]{Y}{}{T}{d} +\strowend \stcaption{C2}{$[\,\mathbf O^{(1)}\mid\mathbf O^{(2)}\mid\mathbf O^{(3)}\,]$}{$T\times d$} \stcaption{WO}{$\mathbf W_O$}{$d\times d$} \stcaption{Y}{$\mathbf Y$}{$T\times d$} -\stnolane + +% ================================================================= formula == +% Placed last so it is centered on the figure that was actually drawn. +\sttopformula{F}{$\displaystyle + \mathbf A^{(i)}=\mathrm{softmax}\!\left( + \frac{\mathbf Q^{(i)}\mathbf K^{(i)\top}}{\sqrt{d_h}}+\mathbf M\right),\qquad + \mathbf O^{(i)}=\mathbf A^{(i)}\mathbf V^{(i)}$} % ============================================================== meaning box == -\node[inner sep=0pt, fit=(F)(rowA)(rowB)(rowC)(Y-shape)(C2-shape)] (all) {}; +\stbbox{all} \stmeaningbox{mb}{16.8cm}{all} {$T$ 序列长度,$d_h$ 单头宽度,$h$ 头数(图中 $h=3$,即堆叠的三张面), $d=h\,d_h$;批轴 $B$ 省略} diff --git a/examples/moe-topk-gather.tex b/examples/moe-topk-gather.tex index aa12d79..2d71752 100644 --- a/examples/moe-topk-gather.tex +++ b/examples/moe-topk-gather.tex @@ -3,6 +3,7 @@ % (graded lightness), an INDEX face (discrete symbols, no ramp), and a BOOLEAN % support face (one flat level) -- plus a gather whose output heights are data % dependent and must sum back to T*k. +% Layout is entirely by cursor: no absolute coordinate appears below. % ../scripts/build.sh moe-topk-gather.tex \documentclass[border=10pt]{standalone} \usepackage[cjk]{supertensor} @@ -22,84 +23,69 @@ \begin{document} \begin{tikzpicture} -% ================================================================= formula == -\node (F) at (0,0) {\stformula{$\displaystyle - \mathbf G=\mathrm{softmax}(\mathbf{XW}_g),\quad - \mathcal I_t=\operatorname*{top-}k_{e}\,\mathbf G_{t,e},\quad - \mathbf D_{t,e}=\mathbf 1[e\in\mathcal I_t],\quad - \mathbf X^{(e)}=\mathrm{gather}(\mathbf X,\mathbf D_{:,e})$}}; - % ============================================================ stage A row === -\node[st stage, below=7mm of F] (SA) {打分:token 对专家}; -\coordinate (a) at ($(SA)+(-3.6,-1.8)$); - -\stface[role=act, bracket=true]{X}{(a)}{T}{d} -\node[st op, right=6mm of X] (mA) {$\times$}; -\stface[role=wg]{Wg}{($(mA)+(1.6,0)$)}{d}{E} -\node[st op, right=6mm of Wg] (eA) {$=$}; -\stface[role=s]{G}{($(eA)+(1.6,0)$)}{T}{E} - -\node[inner sep=0pt, fit=(X)(Wg)(G)] (rowA) {}; -\stlane{rowA} +\ststage{SA}{打分:token 对专家} +\strow{rowA}{T} + \stface[role=act, bracket=true]{X}{}{T}{d} + \stglyph{mA}{$\times$} + \stface[role=wg]{Wg}{}{d}{E} + \stglyph{eA}{$=$} + \stface[role=s]{G}{}{T}{E} +\strowend \stcaption{X}{$\mathbf X$}{$T\times d$} \stcaption{Wg}{$\mathbf W_g$}{$d\times E$} \stcaption{G}{$\mathbf G$}{$T\times E$} -\stnolane % ============================================================ stage B row === -\node[st stage, below=9mm of X-shape.south west, anchor=north west] (SB) - {取前 $k$:连续分数 $\rightarrow$ 离散选择}; -\coordinate (b) at ($(SB.west)+(0.6,-1.7)$); - -% Indices are drawn as symbols, not as magnitudes: expert 3 is not "bigger" -% than expert 0, so the index face gets no lightness ramp. -\stindexface[role=idx]{I}{(b)}{T}{k}{0,1, 1,2, 2,3, 3,0, 0,2, 1,3} -% The same routing decision as a boolean support: one flat level, exactly k -% cells per row, and every unselected cell left unfilled. -\stface[role=m, pattern=data, level=3, - data={3300,0330,0033,3003,3030,0303}]{D}{($(I.east)+(3.1,0)$)}{T}{E} -\starrowlabel{I.east}{D.west}{one-hot} - -\node[st comm, right=9mm of D] (gz) {Gather}; -\starrow{D.east}{gz.west} - -\node[inner sep=0pt, fit=(I)(D)(gz)] (rowB) {}; -\stlane{rowB} +\ststage{SB}{取前 $k$:连续分数 $\rightarrow$ 离散选择} +\strow{rowB}{T} + % Indices are drawn as symbols, not as magnitudes: expert 3 is not "bigger" + % than expert 0, so the index face gets no lightness ramp. + \stindexface[role=idx]{I}{}{T}{k}{0,1, 1,2, 2,3, 3,0, 0,2, 1,3} + \stlink{lb}{one-hot} + % The same routing decision as a boolean support: one flat level, exactly k + % cells per row, and every unselected cell left unfilled. + \stface[role=m, pattern=data, level=3, + data={3300,0330,0033,3003,3030,0303}]{D}{}{T}{E} + \stlink{lg}{} + \stcomm{gz}{Gather} +\strowend \stcaption{I}{$\mathcal I$}{$T\times k$} \stcaption{D}{$\mathbf D$}{$T\times E$} -\stnolane % ============================================================ stage C row === -% Every stage heading starts on the same left rail; only the vertical position -% follows the previous row. -\coordinate (cy) at ($(I-shape.south)+(0,-9mm)$); -\node[st stage, anchor=north west] (SC) at (SB.west |- cy) - {按专家聚合:每个缓冲区的高度是数据决定的}; -\coordinate (c) at ($(SC.west)+(0.5,-1.9)$); - -% Each buffer keeps X's width d -- gather regroups rows, it never reshapes the -% feature axis. The heights are n_e, and they must sum to T*k. -\stindexface[role=idx, border=false]{t0}{(c)}{ne}{1}{1,4,5} -\stface[role=act]{B0}{($(t0.east)+(2*\stunit,0)$)}{ne}{d} -\stindexface[role=idx, border=false]{t1}{($(B0.east)+(1.1,0)$)}{ne}{1}{1,2,6} -\stface[role=act]{B1}{($(t1.east)+(2*\stunit,0)$)}{ne}{d} -\stindexface[role=idx, border=false]{t2}{($(B1.east)+(1.1,0)$)}{ne}{1}{2,3,5} -\stface[role=act]{B2}{($(t2.east)+(2*\stunit,0)$)}{ne}{d} -\stindexface[role=idx, border=false]{t3}{($(B2.east)+(1.1,0)$)}{ne}{1}{3,4,6} -\stface[role=act]{B3}{($(t3.east)+(2*\stunit,0)$)}{ne}{d} - +\ststage{SC}{按专家聚合:每个缓冲区的高度是数据决定的} +\strow{rowC}{ne} + % Each buffer keeps X's width d -- gather regroups rows, it never reshapes + % the feature axis. The heights are n_e, and they must sum to T*k. + % A token column and its buffer are one object: tight gap inside the pair, + % the standing gutter (widened) between pairs. + \stindexface[role=idx, border=false]{t0}{}{ne}{1}{1,4,5} + \stface[role=act, gap=2.5mm]{B0}{}{ne}{d} + \stindexface[role=idx, border=false, gap=12mm]{t1}{}{ne}{1}{1,2,6} + \stface[role=act, gap=2.5mm]{B1}{}{ne}{d} + \stindexface[role=idx, border=false, gap=12mm]{t2}{}{ne}{1}{2,3,5} + \stface[role=act, gap=2.5mm]{B2}{}{ne}{d} + \stindexface[role=idx, border=false, gap=12mm]{t3}{}{ne}{1}{3,4,6} + \stface[role=act, gap=2.5mm]{B3}{}{ne}{d} +\strowend \stcaptiontop{t0}{\stshapefont{token}} - -\node[inner sep=0pt, fit=(t0)(B3)] (rowC) {}; -\stlane{rowC} +\sttrack{t0-top} \stcaption{B0}{$\mathbf X^{(1)}$}{$n_1\times d$} \stcaption{B1}{$\mathbf X^{(2)}$}{$n_2\times d$} \stcaption{B2}{$\mathbf X^{(3)}$}{$n_3\times d$} \stcaption{B3}{$\mathbf X^{(4)}$}{$n_4\times d$} -\stnolane + +% ================================================================= formula == +% Placed last so it is centered on the figure that was actually drawn. +\sttopformula{F}{$\displaystyle + \mathbf G=\mathrm{softmax}(\mathbf{XW}_g),\quad + \mathcal I_t=\operatorname*{top-}k_{e}\,\mathbf G_{t,e},\quad + \mathbf D_{t,e}=\mathbf 1[e\in\mathcal I_t],\quad + \mathbf X^{(e)}=\mathrm{gather}(\mathbf X,\mathbf D_{:,e})$} % ============================================================== meaning box == -\node[inner sep=0pt, fit=(F)(rowA)(rowB)(rowC)(B3-shape)(t0-top)] (all) {}; +\stbbox{all} \stmeaningbox{mb}{16.6cm}{all} {$T$ token 数,$d$ 模型宽度,$E$ 专家数(图中 $E=4$),$k$ 每 token 选中的专家数 (图中 $k=2$),$n_e$ 落到第 $e$ 个专家的 token 数} diff --git a/examples/tp-ffn-allreduce.tex b/examples/tp-ffn-allreduce.tex index 71d7581..f6c06b2 100644 --- a/examples/tp-ffn-allreduce.tex +++ b/examples/tp-ffn-allreduce.tex @@ -1,6 +1,8 @@ % Golden example 1 -- tensor parallel FFN, column-then-row sharding + AllReduce. % Shows: partition geometry (shards tile the parent exactly), one hue per TP -% rank held across every stage, a collective node as a real operation. +% rank held across every stage, a collective node as a real operation, and a +% split along the contracted axis drawn as a column sub-flow. +% Layout is entirely by cursor: no absolute coordinate appears below. % ../scripts/build.sh tp-ffn-allreduce.tex \documentclass[border=10pt]{standalone} \usepackage[cjk]{supertensor} @@ -14,74 +16,71 @@ \stdim{bt}{6} % B*T rows \stdim{d}{4} % model width \stdim{dffl}{4} % d_ff / p (per-rank hidden width) +\stdim{dff}{8} % d_ff = p * (d_ff/p); the height of the stacked W_2 column \begin{document} \begin{tikzpicture} -% ================================================================= formula == -\node (F) at (0,0) {\stformula{$\mathbf{XW}_1=[\,\mathbf{XW}_1^{(1)}\mid - \mathbf{XW}_1^{(2)}\,]=[\,\mathbf H^{(1)}\mid\mathbf H^{(2)}\,]$}}; -\node[below=1.2mm of F] (F2) {\stformula{$\displaystyle - [\,\mathbf H^{(1)}\mid\mathbf H^{(2)}\,] - \begin{bmatrix}\mathbf W_2^{(1)}\\[-1pt]\mathbf W_2^{(2)}\end{bmatrix} - =\sum_{r}\mathbf H^{(r)}\mathbf W_2^{(r)}=\sum_r\mathbf P^{(r)}$}}; - % ============================================================ stage A row === -\node[st stage, below=7mm of F2] (SA) {列切 $\mathbf W_1$:无通信}; -\coordinate (a) at ($(SA)+(-5.6,-1.8)$); - -\stface[role=act, bracket=true]{X}{(a)}{bt}{d} -\node[st op, right=5mm of X] (mA) {$\times$}; -% Two shards, tiled exactly: adjacent faces, no stretching, no gap. -\stface[role=r1]{W1a}{($(mA)+(1.5,0)$)}{d}{dffl} -\stface[role=r2]{W1b}{($(W1a.east)+(2*\stunit,0)$)}{d}{dffl} -\node[st op, right=5mm of W1b] (eA) {$=$}; -\stface[role=r1]{Ha}{($(eA)+(1.5,0)$)}{bt}{dffl} -\stface[role=r2]{Hb}{($(Ha.east)+(2*\stunit,0)$)}{bt}{dffl} - -\node[inner sep=0pt, fit=(X)(W1a)(Ha)(Hb)] (rowA) {}; -\stlane{rowA} +\ststage{SA}{列切 $\mathbf W_1$:无通信} +\strow{rowA}{bt} + \stface[role=act, bracket=true]{X}{}{bt}{d} + \stglyph{mA}{$\times$} + % Two shards, tiled exactly: gap=0pt makes them adjacent by construction, + % so neither stretching nor an eyeballed offset can creep in. + \stface[role=r1]{W1a}{}{d}{dffl} + \stface[role=r2, gap=0pt]{W1b}{}{d}{dffl} + \stglyph{eA}{$=$} + \stface[role=r1]{Ha}{}{bt}{dffl} + \stface[role=r2, gap=0pt]{Hb}{}{bt}{dffl} +\strowend \stcaption{X}{$\mathbf X$}{$BT\times d$} \stcaption{W1a}{$\mathbf W_1^{(1)}$}{$d\times d_{\mathrm{ff}}/p$} \stcaption{W1b}{$\mathbf W_1^{(2)}$}{$d\times d_{\mathrm{ff}}/p$} \stcaption{Ha}{$\mathbf H^{(1)}$}{$BT\times d_{\mathrm{ff}}/p$} \stcaption{Hb}{$\mathbf H^{(2)}$}{$BT\times d_{\mathrm{ff}}/p$} -\stnolane % ============================================================ stage B row === -\node[st stage, below=9mm of X-shape.south west, anchor=north west] (SB) - {行切 $\mathbf W_2$:一次 All-Reduce}; -\coordinate (b) at ($(SB)+(-0.4,-1.9)$); - -\stface[role=r1]{Ga}{(b)}{bt}{dffl} -\stface[role=r2]{Gb}{($(Ga.east)+(2*\stunit,0)$)}{bt}{dffl} -\node[st op, right=5mm of Gb] (mB) {$\times$}; -% W_2 is split along the CONTRACTED axis: the two shards stack vertically and -% together have exactly the height of H's width. Splitting reverses concat. -\stface[role=r1]{W2a}{($(mB)+(1.35,0.46)$)}{dffl}{d} -\stface[role=r2]{W2b}{($(W2a.south)+(0,-2*\stunit)$)}{dffl}{d} -\node[st op, right=5mm of W2a.east |- W2a.south] (eB) {$=$}; -\stface[role=r1]{Pa}{($(eB)+(1.3,0)$)}{bt}{d} -\node[st op, right=4mm of Pa] (plus) {$+$}; -\stface[role=r2]{Pb}{($(plus)+(1.3,0)$)}{bt}{d} - -\node[st comm, right=9mm of Pb] (ar) {All-Reduce}; -\stface[role=act, bracket=true]{Y}{($(ar)+(1.9,0)$)}{bt}{d} -\starrow{Pb.east}{ar.west} -\starrow{ar.east}{Y.west} - -\node[inner sep=0pt, fit=(Ga)(W2a)(W2b)(Pa)(Pb)(Y)] (rowB) {}; -\stlane{rowB} +\ststage{SB}{行切 $\mathbf W_2$:一次 All-Reduce} +\strow{rowB}{dff} % the stacked W_2 column is the tallest object here + \stface[role=r1]{Ga}{}{bt}{dffl} + \stface[role=r2, gap=0pt]{Gb}{}{bt}{dffl} + \stglyph{mB}{$\times$} + % W_2 is split along the CONTRACTED axis: the shards stack vertically and + % together have exactly the height of H's width. Splitting reverses concat, + % which is what gap=0pt inside the column states. + \stcol{W2}{dff} + \stface[role=r1]{W2a}{}{dffl}{d} + \stface[role=r2, gap=0pt]{W2b}{}{dffl}{d} + \stcolend + \stglyph{eB}{$=$} + \stface[role=r1]{Pa}{}{bt}{d} + \stglyph{plus}{$+$} + \stface[role=r2]{Pb}{}{bt}{d} + \stlink{lc}{} + \stcomm{ar}{All-Reduce} + \stlink{ly}{} + \stface[role=act, bracket=true]{Y}{}{bt}{d} +\strowend \stcaption{Ga}{$\mathbf G^{(1)}$}{$BT\times d_{\mathrm{ff}}/p$} \stcaption{Gb}{$\mathbf G^{(2)}$}{$BT\times d_{\mathrm{ff}}/p$} -\stcaption{W2b}{$\mathbf W_2^{(r)}$}{$d_{\mathrm{ff}}/p\times d$} +\stcaption{W2}{$\mathbf W_2^{(r)}$}{$d_{\mathrm{ff}}/p\times d$} \stcaption{Pa}{$\mathbf P^{(1)}$}{$BT\times d$} \stcaption{Pb}{$\mathbf P^{(2)}$}{$BT\times d$} \stcaption{Y}{$\mathbf Y$}{$BT\times d$} -\stnolane + +% ================================================================= formula == +% Placed last so it is centered on the figure that was actually drawn. +\sttopformula{F}{$\begin{gathered} + \mathbf{XW}_1=[\,\mathbf{XW}_1^{(1)}\mid + \mathbf{XW}_1^{(2)}\,]=[\,\mathbf H^{(1)}\mid\mathbf H^{(2)}\,]\\[1.2mm] + [\,\mathbf H^{(1)}\mid\mathbf H^{(2)}\,] + \begin{bmatrix}\mathbf W_2^{(1)}\\[-1pt]\mathbf W_2^{(2)}\end{bmatrix} + =\sum_{r}\mathbf H^{(r)}\mathbf W_2^{(r)}=\sum_r\mathbf P^{(r)} +\end{gathered}$} % ============================================================== meaning box == -\node[inner sep=0pt, fit=(F)(rowA)(rowB)(Y-shape)(Ga-shape)] (all) {}; +\stbbox{all} \stmeaningbox{mb}{16.4cm}{all} {$BT$ 展平后的 token 数,$d$ 模型宽度,$d_{\mathrm{ff}}$ 前馈中间宽度, $p$ TP 并行度(图中 $p=2$)} diff --git a/references/antipatterns.md b/references/antipatterns.md index 13342a9..bb5807e 100644 --- a/references/antipatterns.md +++ b/references/antipatterns.md @@ -15,9 +15,9 @@ dimension. **Fix:** swap the arguments — `\ststack{KT}{...}{dh}{T}{3}`. See `g ## 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. +Both assert a width that the tensor does not have. **Fix:** draw the shards in one band +and give every shard after the first `gap=0pt`, which *states* that they are adjacent +instead of arranging for it; draw an ellipsis for anything omitted. See `geometry.md` §5–6. ## 3. An index drawn as a heatmap @@ -41,12 +41,18 @@ See `style.md`. 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`. + `\stlink` makes this unrepresentable: the label reserves its own width in the band and + the arrow is drawn to whatever lands beside it. 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`. + heights. Use `\strow`/`\strowend`, which arms `\stlane` for you. +- **Hand-tuned offsets.** Each one is a magic number valid only for the content that was + there when you tuned it; the figure that breaks is the *next* one, when a label grows two + characters and lands on a face. Use the cursor. See `layout.md`. +- **A formula line placed first.** It can only be centered on a figure whose width is not + known yet, so it ends up visibly off-center. Call `\sttopformula` after the bands. - **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 diff --git a/references/api.md b/references/api.md index a7809de..77b842e 100644 --- a/references/api.md +++ b/references/api.md @@ -23,17 +23,85 @@ Colors: `stTeal stOrange stCoral stViolet stGray stInk`. An unknown role falls b 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. + +```tex +\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 | +| `\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: + +```tex +\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. + ## Faces ```tex -\stface[keys]{name}{(coord)}{rows}{cols} +\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. `(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`. +`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: @@ -46,6 +114,7 @@ Keys: | `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`. @@ -58,12 +127,21 @@ It deliberately has no lightness ramp — see `semantics.md`. ## Captions +`\strowend` calls `\stlane` for you, so in flow mode captions just follow the band: + +```tex +\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): + ```tex \node[inner sep=0pt, fit=(A)(B)(C)] (rowA) {}; \stlane{rowA} -\stcaption{A}{$\mathbf A$}{$T\times d$} % symbol lane, shape lane +\stcaption{A}{$\mathbf A$}{$T\times d$} \stnolane -\stcaptiontop{A}{\stshapefont{token}} % occasional label above a face ``` `\stcaption` defines `name-sym` and `name-shape` nodes; anchor the next stage heading @@ -71,8 +149,10 @@ against `name-shape.south`. ## Operators, connectors, nodes +Prefer `\stglyph` / `\stcomm` / `\stlink` (above). The raw forms are for absolute placement: + ```tex -\node[st op, right=6mm of A] (m) {$\times$}; +\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} @@ -85,13 +165,14 @@ Connectors route on the background layer automatically. ## Bottom ```tex -\node[inner sep=0pt, fit=(F)(rowA)(rowB)(Y-shape)] (all) {}; +\stbbox{all} % flow: everything drawn so far \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. +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. ## Gotchas @@ -101,3 +182,9 @@ top label in that `fit` or the box will overlap them. An empty `{}` row is dropp - `\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`. diff --git a/references/checklist.md b/references/checklist.md index 8b84d93..5f1cb12 100644 --- a/references/checklist.md +++ b/references/checklist.md @@ -26,8 +26,13 @@ Work through it against the rendered PNG. Any mandatory violation means redraw, ## 4. Full-size visual audit -Open the PNG at 100 %. +Open the PNG at 100 %. Most of the first three items are automatic under the flow layout; +they still need looking at, because the cursor only guarantees that objects do not *push* +into each other, not that the figure reads correctly. +- [ ] No absolute coordinate that a `\strow`/`\stcol` could have expressed. Every remaining + hand-placed node is wrapped in `\sttrack`. +- [ ] `\sttopformula` called after the bands, so the formula is centered on the real figure. - [ ] 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. diff --git a/references/layout.md b/references/layout.md index abd7c06..01fd754 100644 --- a/references/layout.md +++ b/references/layout.md @@ -4,11 +4,29 @@ Everything on the canvas is a bounding box: tensors, full offset stacks, bracket operators, arrow labels, annotations, symbols, shape labels, stage headings, the meaning box, the signature. **Tangency counts as collision.** +## Place by cursor, not by coordinate + +The invariants below are *statements about the finished figure*; the flow layout in +`api.md` is how you get them without checking each one by hand. Use it by default: +`\ststage` / `\strow` … `\strowend` / `\stcol` … `\stcolend`, with an empty coordinate +argument on every face. + +The failure it removes is specific. With hand-written offsets, each gap is a magic number +tuned against the *current* content, so the day a label grows by two characters it silently +lands on the next tensor — the figure still compiles and still looks clean. With the +cursor, every object reserves its own width, so growing one object can only push the rest +apart. And because a band declares its height, an object that does not fit becomes a +`Package supertensor Warning`, which `build.sh` turns into a failed build. + +Hand placement remains available for the cases the cursor cannot express. When you use it, +wrap the node in `\sttrack` so the bounding box and the vertical cursor still see it. + ## 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. +Define one base gutter `g ≥ 1 em`; that is what `\stgutter` (6 mm) is. Unrelated boxes stay +at least `g` apart; stage bands at least `1.5g` (`\strowgap`, `\stblockgap`). Set them once +at the top of the figure rather than per call — a per-call `gap=` is for stating a +*relationship* (`gap=0pt` = these shards tile), not for nudging. Overlap is allowed only inside one declared composite: @@ -33,26 +51,22 @@ shapes <- \stcaption arg 3 ``` Faces of different heights would otherwise hang their captions at different depths. Fix it -with a shared baseline: +with a shared baseline — `\strowend` arms one automatically: ```tex -\node[inner sep=0pt, fit=(Q)(KT)(S)] (rowA) {}; -\stlane{rowA} +\strow{rowA}{T} + ... +\strowend \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. +Under absolute placement, arm it yourself with `\stlane{rowA}` … `\stnolane`. Either way +every `\stcaption` inside 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) {...}; -``` +Stage headings share one left rail. `\ststage` puts them there: the rail is a single stored +`x` (`\stleftrail` to move it), and the `y` is derived from the lowest ink drawn so far, so +a heading can neither drift right nor collide with the row above it. 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 @@ -67,8 +81,10 @@ 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. + another label. This is the single most common failure after a first draft, and `\stlink` + is the fix: it makes the label itself a flow object and draws the arrow to whatever + lands on either side of it, so the label cannot be wider than its connector. + `\starrowlabel` between two hand-placed nodes still has the old failure mode. ## Stacks diff --git a/tests/flow.tex b/tests/flow.tex new file mode 100644 index 0000000..42aa29f --- /dev/null +++ b/tests/flow.tex @@ -0,0 +1,71 @@ +% Regression test for the flow layout: stage headings on a shared left rail, +% bands whose objects reserve their own width, connectors whose labels are flow +% objects. No absolute coordinate appears anywhere in this figure. +% ../scripts/build.sh flow.tex +\documentclass[border=10pt]{standalone} +\usepackage[cjk]{supertensor} + +\stsetrole{act}{stTeal} +\stsetrole{w}{stOrange} +\stsetrole{o}{stViolet} + +\stdim{T}{7} +\stdim{d}{5} +\stdim{r}{3} + +\begin{document} +\begin{tikzpicture} + +\ststage{S1}{一、面、算子、带标签的连接线都由游标排布} +\strow{R1}{T} + \stface[role=act, bracket=true]{X}{}{T}{d} + \stglyph{m1}{$\times$} + \stface[role=w, bracket=true]{W}{}{d}{r} + \stlink{l1}{一个很长的标签也不会压到下一个张量} + \stface[role=o, bracket=true]{Y}{}{T}{r} +\strowend +\stcaption{X}{$\mathbf X$}{$T\times d$} +\stcaption{W}{$\mathbf W$}{$d\times r$} +\stcaption{Y}{$\mathbf Y$}{$T\times r$} + +\ststage{S2}{二、堆叠的偏移页也计入宽度,行首自动回到同一条左轨} +\strow{R2}{T} + \ststack[role=act]{Q}{}{T}{r}{3} + \stlink{l2}{} + \stcomm{c1}{All-Reduce} + \stlink{l3}{sum} + \stface[role=o, pattern=causal]{A}{}{T}{T} + \stgap{4mm} + \stface[role=w, pattern=diag]{D}{}{r}{r} +\strowend +\stcaption{Q}{$\mathbf Q$}{$3\times T\times r$} +\stcaption{A}{$\mathbf A$}{$T\times T$} +\stcaption{D}{$\mathbf D$}{$r\times r$} + +\ststage{S3}{三、列子流:两片沿收缩维叠放,恰好铺满一格} +\strow{R3}{T} + \stface[role=act]{H}{}{T}{d} + \stglyph{m3}{$\times$} + \stcol{Wc}{d} + % d = 2 * (d/2)... here r=3 and d=5, so the two shards are 3 and 2 units. + \stface[role=w]{Wa}{}{r}{T} + \stface[role=o, gap=0pt]{Wb}{}{2}{T} + \stcolend + \stglyph{e3}{$=$} + \stface[role=o, bracket=true]{Z}{}{T}{T} +\strowend +\stcaption{H}{$\mathbf H$}{$T\times d$} +\stcaption{Wc}{$\mathbf W^{(r)}$}{$d\times T$} +\stcaption{Z}{$\mathbf Z$}{$T\times T$} + +\stbbox{all} +\stmeaningbox{mb}{15cm}{all} + {$T$ 序列长,$d$ 模型维,$r$ 低秩维} + {} + {三行的左端都落在同一条左轨上,行内间距由 \texttt{\string\stgutter} 声明一次, + 标签自己占位,所以任何一处变宽都只会把后面的东西推开,不会盖住它们; + 第三行的列子流用 \texttt{gap=0pt} 声明两片相邻,声明高度与实际不符会直接报警} +\stsignature{流式排版自测}{mb} + +\end{tikzpicture} +\end{document}