Skip to content

Commit 6acaa01

Browse files
awfDouglasOrr
andauthored
Add stochastic rounding blog post and demos (#18)
Co-authored-by: Douglas Orr <douglaso@graphcore.ai>
1 parent 98cecb5 commit 6acaa01

29 files changed

Lines changed: 4962 additions & 0 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@
88
*DS_Store*
99

1010
/site
11+
/tmp

mkdocs.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ nav:
1111

1212
docs_dir: pages
1313

14+
exclude_docs: |
15+
posts/**/build-scripts/**
16+
1417
theme:
1518
name: material
1619
custom_dir: assets
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Regenerating assets
2+
3+
This directory has a couple of generated assets alongside the hand-written post:
4+
5+
- `img/sr-rbits-floor-r5.png`
6+
- `img/sr-rbits-floor-r4.png`
7+
- `img/sr-rbits-floor-r2.png`
8+
- `img/sr-rbits-centered-r5.png`
9+
- `img/sr-rbits-centered-r4.png`
10+
- `img/sr-rbits-centered-r2.png`
11+
- `img/sr-r2-floor-vs-centered.png`
12+
- `sr_mlp_lp_rtn_sr_curves.plotly.json`
13+
14+
## Regenerating the SR snapshots
15+
16+
The floor-mode images
17+
18+
- `img/sr-rbits-floor-r5.png`
19+
- `img/sr-rbits-floor-r4.png`
20+
- `img/sr-rbits-floor-r2.png`
21+
22+
are generated from the real scalar SR JavaScript demo using Playwright + Chromium.
23+
24+
Inputs:
25+
26+
- `scripts/sr_rounding_snapshot.html`
27+
- `sr-rosenbrock.css`
28+
- `sr-rosenbrock.js`
29+
30+
Generator:
31+
32+
- `scripts/generate_sr_snapshot_playwright.py`
33+
34+
Run from the repo root:
35+
36+
```bash
37+
uv run python pages/posts/2026/04-stochastic-rounding/scripts/generate_sr_snapshot_playwright.py
38+
```
39+
40+
That overwrites:
41+
42+
```text
43+
img/sr-rbits-floor-r5.png
44+
img/sr-rbits-floor-r4.png
45+
img/sr-rbits-floor-r2.png
46+
img/sr-rbits-5-4-2-floor.png
47+
```
48+
49+
### One-time setup for the snapshot generator
50+
51+
If Playwright is not installed in the project environment yet:
52+
53+
```bash
54+
uv pip install playwright
55+
uv run playwright install chromium
56+
```
57+
58+
## Notes
59+
60+
- The snapshot generator fast-forwards the live JS demo to a large sample count and captures three panels for each mode, with `R=5`, `R=4`, and `R=2`.
61+
- It also adds a boxed zoom inset around the `x \in [3.0, 3.5]` region of each panel.
62+
- `uv run python pages/posts/2026/04-stochastic-rounding/scripts/generate_sr_snapshot_playwright.py --mode centered` similarly writes:
63+
- `img/sr-rbits-centered-r5.png`
64+
- `img/sr-rbits-centered-r4.png`
65+
- `img/sr-rbits-centered-r2.png`
66+
- `img/sr-rbits-5-4-2-centered.png`
67+
- If the scalar demo styling or DOM ids change, update `scripts/sr_rounding_snapshot.html` and possibly `scripts/generate_sr_snapshot_playwright.py`.
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
from pathlib import Path
5+
6+
from PIL import Image, ImageDraw, ImageFont
7+
from playwright.sync_api import sync_playwright
8+
9+
10+
SCRIPTS_DIR = Path(__file__).resolve().parent
11+
POST_DIR = SCRIPTS_DIR.parent
12+
ROOT = POST_DIR.parents[3]
13+
HTML = SCRIPTS_DIR / "sr_rounding_snapshot.html"
14+
TMP = ROOT / "tmp"
15+
16+
17+
@dataclass(frozen=True)
18+
class PanelSpec:
19+
title: str
20+
r_bits: int
21+
centered: bool = False
22+
min_samples: int = 200000
23+
24+
25+
def panel_specs(mode: str) -> list[PanelSpec]:
26+
if mode == "floor":
27+
return [
28+
PanelSpec("Floor SR, R = 5", r_bits=5, centered=False),
29+
PanelSpec("Floor SR, R = 4", r_bits=4, centered=False),
30+
PanelSpec("Floor SR, R = 2", r_bits=2, centered=False),
31+
]
32+
if mode == "centered":
33+
return [
34+
PanelSpec("Centered SR, R = 5", r_bits=5, centered=True),
35+
PanelSpec("Centered SR, R = 4", r_bits=4, centered=True),
36+
PanelSpec("Centered SR, R = 2", r_bits=2, centered=True),
37+
]
38+
raise ValueError(f"unknown mode: {mode}")
39+
40+
41+
def get_font(size: int, bold: bool = False):
42+
candidates = [
43+
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
44+
if bold
45+
else "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
46+
"/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf"
47+
if bold
48+
else "/usr/share/fonts/dejavu/DejaVuSans.ttf",
49+
]
50+
for candidate in candidates:
51+
path = Path(candidate)
52+
if path.exists():
53+
return ImageFont.truetype(str(path), size=size)
54+
return ImageFont.load_default()
55+
56+
57+
BG = (255, 254, 250)
58+
INK = (35, 38, 41)
59+
BOX = (80, 88, 98)
60+
BOX_FILL = (255, 254, 250)
61+
GAP = 28
62+
PAD_X = 24
63+
PAD_Y = 18
64+
X_MIN = 2.9
65+
X_MAX = 7.0
66+
Y_MIN = 2.4
67+
Y_MAX = 7.6
68+
ZOOM_X0 = 3.7
69+
ZOOM_X1 = 4.7
70+
ZOOM_Y0 = 3.7
71+
ZOOM_Y1 = 4.7
72+
73+
74+
def wait_for_samples(page, threshold: int):
75+
page.wait_for_function(
76+
"""threshold => {
77+
const el = document.getElementById("sr-rounding-seed");
78+
if (!el) return false;
79+
const m = /Samples:\\s*([0-9]+)/.exec(el.textContent || "");
80+
return m && Number(m[1]) >= threshold;
81+
}""",
82+
arg=threshold,
83+
timeout=20000,
84+
)
85+
86+
87+
def capture_panel(page, spec: PanelSpec, out_path: Path):
88+
page.get_by_role("button", name="Reset").click()
89+
if spec.centered:
90+
page.locator("#sr-rounding-mode-centered").check()
91+
else:
92+
page.locator("#sr-rounding-mode-floor").check()
93+
page.locator("#sr-rounding-demo").evaluate(
94+
"(el, label) => { el.dataset.meanLegendLabel = label; }",
95+
f"SR, {'Centered' if spec.centered else 'Floor'}, R = {spec.r_bits}",
96+
)
97+
r_input = page.locator("#sr-rounding-rbits")
98+
r_input.fill(str(spec.r_bits))
99+
r_input.blur()
100+
page.get_by_role("button", name="Ffwd").click()
101+
wait_for_samples(page, spec.min_samples)
102+
page.get_by_role("button", name="Pause").click()
103+
page.locator(".sr-rounding-canvas-wrap").screenshot(path=str(out_path))
104+
105+
106+
def sx(image_width: int, x: float) -> float:
107+
left = 58
108+
right = 20
109+
plot_width = image_width - left - right
110+
return left + (x - X_MIN) / (X_MAX - X_MIN) * plot_width
111+
112+
113+
def sy(image_height: int, y: float) -> float:
114+
top = 22
115+
bottom = 58
116+
plot_height = image_height - top - bottom
117+
return top + (1.0 - (y - Y_MIN) / (Y_MAX - Y_MIN)) * plot_height
118+
119+
120+
def add_zoom_inset(panel: Image.Image) -> Image.Image:
121+
panel = panel.copy()
122+
draw = ImageDraw.Draw(panel)
123+
x0 = sx(panel.width, ZOOM_X0)
124+
x1 = sx(panel.width, ZOOM_X1)
125+
y0 = sy(panel.height, ZOOM_Y1)
126+
y1 = sy(panel.height, ZOOM_Y0)
127+
crop_box = (
128+
int(round(x0)),
129+
int(round(y0)),
130+
int(round(x1)),
131+
int(round(y1)),
132+
)
133+
draw.rectangle(crop_box, outline=BOX, width=3)
134+
135+
crop = panel.crop(crop_box)
136+
inset_w = int(panel.width * 0.44)
137+
inset_h = int(inset_w * crop.height / crop.width)
138+
inset = crop.resize((inset_w, inset_h), Image.Resampling.BICUBIC)
139+
inset_box = (
140+
panel.width - inset_w - 92,
141+
panel.height - inset_h - 220,
142+
)
143+
outer_box = [
144+
inset_box[0],
145+
inset_box[1],
146+
inset_box[0] + inset_w,
147+
inset_box[1] + inset_h,
148+
]
149+
panel.paste(inset, inset_box)
150+
draw.rectangle(outer_box, outline=BOX, width=3)
151+
src_tl = (crop_box[0], crop_box[1])
152+
src_bl = (crop_box[0], crop_box[3])
153+
inset_tl = (outer_box[0], outer_box[1])
154+
inset_bl = (outer_box[0], outer_box[3])
155+
draw.line([src_tl, inset_tl], fill=BOX, width=3)
156+
draw.line([src_bl, inset_bl], fill=BOX, width=3)
157+
return panel
158+
159+
160+
def panel_output_path(mode: str, r_bits: int) -> Path:
161+
return POST_DIR / "img" / f"sr-rbits-{mode}-r{r_bits}.png"
162+
163+
164+
def main():
165+
import argparse
166+
167+
parser = argparse.ArgumentParser()
168+
parser.add_argument("--mode", choices=["floor", "centered"], default="floor")
169+
args = parser.parse_args()
170+
171+
panels_spec = panel_specs(args.mode)
172+
out_path = POST_DIR / "img" / f"sr-rbits-5-4-2-{args.mode}.png"
173+
174+
TMP.mkdir(parents=True, exist_ok=True)
175+
out_path.parent.mkdir(parents=True, exist_ok=True)
176+
shot_paths = [
177+
TMP / f"sr-snapshot-{args.mode}-{i}.png" for i in range(len(panels_spec))
178+
]
179+
180+
with sync_playwright() as p:
181+
browser = p.chromium.launch()
182+
page = browser.new_page(
183+
viewport={"width": 1040, "height": 980}, device_scale_factor=2
184+
)
185+
page.goto(HTML.resolve().as_uri())
186+
page.wait_for_selector("#sr-rounding-demo.js-ready", timeout=10000)
187+
for spec, path in zip(panels_spec, shot_paths):
188+
capture_panel(page, spec, path)
189+
browser.close()
190+
191+
panels = [add_zoom_inset(Image.open(path).convert("RGB")) for path in shot_paths]
192+
for panel, spec in zip(panels, panels_spec):
193+
panel.save(panel_output_path(args.mode, spec.r_bits), "PNG")
194+
width = PAD_X * 2 + sum(panel.width for panel in panels) + GAP * (len(panels) - 1)
195+
height = PAD_Y * 2 + max(panel.height for panel in panels)
196+
canvas = Image.new("RGB", (width, height), BG)
197+
198+
x = PAD_X
199+
top_y = PAD_Y
200+
for panel in panels:
201+
canvas.paste(panel, (x, top_y))
202+
x += panel.width + GAP
203+
canvas.save(out_path, "PNG")
204+
205+
for panel in panels:
206+
panel.close()
207+
for path in shot_paths:
208+
path.unlink(missing_ok=True)
209+
210+
print(out_path)
211+
212+
213+
if __name__ == "__main__":
214+
main()
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1" />
6+
<title>SR R=2 Snapshot</title>
7+
<style>
8+
@import url("../sr-rosenbrock.css");
9+
html, body {
10+
margin: 0;
11+
padding: 0;
12+
background: #fffefa;
13+
}
14+
body {
15+
font-family: ui-sans-serif, system-ui, sans-serif;
16+
}
17+
.frame {
18+
width: 980px;
19+
margin: 18px auto;
20+
}
21+
</style>
22+
</head>
23+
<body>
24+
<div class="frame">
25+
<figure id="sr-rounding-demo" class="sr-rounding-demo" aria-label="Integer rounding comparison" data-hide-sample="true" data-large-font="true" data-hide-rtn-legend="true" data-hide-rtn-curve="true" data-hide-y-axis-label="true">
26+
<div class="sr-rounding-toolbar">
27+
<div class="sr-rounding-buttons" aria-label="SR animation controls">
28+
<button id="sr-rounding-rtn" type="button" aria-pressed="true">RTN</button>
29+
<button id="sr-rounding-sample" type="button">SR</button>
30+
<button id="sr-rounding-play" type="button">Play</button>
31+
<button id="sr-rounding-pause" type="button">Pause</button>
32+
<button id="sr-rounding-ffwd" type="button">Ffwd</button>
33+
<button id="sr-rounding-reset" type="button">Reset</button>
34+
</div>
35+
<fieldset class="sr-rounding-mode">
36+
<legend>SR Mode:</legend>
37+
<label for="sr-rounding-mode-floor">
38+
<input id="sr-rounding-mode-floor" type="radio" name="sr-rounding-mode" value="floor" checked />
39+
F
40+
</label>
41+
<label for="sr-rounding-mode-centered">
42+
<input id="sr-rounding-mode-centered" type="radio" name="sr-rounding-mode" value="centered" />
43+
C
44+
</label>
45+
</fieldset>
46+
<label class="sr-rounding-param" for="sr-rounding-rbits">
47+
R
48+
<input id="sr-rounding-rbits" type="number" min="0" max="24" step="1" value="2" inputmode="numeric" />
49+
</label>
50+
<span id="sr-rounding-seed">no SR yet</span>
51+
</div>
52+
<div class="sr-rounding-canvas-wrap">
53+
<canvas id="sr-rounding-canvas"></canvas>
54+
<div class="sr-rounding-fallback" aria-live="polite">JavaScript has not started.</div>
55+
</div>
56+
</figure>
57+
</div>
58+
<script defer src="../sr-rosenbrock.js"></script>
59+
</body>
60+
</html>
179 KB
Loading
200 KB
Loading
209 KB
Loading
264 KB
Loading
264 KB
Loading

0 commit comments

Comments
 (0)