Whisper tiny drawn as eleven named stages on two spines that meet: a log-mel spectrogram through a conv frontend into the audio encoder along the top, token ids through the token embedding into masked self-attention along the bottom, the two joining at a cross-attention block whose six heads are drawn as lanes, then a feed-forward, a box standing for three more decoder blocks, and vocabulary logits.

Whisper tiny — speech recognition, encoder–decoder, two inputs. 37,184,640 parameters over 271 traced operations, log-mel spectrogram to vocabulary logits. The six attention heads are drawn as lanes because this model writes q, k and v out rather than calling nn.MultiheadAttention, which fuses into a single traced node and takes its heads with it. Decoder block one is opened; the remaining three are one box, and repeat counted them by tiling that opened unit against the traced nodes. Its 1,647 units put detail type at about 8px here against the page's 17px, and it clears a 6pt floor at 14.5 inches — a slide or a poster, not a column. Below laptop width it stops shrinking and scrolls sideways at its own size instead; tap it to open the SVG on its own, where the browser's zoom is vector and there is no floor at all.

draughtsman

Architecture figures for PyTorch models.

The tracer supplies the facts. A person or an agent supplies the judgement — which operations to collapse into a stage and what to call it. It never supplies a number: every quantity in the figure is looked up from a trace of your model at render time.

A coverage check then proves no traced operation was silently dropped.

pip install draughtsman-nn PyPI source DOI BSD-3-Clause

The model above is Whisper tiny, and it broke two things.

OpenAI's speech recogniser at its published dimensions, written out in PyTorch from the model card and traced from that source: 80 mel bins, four encoder blocks against four decoder blocks, six heads, sinusoidal audio positions against learned text positions, cross-attention in every decoder block, and the output projection tied to the token embedding. The weights are random — this draws the architecture, not the trained model. It is the only encoder–decoder in the gallery, and it is the reason two parts of the tool are what they are.

trace took one input. It built a single dummy tensor, so every encoder–decoder, two-tower and masked model was excluded — not by difficulty, by signature. Whisper's forward wants the mel spectrogram and the token ids together. --input-shape now repeats, with an optional --dtype per input, and one shape still produces byte-identical output. A model with several inputs has no singular input shape, so a spec asking for one raises rather than quietly describing half the input.

A tied weight was charged twice. Whisper's output projection is its token embedding — one tensor reached through two prim::GetAttr nodes, both billed — and the trace reported the model as having 54% more parameters than it has. Coverage was green throughout: every node sat in exactly one stage, and the figure would have printed a total this model does not have. A parameter is now charged to the earliest substantive consumer, which also decides where it is drawn: the 19.9M-entry table appears at the embedding, where a reader meets it, rather than at a matmul four hundred nodes later.

The tensor, drawn to scale.

A box with numbers in it is what a tracer already gives you. The one thing only this tool asserts is that the area on the page is the tensor's own axes, resolved from the trace at render time — so a stage whose content is a tensor is drawn as one, and the box comes off.

LeNet drawn as stacks of sheets with no boxes around them: six countable sheets at the first two stages, then a solid marked times sixteen.

sheets and chrome: none together. Six channels can be counted, so six sheets are drawn. Sixteen cannot, so it becomes one solid of the same depth carrying its number, and the stage is the tensor rather than a box with a small mark inside it. The key names the scale — square root here, and it says so. Solved to print at 6 inches — 8.74pt type.

U-Net drawn as bare stacks of sheets across nine stages: the sheets shrink through the encoder to a small deep slab at the bottleneck and grow back through the decoder, with three dashed skips arcing across underneath.

The same fields on a model with real spatial extent, and the clearest instance of the claim in this repository: one scale across the whole figure, so equal values draw equal lengths. The face is the spatial map and the depth is the channel count — the pyramid is visible as area rather than stated as a number, and where a stack is too deep to separate it becomes one solid carrying its count. scale: linear, and the key says so. Its 1,607 units put detail type at about 8px here.

