docs(deck): 15 pt body, wider margins, and a layout audit that reports clean
Build on RHEL8 / build (push) Successful in 3m29s
Build on RHEL9 / build (push) Successful in 3m36s
Run tests using data on local RHEL8 / build (push) Successful in 4m13s

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.
This commit is contained in:
kferjaoui
2026-08-27 19:28:11 +02:00
parent bc6121ba5f
commit 59d8ed8a0c
35 changed files with 1591 additions and 1265 deletions
Binary file not shown.
Binary file not shown.
+161
View File
@@ -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 78 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.
+73 -15
View File
@@ -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 67 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 67 m.
The kernel figures were outside this gate for a long time and were set at 78 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` |
+119
View File
@@ -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)
File diff suppressed because it is too large Load Diff
+100
View File
@@ -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}")
+344 -350
View File
File diff suppressed because it is too large Load Diff
+77 -43
View File
@@ -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()
Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 94 KiB