docs(deck): 15 pt body, wider margins, and a layout audit that reports clean
Content box 0.70-12.70 -> 0.50-12.90, applied as an affine map over every module-level layout call. Interstitial thesis and list set at body size, with per-entry measured pitch. ~30 slide cuts: numbers and remarks stay, the sentence explaining them moves to notes(). Restores three code panels that had been reduced to bare list literals -- slide 33's frame-147 walkthrough, 34's recommended pattern, 15's barrier comparison. Figures re-laid wherever text met text (pinning, tile, resultpath, streams, arc, pagefault, pedtiming and others). New deckgate.py holds the projection floor and placement table so make_figs_kernel.py is gated too; its three figures had been set at 7-8 pt, unchecked. audit_layout.py splits lines at a column gutter and drops the overlap threshold to 10%: it was reporting every two-column slide and missing a bullet running under a code panel title. Deck now audits 0 collisions, 0 overruns, all figure text >= 10 pt.
@@ -0,0 +1,161 @@
|
||||
# 2026-08-27 — 15 pt body, wider margins, and a layout checker that works
|
||||
|
||||
Session log for `docs/cf_cuda_performance.pptx` (via `docs/deck/build_performance_deck.py`
|
||||
+ `make_figs.py` + `make_figs_kernel.py`). Deck rebuilt at **36 numbered slides**,
|
||||
56 pages. The rendered PDF now audits **clean**: zero overlapping text, zero text
|
||||
past the footer.
|
||||
|
||||
---
|
||||
|
||||
## 1. The content box widened to `[0.50, 12.90]`
|
||||
|
||||
It was `[0.70, 12.70]`. Narrowing the two outer margins buys 0.4 in of line
|
||||
length — about 3 % more characters per line at every size, and on the widest
|
||||
slides a whole line back.
|
||||
|
||||
It was applied as an **affine map** of x and w over every module-level layout
|
||||
call, not by hand: nothing changes proportion, columns keep their relative gap,
|
||||
and only the two margins give up space. `M`, `COL`, `RAIL_X` and `RAIL_W` moved
|
||||
with it, along with the geometry inside `chrome()`, `annex_chrome()`, `rail()`
|
||||
and `section()`, which the rewriter deliberately did not touch (inside a helper
|
||||
every x is an offset from a caller-supplied origin).
|
||||
|
||||
Three module-level loops carry a **stride** rather than an offset — the hero
|
||||
stat row, the s1/s4/floor cards, the closing four-card grid — and a stride is
|
||||
not an x. Those were the only sites that needed a hand edit, and skipping them
|
||||
would have overlapped the cards rather than moved them.
|
||||
|
||||
`deckgate.placements()` holds its own copy of the four tokens so it can evaluate
|
||||
placement expressions. **It was still holding the old values**, so for one build
|
||||
the legibility gate was checking widths no figure was placed at.
|
||||
|
||||
## 2. The interstitials read at body size
|
||||
|
||||
`section()`'s thesis went 13.5 → **16 pt** and its "coming up" list 12 → **15**.
|
||||
The list is the reason the divider exists, so it is set at body size, not
|
||||
caption size.
|
||||
|
||||
That costs height, so each entry is now **measured** rather than given a fixed
|
||||
step: a two-line entry gets two lines of room instead of sitting on the one
|
||||
below it. The pitch is `15 × 1.45 / 72`, not the `1.22` the nominal line spacing
|
||||
suggests — see §6.
|
||||
|
||||
## 3. Slide-by-slide, what came off
|
||||
|
||||
Roughly 30 specific cuts. The pattern in all of them: the numbers and the
|
||||
remarks stay, the sentence explaining why they are true moves to `notes()`.
|
||||
|
||||
- **3** — spectra caption cut to one clause; the closing line loses "not lit pixels".
|
||||
- **4** — second bullet and the pedestal-timing rail note removed; the timing
|
||||
argument (and its link to A7) is now a speaker note.
|
||||
- **5** — the Comet Lake die caption, which the picture already said.
|
||||
- **6** — "A stalled warp is simply replaced, never reordered."
|
||||
- **7** — right column shortened; the last callout had been sitting past the footer.
|
||||
- **8** — the shared-memory-size paragraph → notes; the tile diagram grew into it.
|
||||
- **9** — the register callout says the two cases and stops; rail note removed.
|
||||
- **10, 12, 16, 18, 20, 21, 23, 24, 26, 28, 29, 30, 31, 35, 36** — one box, one
|
||||
note or one repetitive sentence each. Slide 12 lost its second callout entirely
|
||||
(the per-engine numbers moved to slide 13, where they belong to a measured run
|
||||
rather than to a diagram) and its DMA footnote.
|
||||
- **21** — the s1 / s4 / floor cards are one-liners now, and the picture beneath
|
||||
them is 11.6 in wide instead of 10.6.
|
||||
- **35** — the guidance column halved; the header row had been *under* the warning
|
||||
callout, not below it.
|
||||
|
||||
## 4. Two code panels had been silently deleted
|
||||
|
||||
Slides 33 and 34 each contained a bare list literal where a `code(...)` call
|
||||
used to be — the `code(s, x, y, w,` prefix was gone and the list was evaluated
|
||||
and discarded. Slide 33 had lost the frame-147 walkthrough, which **is** the
|
||||
slide's argument, and 34 had lost "THE RECOMMENDED PATTERN", which is the API
|
||||
the slide exists to teach. Both restored; slide 33's now also shows cuda's
|
||||
`accept` branch, which had never been on the slide next to frozen's `reject`.
|
||||
|
||||
A third instance, on slide 15, had lost the opt2/opt3 barrier comparison.
|
||||
|
||||
## 5. The figures
|
||||
|
||||
Every figure was re-laid where text met text. The ones worth naming:
|
||||
|
||||
- **`fig_pinning`** was the worst: box labels wider than their boxes, arrow
|
||||
labels in a gutter narrower than the words in it. Boxes widened, arrow labels
|
||||
moved above the row.
|
||||
- **`fig_tile`** — `set_aspect("equal")` shrinks the axes box to honour the
|
||||
aspect, so the data-space room reserved under the tile for its key was being
|
||||
squeezed and the key clipped. The key is drawn in **figure** coordinates now.
|
||||
The two panels also sit 0.06 apart instead of 0.135, and the canvas is 6.9 in
|
||||
rather than 7.6 — a narrower canvas at the same placement width is a *bigger*
|
||||
projected figure.
|
||||
- **`fig_resultpath`** — the verdict is the second line of the axes **title**.
|
||||
As free-floating text it had nowhere to go: above the bars it met the value
|
||||
labels, below the axis it met two-line tick labels, beside them it met the
|
||||
gain. Bars also moved 1.35 apart, because the tick labels are ~0.95 x-units
|
||||
wide each.
|
||||
- **`fig_streams`** — the barrier label was in the axes title's row. An axes
|
||||
title is anchored to the axes *box*, so it moves with the limits and not with
|
||||
the data; the fix is headroom in y. "time →" came inside the axes, because as
|
||||
an xlabel it sat on the sentence already written there.
|
||||
- **`fig_arc`** — band labels shortened to `ACT I / II / III`. The gloss was
|
||||
wider than the one-bar ACT III band, so ACT III's label was on ACT II's.
|
||||
- **`fig_pagefault`** — the key moved below the two rows; the right-hand panel
|
||||
owns that half of the figure and "first touch → fault" is 18 x-units wide,
|
||||
which was exactly the clearance left between them.
|
||||
- **`fig_pedtiming`** — "every decision uses the frame-start snapshot" was at
|
||||
`y + 0.18`, sitting on the 2.2 pt trace it labels.
|
||||
- **`fig_cancellation`**, **`fig_f32_kernel`**, **`fig_gpu_model`**,
|
||||
**`fig_regpressure`**, **`fig_occupancy`**, **`fig_overlap_9x9`**,
|
||||
**`fig_first_run`**, **`fig_measure`**, **`fig_mismatch147`** — same class of
|
||||
fix: headroom, or a label moved off the mark it was naming.
|
||||
|
||||
## 6. Line height is not what the nominal spacing says
|
||||
|
||||
Two constants, both wrong in the direction that clips:
|
||||
|
||||
- `code()` sets `lh = 0.0189 * size` in/line for Consolas. The old 0.0174
|
||||
under-counted by 8.6 %.
|
||||
- LibreOffice sets a 15 pt UI line at about **0.31 in**, not the 0.26 that
|
||||
`line=1.25` implies. Every stack computed against 0.26 came out ~20 % short,
|
||||
which is one line on a five-line slide.
|
||||
|
||||
## 7. `deckgate.py`, and the kernel figures joining the gate
|
||||
|
||||
The projection floor, the placement table and the report moved into their own
|
||||
module. There are **two** figure generators and only `make_figs.py` was ever
|
||||
checked; `make_figs_kernel.py`'s three figures were consequently set at 7–8 pt
|
||||
and nobody found out until they were projected. All font sizes there went up
|
||||
×1.30 and the module now imports `deckgate`.
|
||||
|
||||
The floor itself is **10.0 pt**, two thirds of the 15 pt body.
|
||||
|
||||
A string may opt out with `gid="texture"` — for marks nobody is asked to READ,
|
||||
where the pattern is the message and the digits are shading. Two figures use it:
|
||||
`fig_mismatch147`'s per-pixel ADU values (a cell is ~0.18 in wide and a value can
|
||||
be three digits, so nothing that clears the floor can also fit) and `fig_frame`'s
|
||||
3×3 zoom. It is set per `Text` object, never per figure.
|
||||
|
||||
## 8. `audit_layout.py` now finds what it was built to find
|
||||
|
||||
Three changes, each of which had been hiding a real defect:
|
||||
|
||||
1. **Lines split at a column gutter.** Grouping words by baseline alone merges a
|
||||
rail row and a body bullet at the same height into one full-width "line".
|
||||
Two such lines overlap in x by construction, so the audit was reporting a
|
||||
collision on every two-column slide — 11 pages of noise that buried the four
|
||||
real ones.
|
||||
2. **Overlap threshold 25 % → 10 %.** At 25 % a bullet ran a whole word under a
|
||||
code panel title without registering. It did, on slide 17.
|
||||
3. **Two narrow exemptions**, so that a clean deck reports *clean* and the tool
|
||||
stays worth running: page 1 is the PSI template's own title slide, and a
|
||||
one-character "line" beside a real one is a superscript marker — which is what
|
||||
a footnote mark is.
|
||||
|
||||
Result: **0 collisions, 0 overruns**, from 34 pages with collisions at the start
|
||||
of the 15 pt migration.
|
||||
|
||||
## 9. Wrap-join repairs
|
||||
|
||||
Eleven more glued words from earlier bulk rewrites — `over 128SMs`,
|
||||
`thecount`, `opt1 andopt2`, `the hostadds`, `there,invisible`, `make thecolumns`,
|
||||
`costsnothing`, `whosetrue variance`, `mmapthreshold`, `at tol= 0` — where two
|
||||
adjacent Python string literals were joined with nothing between them. Python
|
||||
concatenates them silently; only a render shows it.
|
||||
@@ -12,6 +12,7 @@ are counted.
|
||||
python docs/deck/make_figs.py # figures -> docs/figures/*.png
|
||||
python docs/deck/make_figs_kernel.py # 3 more (fig_frame, fig_occupancy, fig_tile)
|
||||
python docs/deck/build_performance_deck.py
|
||||
python docs/deck/audit_layout.py <the rendered pdf> # must print "clean"
|
||||
|
||||
# the shareable copy, regenerated from the .pptx after every rebuild
|
||||
libreoffice --headless --convert-to pdf --outdir /tmp/pdfout \
|
||||
@@ -41,39 +42,87 @@ interpreter with all four is `/home/ferjao_k/.conda/envs/py/bin/python` — the
|
||||
the title slide, and every other slide of it is deleted at build time. Do not edit the
|
||||
generated `.pptx` by hand — it is overwritten on every build. Edit the script.
|
||||
|
||||
## The type scale
|
||||
|
||||
One place, `build_performance_deck.py`, because 186 scattered `size=` arguments cannot
|
||||
be reasoned about:
|
||||
|
||||
| token | pt | what it is for |
|
||||
|---|---|---|
|
||||
| `PT_BODY` | 15 | bullets: what the slide is claiming |
|
||||
| `PT_LEAD` | 13 | callouts: the sentence to remember, already boxed and bold |
|
||||
| `PT_RAIL` | 13 | rail row values |
|
||||
| `PT_TABLE` | 11.5 | table cells |
|
||||
| `PT_META` | 11 | captions and rail notes: provenance, deliberately quieter |
|
||||
| `PT_CODE` | 10.5 | code panels — read by token, not word by word |
|
||||
| `PT_LABEL` | 9.5 | small-caps labels on rails, tables, statstrips |
|
||||
|
||||
**Raising `PT_BODY` costs text.** 15 pt holds roughly 45 % of the characters 10.5 pt did
|
||||
in the same box, because area scales with the square of point size. The scale is a
|
||||
budget, not a preference: a slide that will not fit loses words, never type size.
|
||||
|
||||
Two line-height constants are calibrated against a LibreOffice render, not against the
|
||||
nominal spacing, and both were wrong in the direction that clips:
|
||||
|
||||
- `code()` sets `lh = 0.0189 * size` in/line for Consolas. The old 0.0174 under-counted
|
||||
by 8.6 %, which is invisible on a short panel and eats the last line or two of a long
|
||||
one.
|
||||
- LibreOffice sets a 15 pt UI line at about **0.31 in**, not the 0.26 that `line=1.25`
|
||||
implies. Budget 0.31 per body line when deciding what a slide can hold.
|
||||
|
||||
## Layout guarantees, and how they are enforced
|
||||
|
||||
Two invariants are checked mechanically, because both fail silently otherwise.
|
||||
Three invariants are checked mechanically, because all three fail silently otherwise.
|
||||
|
||||
**Nothing renders below 9 pt on the projected slide.** A figure's on-screen type size
|
||||
**Nothing renders below 10 pt on the projected slide.** A figure's on-screen type size
|
||||
is `raw_pt × (placement_width / figure_width)`, and neither factor is visible at the
|
||||
point where the font size is written. `make_figs.py` closes that loop: `_placements()`
|
||||
point where the font size is written. `deckgate.py` closes that loop: `placements()`
|
||||
parses the placement width of every figure **out of the deck script itself**, so the
|
||||
gate cannot drift from the layout it checks. Every run ends with either
|
||||
gate cannot drift from the layout it checks. Both generators import it — `make_figs.py`
|
||||
and `make_figs_kernel.py` — and each run ends with either
|
||||
|
||||
```
|
||||
legibility: every string in every figure renders at >= 9.0 pt on the slide.
|
||||
legibility: every string in every figure renders at >= 10.0 pt on the slide.
|
||||
```
|
||||
|
||||
or a list of offenders. Fix them; do not raise the floor. 9 pt on a 13.33 × 7.5 in
|
||||
slide is about 1/60 of slide height, which is the conventional bound for readable
|
||||
supporting detail at 6–7 m.
|
||||
or a list of offenders. Fix them; do not raise the floor. 10 pt is two thirds of the
|
||||
15 pt body, which is the usual lower bound for supporting type at 6–7 m.
|
||||
|
||||
The kernel figures were outside this gate for a long time and were set at 7–8 pt as a
|
||||
result. If you add a third generator, import `deckgate` from it on day one.
|
||||
|
||||
A string may opt out with `gid="texture"`, and only for marks nobody is asked to READ —
|
||||
a value printed into every cell of a pixel map, where the pattern is the message and
|
||||
the digits are shading. Two figures use it: `fig_mismatch147`'s per-pixel ADU values
|
||||
and `fig_frame`'s 3×3 zoom. Set it on the `Text` object, never on a figure.
|
||||
|
||||
Note the feedback trap: `savefig(bbox_inches="tight")` grows the saved canvas to fit a
|
||||
long in-figure caption, which shrinks the placement scale, which shrinks the caption.
|
||||
Raising the font size can make text *smaller*. Shorten the string or re-lay the axes.
|
||||
The corollary is useful: a NARROWER `figsize` at the same placement width is a bigger
|
||||
projected figure and bigger projected type.
|
||||
|
||||
**No text runs past the footer line.** Convert and check:
|
||||
**No text overlaps other text, and none runs past the footer.** Both are checked by
|
||||
`audit_layout.py`, which reads the rendered PDF's word boxes:
|
||||
|
||||
```bash
|
||||
libreoffice --headless --convert-to pdf --outdir /tmp/deck docs/cf_cuda_performance.pptx
|
||||
python scratch/overflow.py /tmp/deck/cf_cuda_performance.pdf
|
||||
python docs/deck/audit_layout.py /tmp/deck/cf_cuda_performance.pdf # -> "clean"
|
||||
```
|
||||
|
||||
Only page 1 may be flagged — that is the PSI template's own title slide. The same
|
||||
script counts unrendered `**` markup, which is the usual symptom of putting markup in a
|
||||
helper that does not parse it: `bullets`, `callout`, `table` and `code` understand
|
||||
`**bold**`; `caption` does not, and nothing understands backticks or `*italics*`.
|
||||
Three things about it are load-bearing:
|
||||
|
||||
- It groups words into lines by baseline and then **splits each line at a column
|
||||
gutter**. Without that split a rail row and a body bullet at the same height merge
|
||||
into one full-width "line", and every two-column slide in the deck reports a
|
||||
collision.
|
||||
- It asserts on the parsed word count. A broken bbox parser yields empty boxes and
|
||||
therefore a *clean* report, which is the worst failure mode a checker can have.
|
||||
- Two exemptions are deliberate and narrow: page 1 is the PSI template's own title
|
||||
slide, and a one-character "line" beside a real one is a superscript marker.
|
||||
|
||||
Overlap has to cover 10 % of the narrower line to register. At 25 % a bullet could run a
|
||||
whole word under a code-panel title without being reported, which it did.
|
||||
|
||||
## Numbering
|
||||
|
||||
@@ -89,6 +138,13 @@ corner and the same way `annex_chrome` draws `A<n>`. The badge names the *rung*,
|
||||
slide, so opt3, opt5 and opt7 repeat theirs across two slides each. When a slide has a
|
||||
badge its eyebrow must not also say "optN" — that reads twice.
|
||||
|
||||
**The content box is `[0.50, 12.90]` on a 13.333 in slide.** It used to be
|
||||
`[0.70, 12.70]`; narrowing the two outer margins bought 0.4 in of line length, which is
|
||||
about 3 % more characters per line at every size. The tokens that carry it are `M`,
|
||||
`COL`, `RAIL_X` and `RAIL_W`, and **`deckgate.placements()` holds copies of all four**
|
||||
so it can evaluate placement expressions. Change one and change the other, or the
|
||||
legibility gate starts checking widths no figure is placed at.
|
||||
|
||||
**Nothing on a slide may point outside the deck.** No notebook names, no
|
||||
`python/tests/…`, no result directories, no "see the write-up" — the slides get shared
|
||||
on their own, and a pointer to something the reader does not have is worse than no
|
||||
@@ -128,8 +184,10 @@ report: opt5 at 9×9, whose per-frame allocation never lets the fault count conv
|
||||
| file | role |
|
||||
|---|---|
|
||||
| `build_performance_deck.py` | the deck: tokens, helpers, every slide |
|
||||
| `make_figs.py` | most figures, plus the legibility gate |
|
||||
| `make_figs.py` | most figures |
|
||||
| `make_figs_kernel.py` | `fig_frame`, `fig_occupancy`, `fig_tile` |
|
||||
| `deckgate.py` | the projection floor and the placement table, shared by both generators |
|
||||
| `audit_layout.py` | reads the rendered PDF: overlapping text, and text past the footer |
|
||||
| `QA.md` | questions the room asks, with the answers and where they are settled |
|
||||
| `frame147.json`, `validation_tiers.json` | measured data two figures read |
|
||||
| `branch_site.json` | the A7 site dump; written by `python/tests/branch_site_dump.py` |
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
"""Does any text on a slide physically overlap any other text?
|
||||
|
||||
The overflow checker only sees text crossing the footer line. It cannot see a
|
||||
bullet running into the code panel underneath it, which is exactly the failure a
|
||||
type-size change produces. This reads the RENDERED pdf and reports every pair of
|
||||
text lines whose bounding boxes actually intersect.
|
||||
|
||||
python docs/deck/audit_layout.py docs/cf_cuda_performance.pdf
|
||||
|
||||
No column heuristics: two lines are a collision only if their boxes overlap in
|
||||
BOTH axes, so side-by-side columns and consecutive baselines never register.
|
||||
"""
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
|
||||
PDF = sys.argv[1] if len(sys.argv) > 1 else "docs/cf_cuda_performance.pdf"
|
||||
FOOTER = 525.5 # the progress track; nothing may sit below it
|
||||
LINE_TOL = 2.5 # words whose baselines are this close share a line
|
||||
X_FRAC = 0.10 # overlap must cover this much of the narrower line
|
||||
# (0.25 let a bullet run a whole word under a code
|
||||
# panel title without registering)
|
||||
Y_MIN = 2.0 # ... and this many points vertically
|
||||
GUTTER = 24.0 # white space this wide separates two columns
|
||||
|
||||
ATTR = re.compile(r'(\w+)="([\d.eE+-]+)"')
|
||||
|
||||
xml = subprocess.run(["pdftotext", "-bbox", PDF, "-"],
|
||||
capture_output=True, text=True).stdout
|
||||
|
||||
pages, cur = [], None
|
||||
for raw in xml.split("\n"):
|
||||
if "<page " in raw:
|
||||
cur = []
|
||||
pages.append(cur)
|
||||
elif "<word " in raw and cur is not None:
|
||||
# A hand-rolled split on quotes silently yields an empty dict here; the
|
||||
# audit then reports a clean deck because it parsed nothing at all.
|
||||
g = dict(ATTR.findall(raw.split("<word ")[1].split(">")[0]))
|
||||
if {"xMin", "yMin", "xMax", "yMax"} <= g.keys():
|
||||
cur.append((float(g["xMin"]), float(g["yMin"]),
|
||||
float(g["xMax"]), float(g["yMax"]),
|
||||
raw.split(">", 1)[1].split("</word>")[0]))
|
||||
|
||||
|
||||
def lines_of(words):
|
||||
"""Group words into lines, and SPLIT a line at a column gutter.
|
||||
|
||||
Grouping by baseline alone merges a rail row and a body bullet that happen
|
||||
to sit at the same height into one full-width "line". Two such lines then
|
||||
overlap in x by construction, and the audit reports a collision on every
|
||||
two-column slide in the deck -- which is most of them. A run of words
|
||||
separated by more than a third of an inch of white space is a different
|
||||
column, not the same line.
|
||||
"""
|
||||
rows = defaultdict(list)
|
||||
for w in words:
|
||||
rows[round(w[3] / LINE_TOL)].append(w)
|
||||
out = []
|
||||
for k in sorted(rows):
|
||||
run = []
|
||||
for w in sorted(rows[k]):
|
||||
if run and w[0] - run[-1][2] > GUTTER:
|
||||
out.append(_span(run))
|
||||
run = []
|
||||
run.append(w)
|
||||
if run:
|
||||
out.append(_span(run))
|
||||
return out
|
||||
|
||||
|
||||
def _span(ws):
|
||||
return (min(w[0] for w in ws), min(w[1] for w in ws),
|
||||
max(w[2] for w in ws), max(w[3] for w in ws),
|
||||
" ".join(w[4] for w in ws))
|
||||
|
||||
|
||||
# Page 1 is the PSI template's own title slide. Its layout is not ours to fix --
|
||||
# the deck imports it whole -- and it puts the date below our footer line.
|
||||
TEMPLATE_PAGE = 1
|
||||
|
||||
overlaps, footer = [], []
|
||||
for pno, words in enumerate(pages, 1):
|
||||
ls = lines_of(words)
|
||||
for i, a in enumerate(ls):
|
||||
if a[3] > FOOTER and pno != TEMPLATE_PAGE:
|
||||
footer.append((pno, a[3], a[4]))
|
||||
for b in ls[i + 1:]:
|
||||
# A one-character "line" beside a real one is a superscript marker
|
||||
# sharing the baseline it annotates. That is what a footnote mark IS.
|
||||
if len(a[4]) == 1 or len(b[4]) == 1:
|
||||
continue
|
||||
ox = min(a[2], b[2]) - max(a[0], b[0])
|
||||
oy = min(a[3], b[3]) - max(a[1], b[1])
|
||||
if oy < Y_MIN or ox <= 0:
|
||||
continue
|
||||
if ox >= X_FRAC * min(a[2] - a[0], b[2] - b[0]):
|
||||
overlaps.append((pno, ox, oy, a[4][:44], b[4][:44]))
|
||||
|
||||
nw = sum(len(p_) for p_ in pages)
|
||||
assert nw > 500, f"parsed only {nw} words -- the bbox parser is broken, not the deck"
|
||||
print(f"pages: {len(pages)} words parsed: {nw}\n")
|
||||
print(f"COLLIDING text: {len(overlaps)}")
|
||||
seen = set()
|
||||
for pno, ox, oy, a, b in overlaps:
|
||||
if pno in seen and len([o for o in overlaps if o[0] == pno]) > 3:
|
||||
continue
|
||||
seen.add(pno)
|
||||
print(f" p{pno:>3} x{ox:5.1f} y{oy:5.1f} {a!r}\n {b!r}")
|
||||
n_pages = len({o[0] for o in overlaps})
|
||||
print(f" ({n_pages} pages affected)")
|
||||
|
||||
print(f"\nBELOW THE FOOTER: {len(footer)}")
|
||||
for pno, y, txt in footer:
|
||||
print(f" p{pno:>3} y {y:6.1f} {txt[:70]!r}")
|
||||
|
||||
print("\nclean" if not (overlaps or footer) else "\nFIX THE ABOVE")
|
||||
sys.exit(1 if (overlaps or footer) else 0)
|
||||
@@ -0,0 +1,100 @@
|
||||
"""The projection floor, and the one thing both figure generators must share.
|
||||
|
||||
A figure's on-screen type size is `raw_pt x (placement_width / figure_width)`,
|
||||
and neither factor is visible where the font size is written. This closes that
|
||||
loop by reading the placement width out of the deck script itself, so the gate
|
||||
cannot drift from the layout it checks.
|
||||
|
||||
It lives in its own module because there are TWO generators -- make_figs.py and
|
||||
make_figs_kernel.py -- and for a long time only the first was checked. The
|
||||
kernel figures were consequently set at 7-8 pt and nobody found out until they
|
||||
were projected.
|
||||
"""
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib
|
||||
|
||||
# The floor, in points, on the projected slide. Set for a room where the back
|
||||
# row is 6-7 m away: on a 13.33 x 7.5 in slide, 10 pt is two thirds of the 15 pt
|
||||
# body size, which is the usual lower bound for supporting type. Below that a
|
||||
# label inside a plot reads as a footnote rather than part of the argument.
|
||||
MIN_EFF_PT = 10.0
|
||||
|
||||
# A string may opt out by carrying gid="texture". That is for marks nobody is
|
||||
# asked to READ -- a value printed into every cell of a pixel map, where the
|
||||
# pattern is the message and the digits are shading. It is set per Text object,
|
||||
# never per figure, so an exemption stays a decision about one label.
|
||||
EXEMPT_GID = "texture"
|
||||
|
||||
DECK = Path(__file__).resolve().parent / "build_performance_deck.py"
|
||||
VIOLATIONS = []
|
||||
|
||||
|
||||
def placements():
|
||||
"""{figure name: the narrowest width the deck places it at}.
|
||||
|
||||
The token values must track build_performance_deck.py. They did not when the
|
||||
margins were narrowed, and the gate then silently checked widths no figure
|
||||
was ever placed at.
|
||||
"""
|
||||
if not DECK.exists():
|
||||
return {}
|
||||
env = {"M": 0.50, "COL": 8.16, "RAIL_W": 3.62, "RAIL_X": 9.28,
|
||||
"W": 13.333, "H": 7.5}
|
||||
out = {}
|
||||
for m in re.finditer(r'\b(?:card_)?figure\(s,\s*"([a-z0-9_]+)",([^)]*)\)',
|
||||
DECK.read_text()):
|
||||
args = [a.strip() for a in m.group(2).split(",")]
|
||||
if len(args) < 3:
|
||||
continue
|
||||
try:
|
||||
w = float(eval(args[2], {}, env))
|
||||
except Exception:
|
||||
continue
|
||||
out[m.group(1)] = min(out.get(m.group(1), 99.0), w)
|
||||
return out
|
||||
|
||||
|
||||
PLACE_W = placements()
|
||||
|
||||
|
||||
def check(fig, path, name, dpi, place_w=None):
|
||||
"""Report every string in `fig` that will project below the floor.
|
||||
|
||||
Call it BEFORE closing the figure and AFTER saving it: the saved width is
|
||||
what sets the scale, and `bbox_inches="tight"` means that width is not the
|
||||
figsize.
|
||||
"""
|
||||
from PIL import Image
|
||||
|
||||
texts = [(t.get_text(), t.get_fontsize())
|
||||
for t in fig.findobj(matplotlib.text.Text)
|
||||
if t.get_text().strip() and t.get_visible()
|
||||
and t.get_gid() != EXEMPT_GID]
|
||||
if place_w is None:
|
||||
place_w = PLACE_W.get(name)
|
||||
if place_w is None:
|
||||
print("wrote", name)
|
||||
return
|
||||
pw = Image.open(path).size[0] / dpi
|
||||
scale = place_w / pw
|
||||
bad = sorted({(round(sz * scale, 2), round(sz, 1), txt[:44].replace("\n", " "))
|
||||
for txt, sz in texts if sz * scale < MIN_EFF_PT - 0.05})
|
||||
print(f"wrote {name:22s} {pw:5.2f} in -> {place_w:5.2f} in "
|
||||
f"(x{scale:.3f}) min eff "
|
||||
f"{min((sz * scale for _, sz in texts), default=99):.1f} pt")
|
||||
for eff, raw, txt in bad:
|
||||
VIOLATIONS.append((name, eff, raw, txt))
|
||||
print(f" ILLEGIBLE {eff:5.2f} pt (set {raw:4.1f}) {txt!r}")
|
||||
|
||||
|
||||
def report():
|
||||
if not VIOLATIONS:
|
||||
print(f"\nlegibility: every string in every figure renders at "
|
||||
f">= {MIN_EFF_PT} pt on the slide.")
|
||||
return
|
||||
print(f"\nlegibility: {len(VIOLATIONS)} strings below {MIN_EFF_PT} pt "
|
||||
f"in {len({v[0] for v in VIOLATIONS})} figures")
|
||||
for name, eff, raw, txt in sorted(VIOLATIONS):
|
||||
print(f" {name:22s} {eff:5.2f} pt {txt!r}")
|
||||
@@ -17,6 +17,9 @@ from matplotlib.colors import LinearSegmentedColormap
|
||||
from matplotlib.patches import Rectangle, FancyArrowPatch
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import deckgate # noqa: E402 -- the projection floor, shared with make_figs.py
|
||||
|
||||
OUT = Path(__file__).resolve().parent.parent / "figures"
|
||||
OUT.mkdir(exist_ok=True)
|
||||
|
||||
@@ -30,7 +33,7 @@ TEXT2 = "#A5B2C4"
|
||||
MUTED = "#6B7A90"
|
||||
|
||||
plt.rcParams.update({
|
||||
"font.family": "DejaVu Sans", "font.size": 9,
|
||||
"font.family": "DejaVu Sans", "font.size": 11.7,
|
||||
"text.color": PALE, "axes.labelcolor": TEXT2,
|
||||
"xtick.color": TEXT2, "ytick.color": TEXT2,
|
||||
"axes.edgecolor": RULE, "axes.facecolor": "none",
|
||||
@@ -43,11 +46,18 @@ CMAP = LinearSegmentedColormap.from_list(
|
||||
"deck", ["#080C12", "#10202F", ACCENT, AMBER, "#FFF3DC"])
|
||||
|
||||
|
||||
DPI = 220
|
||||
|
||||
|
||||
def save(fig, name):
|
||||
fig.savefig(OUT / f"{name}.png", dpi=220, transparent=False,
|
||||
"""Write the PNG, then hold it to the SAME projection floor make_figs.py
|
||||
uses. These three figures were unchecked for a long time and were set at
|
||||
7-8 pt as a result -- roughly 8 pt on the screen, half the body size."""
|
||||
path = OUT / f"{name}.png"
|
||||
fig.savefig(path, dpi=DPI, transparent=False,
|
||||
bbox_inches="tight", pad_inches=0.08)
|
||||
deckgate.check(fig, path, name, DPI)
|
||||
plt.close(fig)
|
||||
print("wrote", name)
|
||||
|
||||
|
||||
def bare(ax, keep=("left", "bottom")):
|
||||
@@ -77,11 +87,11 @@ def fig_frame():
|
||||
for s in ax.spines.values():
|
||||
s.set_color(RULE)
|
||||
ax.set_title("one frame, pedestal subtracted · 150×150 crop",
|
||||
color=MUTED, fontsize=8, pad=7)
|
||||
color=MUTED, fontsize=10.4, pad=7)
|
||||
cb = fig.colorbar(im, ax=ax, fraction=0.045, pad=0.02)
|
||||
cb.outline.set_edgecolor(RULE)
|
||||
cb.ax.tick_params(labelsize=7, color=RULE)
|
||||
cb.set_label("ADU above pedestal", color=MUTED, fontsize=7.5)
|
||||
cb.ax.tick_params(labelsize=10.0, color=RULE)
|
||||
cb.set_label("ADU above pedestal", color=MUTED, fontsize=9.8)
|
||||
|
||||
# Pick a clean, well-isolated charge-sharing event: a local maximum of
|
||||
# moderate amplitude whose 3×3 core carries the charge and whose
|
||||
@@ -118,14 +128,19 @@ def fig_frame():
|
||||
for dy in (-1, 0, 1):
|
||||
for dx in (-1, 0, 1):
|
||||
v = win[4 + dy, 4 + dx]
|
||||
# A zoom cell is ~0.25 in wide and a value can be four digits, so
|
||||
# these cannot clear the projection floor without colliding. They
|
||||
# are texture: the argument is that the charge is SPREAD, and the
|
||||
# 3x3 sum that proves it is set below the panel at full size.
|
||||
ax2.text(4 + dx, 4 + dy, f"{v:.0f}", ha="center", va="center",
|
||||
color=BG if v > 500 else PALE, fontsize=7,
|
||||
gid="texture",
|
||||
color=BG if v > 500 else PALE, fontsize=6.4,
|
||||
fontweight="bold" if dx == 0 and dy == 0 else "normal",
|
||||
zorder=6)
|
||||
ax2.set_title("9×9 zoom on one hit", color=MUTED, fontsize=8, pad=7)
|
||||
ax2.set_title("9×9 zoom on one hit", color=MUTED, fontsize=10.4, pad=7)
|
||||
ax2.text(4, 9.2, f"3×3 sum = {win[3:6, 3:6].sum():.0f} ADU — one photon.\n"
|
||||
"The peak pixel holds only part of the charge.",
|
||||
ha="center", va="top", color=TEXT2, fontsize=7.5)
|
||||
ha="center", va="top", color=TEXT2, fontsize=9.8)
|
||||
save(fig, "fig_frame")
|
||||
|
||||
|
||||
@@ -133,11 +148,19 @@ def fig_frame():
|
||||
def fig_tile():
|
||||
B, r = 16, 1 # 16×16 block, 3×3 cluster → 1-px halo
|
||||
n = B + 2 * r
|
||||
fig = plt.figure(figsize=(7.6, 3.1))
|
||||
# 6.9 in wide, not 7.6: the deck places this at 8.36 in, so a narrower
|
||||
# canvas is a LARGER projected figure and larger projected type. The two
|
||||
# panels sit 0.06 apart rather than 0.135, which is the gap the slide was
|
||||
# paying for twice -- once inside the picture, once beside it.
|
||||
fig = plt.figure(figsize=(6.9, 2.62))
|
||||
|
||||
ax = fig.add_axes([0.0, 0.0, 0.44, 1.0])
|
||||
# The tile axes holds ONLY the tile. With set_aspect("equal") matplotlib
|
||||
# shrinks the axes box to honour the aspect, so any data-space room reserved
|
||||
# underneath for a legend gets squeezed and the legend is clipped. The key
|
||||
# is drawn in figure coordinates instead, where nothing can take it back.
|
||||
ax = fig.add_axes([0.015, 0.30, 0.44, 0.70])
|
||||
ax.set_aspect("equal"); ax.axis("off")
|
||||
ax.set_xlim(-0.6, n + 0.6); ax.set_ylim(-3.2, n + 1.3)
|
||||
ax.set_xlim(-0.6, n + 0.6); ax.set_ylim(-0.6, n + 1.5)
|
||||
for i in range(n):
|
||||
for j in range(n):
|
||||
halo = i < r or j < r or i >= n - r or j >= n - r
|
||||
@@ -153,78 +176,88 @@ def fig_tile():
|
||||
ax.add_patch(Rectangle((tj + r, n - 1 - (ti + r)), 0.92, 0.92,
|
||||
facecolor=AMBER, edgecolor="none"))
|
||||
ax.text(n / 2, n + 0.45, "shared-memory tile · 18 × 18",
|
||||
ha="center", color=MUTED, fontsize=8)
|
||||
for y, c, t in [(-1.05, AMBER, "the thread's own pixel"),
|
||||
(-1.85, ACCENT, "its 3×3 neighbourhood"),
|
||||
(-2.65, RULE, "halo — loaded, never centred on")]:
|
||||
ax.add_patch(Rectangle((0, y), 0.7, 0.36, facecolor=c, edgecolor="none"))
|
||||
ax.text(1.0, y + 0.18, t, va="center", color=TEXT2, fontsize=7.5)
|
||||
ha="center", color=MUTED, fontsize=10.4)
|
||||
for k, (c, t) in enumerate([(AMBER, "the thread's own pixel"),
|
||||
(ACCENT, "its 3×3 neighbourhood"),
|
||||
(RULE, "halo — loaded, never centred on")]):
|
||||
fy = 0.20 - k * 0.095
|
||||
fig.patches.append(Rectangle((0.020, fy - 0.018), 0.022, 0.042,
|
||||
transform=fig.transFigure, facecolor=c,
|
||||
edgecolor="none", zorder=5))
|
||||
fig.text(0.055, fy, t, va="center", color=TEXT2, fontsize=9.8)
|
||||
|
||||
# right: tile cost vs cluster size
|
||||
ax2 = fig.add_axes([0.575, 0.20, 0.40, 0.62])
|
||||
ax2 = fig.add_axes([0.52, 0.20, 0.46, 0.62])
|
||||
labels = ["3×3\n18×18", "5×5\n20×20", "7×7\n22×22", "9×9\n24×24"]
|
||||
kb = [(16 + 2 * (k // 2)) ** 2 * 4 / 1024 for k in (3, 5, 7, 9)]
|
||||
ax2.bar(np.arange(4), kb, width=0.55, color=ACCENT, zorder=3)
|
||||
for i, v in enumerate(kb):
|
||||
ax2.text(i, v + 0.12, f"{v:.1f}", ha="center", color=PALE, fontsize=9,
|
||||
ax2.text(i, v + 0.12, f"{v:.1f}", ha="center", color=PALE, fontsize=11.7,
|
||||
fontweight="bold")
|
||||
ax2.axhline(100, color=PALE, lw=1.2, ls="--")
|
||||
ax2.set_xticks(np.arange(4)); ax2.set_xticklabels(labels, color=TEXT2,
|
||||
fontsize=8)
|
||||
ax2.set_ylim(0, 3.4); ax2.set_yticks([])
|
||||
fontsize=10.4)
|
||||
ax2.set_ylim(0, 4.4); ax2.set_yticks([])
|
||||
bare(ax2, keep=("bottom",))
|
||||
ax2.set_title("KB of shared memory per 16×16 block (float tile)",
|
||||
color=MUTED, fontsize=8, pad=8)
|
||||
ax2.text(3.55, 3.15, "100 KB available per SM on Ada\n"
|
||||
"— shared memory is never the limit",
|
||||
ha="right", va="top", color=PALE, fontsize=7.5)
|
||||
color=MUTED, fontsize=10.4, pad=8)
|
||||
ax2.text(3.55, 4.3, "100 KB available per SM on Ada\n"
|
||||
"— shared memory is never the limit",
|
||||
ha="right", va="top", color=PALE, fontsize=9.8)
|
||||
save(fig, "fig_tile")
|
||||
|
||||
|
||||
# ------------------------------------------------- 3. occupancy / registers
|
||||
def fig_occupancy():
|
||||
fig, (ax, ax2) = plt.subplots(1, 2, figsize=(11.2, 2.95),
|
||||
gridspec_kw={"width_ratios": [1, 1.5]})
|
||||
# 1.7, not 1.5: the right panel carries three two-line tick labels and they
|
||||
# were touching. Widening the panel is the fix; shrinking the labels below
|
||||
# the projection floor is not.
|
||||
fig, (ax, ax2) = plt.subplots(1, 2, figsize=(11.6, 2.95),
|
||||
gridspec_kw={"width_ratios": [1, 1.7]})
|
||||
|
||||
# left — registers set the occupancy, per cluster size
|
||||
occ = [100.0, 33.3]
|
||||
ax.bar([0, 1], occ, width=0.5, color=[ACCENT, AMBER], zorder=3)
|
||||
for i, o in enumerate(occ):
|
||||
ax.text(i, o + 3, f"{o:.0f}%", ha="center", color=PALE, fontsize=12,
|
||||
ax.text(i, o + 3, f"{o:.0f}%", ha="center", color=PALE, fontsize=15.6,
|
||||
fontweight="bold")
|
||||
ax.set_xticks([0, 1])
|
||||
ax.set_xticklabels(["3×3 cluster\n38 regs/thread · 6 blocks/SM",
|
||||
"9×9 cluster\n128 regs/thread · 2 blocks/SM"],
|
||||
color=TEXT2, fontsize=8.5)
|
||||
# Three short lines, not two long ones: the left panel is a third of the
|
||||
# figure and the two labels were running into each other.
|
||||
ax.set_xticklabels(["3×3 cluster\n38 regs/thread\n6 blocks/SM",
|
||||
"9×9 cluster\n128 regs/thread\n2 blocks/SM"],
|
||||
color=TEXT2, fontsize=10.4)
|
||||
ax.set_ylim(0, 122); ax.set_yticks([])
|
||||
bare(ax, keep=("bottom",))
|
||||
ax.set_title("achieved occupancy, 16×16 block · f32 build", color=MUTED,
|
||||
fontsize=8.5, pad=8)
|
||||
fontsize=11.1, pad=8)
|
||||
|
||||
# right — block-size sweep, both cluster sizes
|
||||
o3 = [100.0, 100.0, 66.7]
|
||||
o9 = [33.3, 33.3, 0.0]
|
||||
halo3 = [56, 27, 13]
|
||||
blocks = [f"{b}\nhalo +{h}% of the tile"
|
||||
for b, h in zip(["8×8 · 64 threads", "16×16 · 256 threads",
|
||||
"32×32 · 1024 threads"], halo3)]
|
||||
blocks = [f"{b}\n{t}\nhalo +{h}%"
|
||||
for b, t, h in zip(["8×8", "16×16", "32×32"],
|
||||
["64 threads", "256 threads", "1024 threads"],
|
||||
halo3)]
|
||||
x = np.arange(3); w = 0.34
|
||||
ax2.bar(x - w / 2, o3, width=w, color=ACCENT, zorder=3, label="3×3 cluster")
|
||||
ax2.bar(x + w / 2, o9, width=w, color=AMBER, zorder=3, label="9×9 cluster")
|
||||
for xi, (a, b) in enumerate(zip(o3, o9)):
|
||||
ax2.text(xi - w / 2, a + 3, f"{a:.0f}%", ha="center", color=TEXT2,
|
||||
fontsize=8.5)
|
||||
fontsize=11.1)
|
||||
ax2.text(xi + w / 2, b + 3,
|
||||
("will not launch\n(registers)" if b == 0 else f"{b:.0f}%"),
|
||||
ha="center", va="bottom", color=AMBER if b == 0 else TEXT2,
|
||||
fontsize=8 if b == 0 else 8.5,
|
||||
ha="left" if b == 0 else "center", va="bottom",
|
||||
color=AMBER if b == 0 else TEXT2,
|
||||
fontsize=10.4 if b == 0 else 11.1,
|
||||
fontweight="bold" if b == 0 else "normal")
|
||||
ax2.set_xticks(x); ax2.set_xticklabels(blocks, color=TEXT2, fontsize=8.5)
|
||||
ax2.set_ylim(0, 122); ax2.set_yticks([])
|
||||
ax2.set_xticks(x); ax2.set_xticklabels(blocks, color=TEXT2, fontsize=9.8)
|
||||
ax2.set_ylim(0, 122); ax2.set_xlim(-0.6, 2.85); ax2.set_yticks([])
|
||||
bare(ax2, keep=("bottom",))
|
||||
ax2.legend(frameon=False, fontsize=8.5, labelcolor=TEXT2, loc="upper right")
|
||||
ax2.legend(frameon=False, fontsize=11.1, labelcolor=TEXT2, loc="upper right")
|
||||
ax2.set_title("occupancy vs block size (halo overhead quoted for 3×3)",
|
||||
color=MUTED, fontsize=8.5, pad=8)
|
||||
color=MUTED, fontsize=11.1, pad=8)
|
||||
fig.subplots_adjust(bottom=0.26)
|
||||
save(fig, "fig_occupancy")
|
||||
|
||||
@@ -234,3 +267,4 @@ if __name__ == "__main__":
|
||||
fig_occupancy()
|
||||
fig_frame()
|
||||
print("done ->", OUT)
|
||||
deckgate.report()
|
||||
|
||||
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 181 KiB After Width: | Height: | Size: 215 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 94 KiB |