Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions .github/workflows/pta-web.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# pta-web.yml -- the canary's PTA harness as a browser module, checked and published.
#
# tools/pta-web/build.sh builds the harness pinned in docs/designs/canary/harness
# with gcc and with emcc; tools/pta-web/compare.sh runs the canary's rows
# natively, as a node build and as the browser module itself, and fails unless
# all three print the same bytes.
#
# On main (and on demand) a passing build is force-pushed as one commit to the
# orphan branch `pta-web`: the module, the canary network, the pooled data,
# rows.json (the canary's settings and what the harness printed for them) and
# manifest.json (source commit, harness digests, compilers, file digests).
# A page can load it from raw.githubusercontent.com with no token. A build that
# does not match the harness is never published.
#
# Pull requests build and compare, and publish nothing.
name: PTA web module

on:
push:
branches: [main]
paths:
- 'docs/designs/canary/**'
- 'tools/pta-web/**'
- '.github/workflows/pta-web.yml'
pull_request:
paths:
- 'docs/designs/canary/**'
- 'tools/pta-web/**'
- '.github/workflows/pta-web.yml'
workflow_dispatch:

permissions:
contents: write # push the pta-web branch

concurrency:
group: pta-web-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
runs-on: ubuntu-24.04
timeout-minutes: 45
steps:
- uses: actions/checkout@v4

- name: Emscripten
uses: mymindstorm/setup-emsdk@v14
with:
version: 3.1.74

- name: Build natively and as WebAssembly
run: bash tools/pta-web/build.sh "$RUNNER_TEMP/pta-web"

- name: Compare native, node build and browser module
run: bash tools/pta-web/compare.sh "$RUNNER_TEMP/pta-web"

- name: Summary
if: always()
run: |
f="$RUNNER_TEMP/pta-web/summary.md"
[ -f "$f" ] && cat "$f" >> "$GITHUB_STEP_SUMMARY" || echo "no summary written" >> "$GITHUB_STEP_SUMMARY"

- name: Keep the build and the runs
if: always()
uses: actions/upload-artifact@v4
with:
name: pta-web
retention-days: 30
if-no-files-found: ignore
path: |
${{ runner.temp }}/pta-web/web
${{ runner.temp }}/pta-web/runs
${{ runner.temp }}/pta-web/summary.md

# Only after compare passed (no always()), and never from a pull request.
- name: Publish to the pta-web branch
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
run: |
set -eu
web="$RUNNER_TEMP/pta-web/web"
cp "$RUNNER_TEMP/pta-web/summary.md" "$web/"
python3 - "$web" <<'PY'
import hashlib, json, os, subprocess, sys, datetime
web = sys.argv[1]
sha = lambda p: hashlib.sha256(open(p, 'rb').read()).hexdigest()
harness = 'docs/designs/canary/harness'
doc = dict(
repo=os.environ['GITHUB_REPOSITORY'], commit=os.environ['GITHUB_SHA'],
run_url=f"{os.environ['GITHUB_SERVER_URL']}/{os.environ['GITHUB_REPOSITORY']}/actions/runs/{os.environ['GITHUB_RUN_ID']}",
generated_at=datetime.datetime.now(datetime.timezone.utc).isoformat(timespec='seconds'),
emcc=subprocess.run(['emcc', '--version'], capture_output=True, text=True).stdout.splitlines()[0],
gcc=subprocess.run(['gcc', '--version'], capture_output=True, text=True).stdout.splitlines()[0],
harness={f: sha(os.path.join(harness, f)) for f in sorted(os.listdir(harness))},
files={f: dict(bytes=os.path.getsize(os.path.join(web, f)), sha256=sha(os.path.join(web, f)))
for f in sorted(os.listdir(web)) if f != 'manifest.json'},
identical_to_native=True,
)
json.dump(doc, open(os.path.join(web, 'manifest.json'), 'w'), indent=2)
PY
cat > "$web/README.md" <<MD
# pta-web