The same model, from the tool that traces it.

torchview 0.2.7 on the same build_whisper_tiny, called the way its own documentation calls it: draw_graph(model, input_data=[mel, tokens]), depth left at the default. Nothing in it is wrong. Seventy-four boxes, ninety-six edges, every operation carrying its real shapes. It is 547 × 4,896 points, and there is no page that goes on.

torchview's graph of Whisper tiny: a single narrow column of small boxes, each an operation with its input and output shapes listed beside it, running far below the bottom of the window.

torchview 0.2.7, at 1:1 — committed at 2× density, so a zoom reads rather than blurs. The window holds about a twelfth of it.

The draughtsman figure of Whisper tiny again, fitting inside a window of the same height with room left over.

draughtsman, the figure at the top of this page, in a window of exactly the same size. 335px of figure in 418px of window: the space under it is empty because the figure has ended.

torchview's graph of Whisper tiny with the eleven draughtsman stages drawn as tinted regions around the nodes each one covers, named down the right-hand side.

The same two views on one axis. Every stage is drawn around the nodes it covers, at torchview's own scale, so the heights are the claim: audio encoder is 24 boxes tall and ×3 is 27. The figure is 520 × 1,730 — scroll it, or open it on its own.

Asked for the depth draughtsman covers, torchview gives 234 boxes at 2,088 × 14,688 pixels. Neither number is a criticism of it — tracing is the half that can be mechanised, and it is the half this project takes from a tracer too. What it adds is the other half: a person or an agent decides the grouping and the names, and a coverage check then proves no traced operation went missing.

Three stages.

trace

Every node, shape and parameter count, from the model.

abstract

A person or an agent groups and names. Neither writes a number.

render

Deterministic SVG. Quantities read back by node id.

What you can ask a figure to do.

Everything below is a field in spec.json — judgement, committed, and the same on any machine. None of it is a render-time flag, because a figure that comes out differently depending on who ran it cannot be checked against the model.

The tensor, drawn to scale — glyph

One axis tall, another wide, both from the same shape, so the area the eye reads is a real product. block is one rectangle; marks draws the axes as countable objects and turns to a labelled bar past about thirty, where counting stops; sheets takes a third axis and stacks it, and is the only style that may. scale chooses sqrt or the faithful linear, and the key names which.

Both are directly above: sheets counted and then collapsed in lenet, one linear scale across unet.

No box at all — chrome

A box is right when a stage's content is words. It is wrong when the content is a drawing of the tensor: you get a rectangle around a rectangle competing for the same reading, and the eye settles on the larger. chrome set to none lets the tensor be the stage, and the colour family moves onto the glyph.

The box off, above; the box on, below. chrome is a field of a stage as well as of a figure, so a figure can box its words and bare its tensors — and tube still boxes both, because bared, its mark loses everything --icon strips and there is no bulk left to survive. Measured, not assumed.

Parallelism and repetition — lanes, repeat

lanes names the channels of a bank the agent chose to draw as parallel, and check asserts there are exactly as many labels as the model has channels. repeat names the stages that draw one unit and draughtsman counts how many times they tile against the traced nodes — so a repetition the graph does not contain fails rather than draws.

Edges that say what they are — edges

A label, a dashed style for an identity or a skip, and untraced — an arrow the trace does not contain, drawn anyway with a written reason. A reader wants a sampled latent to depend on its mean and variance; the trace records only the shape read. That reason lands in a diff instead of in nobody's head.

Arrangement — orientation, wrap

Depth otherwise converts straight into width, which is the ribbon this page criticises other tools for. wrap breaks the spine into rows at a stated width, and a break is refused where a long edge is still in flight — so a net whose skips span its whole depth barely wraps, which is the honest answer rather than a row break drawn through a skip.

A number as a picture — meters

