Add flow layout: cursor placement, left rail, declared band heights

Figures were positioned by hand-written offsets. Every gap was a magic
number tuned against the content that happened to be there, so a label
that grew two characters landed on the next tensor, and two stages
started from two different x shared no rail. Both failures compile
cleanly.

Replace it with a cursor. Objects placed with an empty coordinate
argument reserve their own width -- including a stack's offset sheets
and a bracket's overhang -- and gaps are declared once (\stgutter,
\strowgap, \stblockgap). The gap belongs to the object that follows it
and the first object in a band gets none, so every band starts flush on
a shared rail and gap=0pt states that two shards tile exactly.

\stlink makes a connector's label a flow object, which is what removes
the label-wider-than-its-arrow failure entirely. \stcol is a vertical
sub-flow for a split along the contracted axis. \strow declares its
height, so an object that does not fit -- or a column that does not add
up to what it declared, and is therefore drawn off-center -- becomes a
package warning, which build.sh fails on.

Absolute placement is unchanged: passing a coordinate takes the original
code path, and \sttrack folds a hand-placed node back into the cursor.

All three golden examples and the new tests/flow.tex are converted and
build clean.
This commit is contained in:
dela
2026-08-05 15:55:21 +08:00
parent 7a22bef9e3
commit 7b59c81d02
11 changed files with 672 additions and 223 deletions
+44 -56
View File
@@ -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$ 省略}