Written by \`.github/workflows/pta-web.yml\` and force-pushed on every passing build: the
canary's PTA harness (\`docs/designs/canary/harness\`) as a browser module, with the canary
network and MNIST pooled to 8 x 8. Every row in \`rows.json\` printed the same bytes natively,
as a node build and from this module. Source commit: ${GITHUB_SHA}.
MD
cd "$web"
git init -q -b pta-web
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -A
git commit -q -m "PTA web module for ${GITHUB_SHA::7}"
git push -q --force "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${GITHUB_REPOSITORY}.git" pta-web
echo "published $(ls | wc -l) files to pta-web"
24 changes: 24 additions & 0 deletions tools/pta-web/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# tools/pta-web

The canary's PTA harness (`docs/designs/canary/harness`) built as a WebAssembly
module a browser can run, and checked against the native harness byte for byte.

- `build.sh OUT` builds the pinned harness with gcc and with emcc (a node build,
and the browser module), checks MNIST against `regenerate.sh`'s digests, and
pools it to 8 x 8 with the harness's own `pool`.
- `compare.sh OUT` runs the canary's rows (seed 1, 128 x 64 tile, 1,000 images:
format only; as budgeted; as budgeted, lit, fitted) natively, as the node
build and as the browser module (`run-web.mjs`), and fails unless all three
print the same bytes. It writes `rows.json` and a timing table.
- `run-web.mjs` runs the browser module the way a page does: data in its
in-memory filesystem, `callMain` with the harness's arguments.

`.github/workflows/pta-web.yml` runs both on changes to the canary or these
tools, and on `main` publishes a passing build to the `pta-web` branch, with a
`manifest.json` of digests. A build that differs from the harness is never
published.

The module takes the harness's own command line. `eval --data /data --net
/canary.net` plus the flags in `rows.json` reproduces those rows; the same
flags at other values (`--abits`, `--adcbits`, `--thermal8`, `--photons8`,
`--prog`, `--xtalk`, `--src`...) run the canary at other settings.
66 changes: 66 additions & 0 deletions tools/pta-web/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: Apache-2.0
# Copyright 2026 Gridworkz
# build.sh -- the canary's pinned harness, natively and as WebAssembly.
#
# The PTA error model on the frozen canary, for a browser, printing exactly what
# the harness prints. This builds the
# harness pinned in docs/designs/canary/harness (the sources regenerate.sh
# checks) twice from the same files and flags -- gcc, and emcc -- and prepares
# the data both read: MNIST checked against regenerate.sh's digests, pooled to
# 8 x 8 by the harness's own `pool`.
#
# Usage: tools/pta-web/build.sh OUT_DIR (needs gcc, emcc, curl, gzip)
set -eu
HERE=$(cd "$(dirname "$0")" && pwd)
ROOT=$(cd "$HERE/../.." && pwd)
SRC="$ROOT/docs/designs/canary/harness"
OUT=$1
mkdir -p "$OUT/mnist" "$OUT/data" "$OUT/web"

# The flags pta_mnist.sh uses for SHAPE=chain, the canary's shape.
CFLAGS="-O2 -std=c99 -Wall -Wextra -pedantic -I$SRC -DN_SIDE=8 -DN_HID=64 -DN_OUT=16"

echo "== MNIST, checked against docs/designs/canary/regenerate.sh"
while read -r want name; do
[ -s "$OUT/mnist/$name.gz" ] || curl -sSfL --retry 3 -o "$OUT/mnist/$name.gz" \
"https://ossci-datasets.s3.amazonaws.com/mnist/$name.gz"
got=$(gzip -dc "$OUT/mnist/$name.gz" | sha256sum | cut -c1-64)
[ "$got" = "$want" ] || { echo "$name.gz does not hold MNIST's $name" >&2; exit 1; }
done <<'DIGESTS'
ba891046e6505d7aadcbbe25680a0738ad16aec93bde7f9b65e87a2fc25776db train-images-idx3-ubyte
65a50cbbf4e906d70832878ad85ccda5333a97f0f4c3dd2ef09a8a9eef7101c5 train-labels-idx1-ubyte
0fa7898d509279e482958e8ce81c8e77db3f2f8254e26661ceb7762c4d494ce7 t10k-images-idx3-ubyte
ff7bcfd416de33731a308c3f266cc351222c34898ecbeaf847f06e48f7ec33f2 t10k-labels-idx1-ubyte
DIGESTS

echo "== native build"
gcc --version | head -1
gcc $CFLAGS -o "$OUT/pta_mnist" "$SRC/pta_mnist.c" "$SRC/pta_tile_model.c" -lm
"$OUT/pta_mnist" selftest

echo "== data, pooled to 8 x 8 by the harness itself"
for f in train-images-idx3-ubyte train-labels-idx1-ubyte t10k-images-idx3-ubyte t10k-labels-idx1-ubyte; do
gzip -dc "$OUT/mnist/$f.gz" > "$OUT/data/$f"
case $f in *images*) "$OUT/pta_mnist" pool "$OUT/data/$f" "$OUT/data/$f" > /dev/null ;; esac
done
echo chain > "$OUT/data/shape"; echo as-is > "$OUT/data/pixels"

echo "== WebAssembly builds"
emcc --version | head -1
# For node: the same command line, with the real filesystem, so it reads exactly what gcc's does.
emcc $CFLAGS -sALLOW_MEMORY_GROWTH=1 -sNODERAWFS=1 -sEXIT_RUNTIME=1 \
-o "$OUT/pta_mnist_node.js" "$SRC/pta_mnist.c" "$SRC/pta_tile_model.c" -lm
# For the browser: a module the page runs in a worker, data in its in-memory filesystem.
# Node is allowed too, so compare.sh can run this exact file, not just a sibling build.
emcc $CFLAGS -O3 -sALLOW_MEMORY_GROWTH=1 -sMODULARIZE=1 -sEXPORT_NAME=PtaMnist \
-sENVIRONMENT=web,worker,node -sINVOKE_RUN=0 -sEXIT_RUNTIME=0 \
-sEXPORTED_RUNTIME_METHODS=callMain,FS \
-o "$OUT/web/pta_mnist.js" "$SRC/pta_mnist.c" "$SRC/pta_tile_model.c" -lm
cp "$ROOT/docs/designs/canary/canary_seed1.net" "$OUT/web/"
for f in train-images-idx3-ubyte train-labels-idx1-ubyte t10k-images-idx3-ubyte t10k-labels-idx1-ubyte; do
gzip -9c "$OUT/data/$f" > "$OUT/web/$f.gz"
done

echo "== sizes"
( cd "$OUT/web" && for f in *; do printf '%-34s %9d bytes gzip %9d\n' "$f" "$(wc -c < "$f")" "$(gzip -9c "$f" | wc -c)"; done )
63 changes: 63 additions & 0 deletions tools/pta-web/compare.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: Apache-2.0
# Copyright 2026 Gridworkz
# compare.sh -- run canary settings natively, as the node build and as the
# browser module itself; demand the same bytes from all three.
#
# The rows are the canary's own (pta_mnist.sh, `canary`): seed 1, draw 0, the
# 128 x 64 tile, the gate's first thousand images, the scale from a thousand
# held-out ones. A browser demo is only worth building if its numbers are the
# harness's numbers, so any difference fails this, and the table says how long
# each took both ways.
#
# Usage: tools/pta-web/compare.sh OUT_DIR (after build.sh; needs node)
set -u
OUT=$1
HERE=$(cd "$(dirname "$0")" && pwd)
NET="$(cd "$HERE/../.." && pwd)/docs/designs/canary/canary_seed1.net"
g="--rows 128 --cols 64"
q="--abits 6 --adcbits 8"
r="$q --thermal8 0.25 --photons8 30 --prog 1 --xtalk 0.02"
t="--host 2 --probe 1 --gate 1 --gatescale 1000"
src="--src 0.01 --srcline 0.05 --srcflat 0.2 --buses 2"
fix="--levelprobe 16 --levelfix weights8"
ROWS=(
"format_only|$g --impair quant $q --images 1000 $t"
"as_budgeted|$g --impair quant,thermal,shot,prog,xtalk $r --images 1000 $t"
"lit_fitted|$g --impair quant,thermal,shot,prog,xtalk $r --images 1000 $src $fix --gatefit 1 $t"
)
SUM="$OUT/summary.md"; mkdir -p "$OUT/runs"
{ echo "| row | native s | wasm, node build s | wasm, browser module s | identical |"; echo "|---|---|---|---|---|"; } > "$SUM"
bad=0
for row in "${ROWS[@]}"; do
name=${row%%|*}; args=${row#*|}
t0=$(date +%s.%N); "$OUT/pta_mnist" eval --data "$OUT/data" --net "$NET" --seed 1 $args > "$OUT/runs/$name.native.txt" 2>&1; rn=$?
t1=$(date +%s.%N); node "$OUT/pta_mnist_node.js" eval --data "$OUT/data" --net "$NET" --seed 1 $args > "$OUT/runs/$name.wasm.txt" 2>&1; rw=$?
t2=$(date +%s.%N); node "$HERE/run-web.mjs" "$OUT/web" "$OUT/data" eval --data @DATA --net @NET --seed 1 $args > "$OUT/runs/$name.web.txt" 2>&1; rb=$?
t3=$(date +%s.%N)
dn=$(awk "BEGIN{print $t1 - $t0}"); dw=$(awk "BEGIN{print $t2 - $t1}"); db=$(awk "BEGIN{print $t3 - $t2}")
# The browser module reads /canary.net where the others read the repo's path;
# the harness does not print the path, so the bytes must still agree.
if [ $rn -eq 0 ] && [ $rw -eq 0 ] && [ $rb -eq 0 ] && cmp -s "$OUT/runs/$name.native.txt" "$OUT/runs/$name.wasm.txt" \
&& cmp -s "$OUT/runs/$name.native.txt" "$OUT/runs/$name.web.txt"; then same=yes; else same="**no** (exit $rn/$rw/$rb)"; bad=1; fi
printf '| %s | %.2f | %.2f | %.2f | %s |\n' "$name" "$dn" "$dw" "$db" "$same" >> "$SUM"
echo "== $name: native ${dn}s, node build ${dw}s, browser module ${db}s, identical: $same"
echo "--- native output:"; head -c 1500 "$OUT/runs/$name.native.txt"; echo
[ "$same" = yes ] || { echo "--- diff, node build:"; diff "$OUT/runs/$name.native.txt" "$OUT/runs/$name.wasm.txt" | head -20;
echo "--- diff, browser module:"; diff "$OUT/runs/$name.native.txt" "$OUT/runs/$name.web.txt" | head -20; }
done
cat "$SUM"

# rows.json: each row's arguments and the line the harness printed for it, so a
# page can offer the canary's own settings and show their published result.
python3 - "$OUT" "${ROWS[@]}" <<'PY'
import json, sys
out, rows = sys.argv[1], sys.argv[2:]
doc = []
for r in rows:
name, args = r.split('|', 1)
line = open(f'{out}/runs/{name}.native.txt').read().strip()
doc.append(dict(name=name, args=args.split(), output=line))
json.dump(doc, open(f'{out}/web/rows.json', 'w'), indent=2)
PY
exit $bad
26 changes: 26 additions & 0 deletions tools/pta-web/run-web.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 Gridworkz
// run-web.mjs -- run the browser module (web/pta_mnist.js) the way a page does:
// data copied into its in-memory filesystem, callMain with the harness's own
// arguments, stdout collected. Usage: node run-web.mjs WEB_DIR DATA_DIR ARGS...
import { readFileSync, readdirSync } from 'node:fs';
import { createRequire } from 'node:module';
import { join } from 'node:path';

const [webDir, dataDir, ...args] = process.argv.slice(2);
const require = createRequire(import.meta.url);
const PtaMnist = require(join(webDir, 'pta_mnist.js'));
const out = [];
const mod = await PtaMnist({
print: (t) => out.push(t),
printErr: (t) => out.push(t),
locateFile: (f) => join(webDir, f),
});
mod.FS.mkdir('/data');
for (const f of readdirSync(dataDir)) mod.FS.writeFile(`/data/${f}`, readFileSync(join(dataDir, f)));
mod.FS.writeFile('/canary.net', readFileSync(join(webDir, 'canary_seed1.net')));
let code = 0;
try { code = mod.callMain(args.map((a) => a.replace('@DATA', '/data').replace('@NET', '/canary.net'))); }
catch (e) { if (e && typeof e.status === 'number') code = e.status; else throw e; }
process.stdout.write(out.join('\n') + (out.length ? '\n' : ''));
process.exit(code ?? 0);
Loading