A quantity drawn as a bar rather than read as digits. The value is a reference the renderer resolves, never a number the agent typed. The label is also the series: every bar sharing one is drawn on a single scale across the figure, and bars in different series are never comparable, because parameters and frames have no common unit.

A key under the drawing — legend

One row per colour family actually drawn, each carrying its share of the traced operations and parameters — counted off the graph, not off the picture. Colour cannot carry proportion when a box is a collapsed stage rather than a layer, so the legend answers how much of this model is convolution as a fact instead of an impression. Off by default.

Where it is going — output

width states the size the figure will be printed at and min_type the type it must hold there. Layout solves against that budget, and check refuses a figure whose smallest type would land under the floor — naming the number, and how much narrower it would have to be. The type is the one thing that never gives.

At a size where nothing can be read — --icon

At a card or tile size no type survives at any point size, so the honest response is not a smaller floor but no text. render --icon WxH removes what cannot be read — labels, key, sub-pixel detail, any stage that was only text, and the arrows into it — and crops to what is left. It never crops to fill, because a cropped net is a net with a stage missing at the one size where nobody can tell. Every model as a mark, below.

Saying what was left out — elided, batch_axis, constants

A dropped node needs a written reason, so the loss is a decision in a diff. batch_axis declares which axis to stop drawing, and check refuses the declaration wherever the hidden number is not 1. constants is required before a spec may quote a traced constant the tracer warned it had baked out of a tensor.

Every model as a mark, at the size it will be used.

Each of these is render --icon 192x96 against the same committed spec that draws the figure above it — one by two inches, shown here at actual size. Nothing is hand-placed and nothing is redrawn: a mark is the figure with everything unreadable taken out.

Three of them work. --icon prints the scale it drew at, and that number predicts the verdict. The tool calls a mark readable at 0.25× and up and noise below 0.19×, with the pair between marked marginal — and both boundaries sit in the middle of a real gap in the measurements rather than against the nearest one. That is the whole reason they are those numbers: the readable marks stop at 0.2975× and the marginal pair starts at 0.2114×, which is 0.086 of empty range to put a line through.

It used to print that number and throw it away, so a mark nobody could see shipped as quietly as one anybody could. It now says which band it landed in and warns when that is not the top one. It is a warning and not a refusal, because seven of the ten here are under the line and ship anyway — a gate that fails most of its own corpus is one somebody turns off.

The first attempt at those bands was wrong, and the way it was wrong is the reason this page exists. They were set at 0.30× and 0.20× — read off a contact sheet that prints the scale to two places. lstm prints as 0.30× and is really 0.2975, so the boundary put the lowest mark anyone had confirmed readable on the wrong side of its own line, by 0.0025. Every model still rendered and nothing said a word. Rounding a number for a person to read and thresholding it for a machine to decide are different jobs, and one value doing both silently does the second one badly.

Nothing re-rendered a committed mark either, until now. tube's moved from 0.15× to 0.20× when its wrap changed for the banner further up this page — a mark crossing a band because of an edit aimed somewhere else — and a person caught it against that same contact sheet. CI now re-renders every committed mark and reads the slot back out of the file, so a mark states the size it was fitted to and the check needs no constant of its own.

The mlp figure reduced to a textless mark at 192 by 96 pixels.

mlp 0.40× reads

Reads, and says nothing. Boxes and arrows — nothing in it identifies the tool that drew it.

The lenet figure reduced to a textless mark at 192 by 96 pixels.

lenet 0.39× reads

The best mark here, and the only one that is not plain rectangles.

The lstm figure reduced to a textless mark at 192 by 96 pixels.

lstm 0.30× reads

Reads, slightly busy where the wrapped return crosses back.

The dual figure reduced to a textless mark at 192 by 96 pixels.

dual 0.21× marginal

Legible with nothing near it. The merge is already soft.

The tube figure reduced to a textless mark at 192 by 96 pixels.

tube 0.20× marginal

The model in the banner above, and it does not survive the shrink.

