Files
aare/docs/deck/make_figs_kernel.py
T
kferjaoui 59d8ed8a0c
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
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.
2026-08-27 19:28:11 +02:00

271 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Figures for the fused deck — kernel/algorithm/occupancy half.
Same palette and conventions as make_figs.py (deck palette, dark, tight bbox).
Writes into docs/figures/ alongside the optimization figures.
Occupancy numbers are not hand-computed: they come from
cudaOccupancyMaxActiveBlocksPerMultiprocessor + cudaFuncGetAttributes on the
real kernel (RTX 4090, sm_89), measured 2026-08-11 — see the table in
build_performance_deck.py.
"""
import sys
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
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)
BG = "#0B1018"
PANEL = "#121A28"
RULE = "#1E2836"
ACCENT = "#1E90C2"
AMBER = "#E8B25C"
PALE = "#E7EDF4"
TEXT2 = "#A5B2C4"
MUTED = "#6B7A90"
plt.rcParams.update({
"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",
"figure.facecolor": BG, "savefig.facecolor": BG,
"axes.grid": False, "svg.fonttype": "none",
})
# deck-native sequential map: background → accent → amber → white-hot
CMAP = LinearSegmentedColormap.from_list(
"deck", ["#080C12", "#10202F", ACCENT, AMBER, "#FFF3DC"])
DPI = 220
def save(fig, name):
"""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)
def bare(ax, keep=("left", "bottom")):
for s in ("top", "right", "left", "bottom"):
ax.spines[s].set_visible(s in keep)
# ------------------------------------------------ 1. what a frame looks like
def fig_frame():
"""Pedestal-subtracted MOENCH frame + a zoom on one real 3×3 cluster."""
sys.path.append("/home/ferjao_k/aare/build")
from aare import File
base = Path("/mnt/sls_det_storage/moench_data/2603_MaxIVBeamtime/"
"2026032408/process/xrf/")
pd = File(base / "Cu_factor_10_pedestal_master_0.json")
ped = np.mean([np.asarray(pd.read_frame(), dtype=np.float64)
for _ in range(200)], axis=0)
f = File(base / "Cu_factor_10_data_master_0.json")
frame = np.asarray(f.read_frame(), dtype=np.float64) - ped
crop = frame[40:190, 40:190]
fig = plt.figure(figsize=(7.4, 3.25))
ax = fig.add_axes([0.0, 0.02, 0.44, 0.94])
im = ax.imshow(crop, cmap=CMAP, vmin=-40, vmax=1200, interpolation="nearest")
ax.set_xticks([]); ax.set_yticks([])
for s in ax.spines.values():
s.set_color(RULE)
ax.set_title("one frame, pedestal subtracted · 150×150 crop",
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=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
# surrounding ring is quiet — i.e. what the algorithm is designed to find.
best, win = None, None
for r in range(6, frame.shape[0] - 6):
for c in range(6, frame.shape[1] - 6):
v = frame[r, c]
if not (500 < v < 2000):
continue
w = frame[r - 4:r + 5, c - 4:c + 5]
if w.max() > v: # must be the local max
continue
core = w[3:6, 3:6]
ring = np.concatenate([w[:3].ravel(), w[6:].ravel(),
w[3:6, :3].ravel(), w[3:6, 6:].ravel()])
if ring.max() > 80: # neighbourhood must be quiet
continue
share = (core.sum() - v) / core.sum() # charge outside the peak
if best is None or share > best[0]:
best, win = (share, r, c), w
if win is None: # fallback: brightest pixel
inner = frame[6:-6, 6:-6]
r, c = np.unravel_index(np.argmax(inner), inner.shape)
win = frame[r + 2:r + 11, c + 2:c + 11]
ax2 = fig.add_axes([0.60, 0.10, 0.30, 0.78])
ax2.imshow(win, cmap=CMAP, vmin=-40, vmax=1200, interpolation="nearest")
ax2.set_xticks([]); ax2.set_yticks([])
for s in ax2.spines.values():
s.set_color(RULE)
ax2.add_patch(Rectangle((2.5, 2.5), 3, 3, fill=False, edgecolor=PALE,
lw=1.8, zorder=5))
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",
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=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=9.8)
save(fig, "fig_frame")
# --------------------------------------------- 2. shared-memory tile + halo
def fig_tile():
B, r = 16, 1 # 16×16 block, 3×3 cluster → 1-px halo
n = B + 2 * r
# 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))
# 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(-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
ax.add_patch(Rectangle((j, n - 1 - i), 0.92, 0.92,
facecolor=RULE if halo else "#17394F",
edgecolor="none"))
# one thread's 3×3 neighbourhood
ti, tj = 6, 5
for di in (-1, 0, 1):
for dj in (-1, 0, 1):
ax.add_patch(Rectangle((tj + r + dj, n - 1 - (ti + r + di)), 0.92,
0.92, facecolor=ACCENT, edgecolor="none"))
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=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.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=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=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=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():
# 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=15.6,
fontweight="bold")
ax.set_xticks([0, 1])
# 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=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}\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=11.1)
ax2.text(xi + w / 2, b + 3,
("will not launch\n(registers)" if b == 0 else f"{b:.0f}%"),
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=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=11.1, labelcolor=TEXT2, loc="upper right")
ax2.set_title("occupancy vs block size (halo overhead quoted for 3×3)",
color=MUTED, fontsize=11.1, pad=8)
fig.subplots_adjust(bottom=0.26)
save(fig, "fig_occupancy")
if __name__ == "__main__":
fig_tile()
fig_occupancy()
fig_frame()
print("done ->", OUT)
deckgate.report()