serq draw: a program as a figure¶
Status: experimental. The command works and every program in examples/
and every IR file in tools/oracle/ renders under make check, but the
notation, the flags and the output are not stable, and --format svg/tikz
output may change between releases. §5 lists what is not done. Design
discussion: RFC #1.
FILE is program text (.sq) or IR (.json), as for run, check and
ir. Output goes to stdout unless --out names a file. --set and --def apply to
program text and are rejected on .json, where the constants are already
folded and the definitions expanded.
1. Why the figure is generated¶
pyncd (MIT Zardini Lab) writes a deep learning model as an algebraic term
and derives the PyTorch code, the backward pass and the diagram from it. The
diagram cannot drift from the model, because it is not a second description of
the model: it is the same term in a second notation.
docs/ir.md makes the same argument for serQ, and names the failure it fixes:
before the IR, the vLLM request program had three hand-kept copies. A figure
drawn by hand would be a fourth.
The view is a pure function of ir::Program. It runs no simulation, draw
no measurements, and are deterministic: the same IR gives the same bytes.
It adds no a type to src/ir.rs, so IR_VERSION is unaffected.
2. The deployment view¶
The program as a queueing network.
Pools and stages are declared, but the arrows are not — the flow is a property
of the session program. deployment::project walks it carrying a hold stack:
| Nodes | one per stage a Run reaches (Node::stage is Some); a CRef with count > 1 is one node labelled [N]. A loop that decides before its first station adds a decision ◇, a node with no stage (Node::stage is None, kind is Decision) |
| Edges | the successor relation on Runs in session order, threaded through Branch (both arms) and Loop (a body that decides before its first station - several first stations, or an end before any - starts at a decision ◇, named by the chooses it makes, which every turn comes back to and which an end before any station leaves from; any other body is walked twice, so its last stations lead back to the station it starts at). An arrow forward past other stations, and an entry past the first, run in a lane below the row rather than through them |
| Enclosure | every Run is tagged with the Holds around it; a group of stations sharing a hold on pool p becomes p's dashed box: the units it holds there. A leased pool stays on the stations after its hold and a Release takes it off. A choose picks an instance, drawn as a solid box around what the session indexes by its variable; inside one only its own pools are boxed, and a run between two instances (examples/pd-disaggregation/llmd_nixl_pull.sq's read) is an arrow between their boxes carrying what it moves and the link's latency, where pool boxes would cross. A hold whose units are the constant 0 only reserves (reqs (0) reserve (1), a request parked without a running slot), occupies nothing, and draws no box. A pool held at one station alone - every hold that takes it there reaches no other station (Net::resident_pools, from the stations each hold reaches) - is that station's: the station is drawn in an unfilled frame with a row per such pool under its glyph, and no dashed box. An instance is a filled panel, so a frame inside one reads as the station's, not the pod's. A transfer between instances does not count: it holds the sender's pool and the receiver's, and its arrow says so (P.kv[i] → D.kv[j]), so each KV is its engine's |
| Edge labels | a Branch guard, via Program::show_expr |
| Ends | CArrival labels the in-arrow, End the out-arrow |
Two runs at the same stage in a row are two visits, not a flow, and are not
drawn. A chain of guards that moves nobody (routing.sq has five sibling
branch (policy == k) blocks) collapses to one edge rather than multiplying
out. A choose annotates the station whose reference reads the attribute it
names — rep[j], not whatever station happens to come next.
Glyphs¶
| IR | Glyph |
|---|---|
Fifo(c) |
circle, FIFO, the server count when c ≠ 1 |
Ps(φ) |
circle, PS, φ beneath |
Delay |
rounded box with the shape of its duration's density - decaying for ~exp and ~h2, a hump for ~erlang, flat for ~uniform, a spike for ~det or a constant - and the duration as written under it (~exp(3)). A duration that is no distribution, or too long to fit, gets the lecture's row of small circles: infinitely many servers |
Step { … } |
a rounded box with a token-budget bar. The lecture has no glyph for this one: docs/language.md §4 calls the colocated engine the one stage kind the lecture could not express |
| a pool | a drum, its capacity and options written beside it (cap 8192 · block 16) |
| a pool held at one station alone | a row in the station's unfilled frame, under its glyph: drum, name, options |
| a pool held across several stations | dashed rounded box, the drum and options in the column at its left |
| a pool a hold caches in | a grey strip under its row, or along the bottom of its box |
| the queue a hold waits in | ahead of a dashed box, one per hold rather than per pool: a hold of several pools joins the queue of its first (interp.rs enqueue_hold). A frame draws none: its pools are taken at its entrance |
admit via S |
admit via S among the pool's options; from a dashed box, also a dashed edge from the queue to S |
A pool's eviction order is drawn only where something is cached in it: an order over an empty cache says nothing.
Which pool a cache clause leaves units in follows interp.rs::release_hold: a
hold with a growing run caches in that pool alone, and one without caches in
all of its pools. replica.sq is the case that makes the difference visible
— its hold batch (1), kv (…) really does keep a unit of batch cached.
3. Formats¶
--format tikz (default) writes a tikzpicture that needs \usepackage{tikz}
and nothing else, with the colours it uses defined above it. It is the default
because a figure in a LaTeX document is best TikZ source the document can
\input: an image inherits neither the document's fonts nor its rules, and
does not diff.
--format svg writes a standalone SVG. Colours are written as presentation
attributes with a prefers-color-scheme override, not as CSS custom
properties, because librsvg and cairosvg ignore var() and paint the
result black.
tsncd, the renderer behind pyncd, has no TikZ backend: it draws SVG into
the DOM with KaTeX and captures with html-to-image, and its figures reach
documents as images. That is right for notebooks and web pages. The TikZ
backend here is a divergence justified by the target artefact, not an
imitation. What is copied from tsncd is the layer that makes a second writer
cheap — one Figure, two writers, and geometry tested instead of bytes.
4. Layout and tests¶
src/view/figure.rs the geometry a view produces and a writer consumes
src/view/deployment.rs ir::Program -> Figure the network projection
src/view/tikz.rs Figure -> String
src/view/svg.rs Figure -> String
Figure is the test surface; no writer decides a coordinate. tests/draw.rs
asserts on rectangles and on the projected Net, with golden files
(tests/golden/, make draw-golden) guarding the writers; the figures the
site shows (docs/assets/NAME.deployment.svg) must be what their program
(the one NAME.sq under examples/*/ or docs/tutorial/programs/) draws now, and
make draw-golden rewrites them too. make check draws
every program in both formats, and every IR file in
tools/oracle/.
5. Not done¶
- A pool held in two places is drawn twice, as two frames or two enclosures. That is correct — a box spanning both would swallow the stations between them — but a reader may want to see that the two are the same pool, and nothing says so beyond the name.
- One station row. A program with many stages runs off to the right instead of wrapping.
- Long expressions are elided with
~rather than wrapped or footnoted. letnames are gone:cap blocks * bsprints as160000. The IR folds constants, and the folded value is what the run uses.- No step-trace figure. The figure with iterations across and residents
down — where magnitude and the invariant
allocated + cached ≤ caplive — needs a structured trace that--dumpdoes not emit. It is the natural next one.