The resnet figure reduced to a textless mark at 192 by 96 pixels.

resnet 0.17× does not read

Flecks and hairlines.

The transformer figure reduced to a textless mark at 192 by 96 pixels.

transformer 0.15× does not read

Flecks and hairlines.

The vae figure reduced to a textless mark at 192 by 96 pixels.

vae 0.14× does not read

Flecks and hairlines.

The unet figure reduced to a textless mark at 192 by 96 pixels.

unet 0.12× does not read

Dirt on the glass.

The whisper figure reduced to a textless mark at 192 by 96 pixels.

whisper 0.11× does not read

The figure this page opens with, and the lowest mark in the set: at 0.11× none of it survives.

LeNet wins because it loses the most. Its figure has nine stages and its mark keeps four: the image, the flatten, both dense layers and the class logits draw only text, so they go with the text. What is left reads as a bare conv stack and gives no hint the classifier head was ever there. That is a real loss of information, and it is exactly why the mark works — four large glyphs instead of nine cramped ones. As a figure it is wrong; as a mark it is the best of them. It is going on a card, so it is a mark.

One stated limit. --icon post-processes a rendered figure rather than re-solving the layout without labels, so boxes still carry the size of text nobody is drawing. The boxed nets pay for it and the glyph nets come out tight.

A transformer drawn as a long horizontal ribbon of named stages, with a labelled dashed skip and a block marked as a repeated unit.

lanes and repeat, and the reason wrap exists. The agent named the unit; draughtsman tiled it against the traced nodes and counted. Its 1,277 units put detail type at about 10px here, and holding the page's body size would take 2,285px — the ribbon problem stated as a number rather than complained about. It declares no print width at all, because at this width there is none that clears the floor.

It refuses figures that are wrong.

Coverage asks whether an operation was dropped. Each of these asks something else, and each exists because a figure was wrong while coverage was green: an arrow the trace does not contain, a tied weight counted twice, a repeat count nobody verified, a glyph whose axes come from different tensors, a name painted over its own drawing, type too small to read at the size it will be printed, and an arrow whose path runs through a stage it has nothing to do with.

The last one was found by looking. A figure in this gallery drew one branch's output straight through the other branch on its way to a concatenate, and there is no such path in the trace — the confident-and-wrong figure this project convicts other tools of, in its own examples. Coverage was green, the type cleared its floor, and the committed SVG was exactly what the spec produced. Nothing had ever asked where an edge's own path goes. tools/edge_collisions.py is that look, mechanised.

The half a machine cannot check.

A green check says no operation was dropped. It says nothing about whether the names are good, the grouping natural or the figure legible — and check ends by saying so, so a green check is never read as a good figure. draughtsman ui is where a person does the other half: the figure, the coverage panel and every traced node in one place, the grouping editable, the picture redrawing as you change it. It need not start from an agent's answer, or from nothing: abstract --by-module writes a spec grouped by the module each node ran in — every node placed, arrows from the trace, no number typed, nothing judged — and ui is where the judging happens. Nothing checks who wrote the spec.

Point it at a directory and All models renders every figure onto one sheet with its coverage state and aspect ratio — a layout defect in one model of many does not announce itself in a passing check, and opening a tab per model to find it is how it stays unfound. Every picture it shows comes from the same render() the CLI calls, so the figure you judge is the figure that ships.

A model for each way it could break.

How each model might break the tool was written down before it was traced. Whisper did, twice over — the two findings at the top of this page — and what they cost is in DECISIONS.md, which is the most useful file here. Every model is written out in full in the repository — no torchvision, no downloads, no pinned third-party version — with the graph.json it was measured from and the spec.json that arranged it, so the whole pipeline reproduces from a clone.

A dual-branch 1-D network: two parallel branches over one input, rejoining at a concat, then a gate and a conv head.

25,989 parameters. Solved to print at 6 inches — 8.92pt type, so it stays readable in a double column. This is the figure the edge check was written against.