# Infrastructure The machines the generation loop runs on, and how work reaches them. The loop itself is in `docs/generation-loop.md`; measured rates are in `docs/rig-capacity.md`. Hardware is named by role and class, never by hostname, address or account — this repository is assumed public. Real names live in the gitignored `bench/*.local.json` and the local rig notes. ## In brief - **The loop.** One engine function per spec algorithm (for example the CSS 2.1 width calculation), written by a model, checked against cached browser results. - **Sampling tier.** GPU cards running mid-sized models write N versions of a function from the spec text, the engine's Rust interfaces and the currently failing cases. Many cheap tries beat one careful one, because the browsers say which tries are right. - **Capacity tier.** When every version fails, a larger model gets the spec, the best failing versions and the exact per-box geometry differences. - **Verification tier.** CPU servers compile candidates in one batch and run each inside the engine over the cached corpus, under a millisecond per page. The same tier builds that cache by running the three browsers once per page, and runs conformance suites. - **Outcomes.** Keep the passing version, sample again with the new failures, or escalate. What is kept is ordinary Rust with a provenance record; no model runs in the shipped engine. Environment variables are named `NEV_*`; older records use `GENERA_*`, which every reader still accepts (`docs/rename-to-nev.md`). ## Detail ### The full cycle ```mermaid flowchart TD browsers["Chromium, Firefox, WebKit
once per page (CPU)"] --> cache["cached ground truth
three-engine vote"] spec["spec text
one algorithm"] --> sample["SAMPLE (GPU pool)
N versions of one function"] sample --> verify["VERIFY (CPU)
dedupe, batch compile,
run over the corpus"] cache --> verify verify -- "all pass" --> accept["commit Rust source
+ provenance record"] verify -- "some fail" --> sample verify -- "all fail repeatedly" --> escalate["ESCALATE
larger model + geometry diffs"] escalate --> verify ``` Nothing is accepted on a model's word: escalated output goes through the same check. Writing candidates is the binding constraint; verification has a large margin over it. ### What comes out, and how the engine uses it The cycle outputs **ordinary Rust source**, committed next to the hand-written code it plugs into, with spec steps as comments and a **provenance record** (`roadmap.md` → "Provenance is a first-class artifact"). The engine compiles it like any other code; the GPU and CPU fleets exist only at build time. **Provenance drives maintenance.** When a spec section or a definition it uses changes, the provenance index finds exactly the functions to regenerate (`roadmap.md` → "A specification changes"); a browser update re-dumps the ground truth instead. The same index is the bill of materials for a configurable build (`roadmap.md` → "Phase 4"). ## Measured baseline One oracle process, three engines, 235 documents including browser startup: **17.9 s**, about **13 documents per second per engine**, using about two cores. Scaled to a few hundred cores that would be about 3,000 documents per second, a ten-million-case corpus in about an hour — but per-core throughput on the contended 16-core development box fell from 1.48 to 0.48 docs/s/core between 1 and 16 workers (`docs/rig-capacity.md` → "Oracle corpus building"), so the figure on dedicated hardware is still to be measured. GPU-side figures: `docs/rig-capacity.md` → "GPU tier". ## Three tiers **Sampling tier — high memory bandwidth GPUs.** Bulk best-of-N generation, throughput-bound. Decode speed tracks *active* parameters, so a mixture-of-experts model with a small active fraction gives several times the candidates of a dense model. Ampere cards have no fp8, so quantization is 4-bit weight-only. **Capacity tier — large unified-memory accelerators.** About a third of the bandwidth, several times the memory: for the few hard calls and any teacher model. About 100B total with ~12B active is the realistic ceiling. **Verification tier — many CPU cores.** Corpus building, candidate compilation and the inner loop. It must never spawn a browser per case. ## As discovered Every host advertises itself over mDNS and answers an HTTP inventory; a collector aggregates it. What that shows, by role: - **The model gateway** is a LiteLLM instance fronting one alias per model pool. The aliases, their pools, how many requests each takes at once, and which are retired are declared in `bench/folknet.json` (the two GLM-5.3 Flash aliases, without backends since 2026-09-22, were removed from the gateway on 2026-09-25); whether each one answers right now is in each lane's snapshot, which a probe refreshes every 5 minutes and `bench/folknet-status.sh [--lane N]` prints (status, how long it has held it, last answer, snapshot age). Design: `docs/folknet-inventory.md`. Virtual keys carry per-key allowlists, so a new alias must be granted to the caller's key. Each pool's HAProxy reports liveness, not readiness: a `/v1/models` answer does not prove a completion will return. - **Four ARM unified-memory nodes (DGX Spark class)** — `aarch64`, one GB10 GPU, 20 cores, 121 GB RAM, 3.7 TB NVMe each. They served GLM-5.3 Flash as two independent two-node pairs until 2026-09-22. Qwen3.8-Flash-Next did not run under any vLLM build on the rig (the architecture was too new for the rig's EXL3 fork, and upstream has no EXL3 backend), so it serves through the checkpoint's own engine (TabbyAPI) instead, one replica per node, about 75 GB plus ~45 GB of page cache each. Two of the four now serve DeepSeek-V4.1-Flash. Full account: `docs/rig-capacity.md` → "Attempted — Qwen3.8-Flash-Next". - **The eight-card host** — eight 24 GB consumer cards on a 64-core EPYC with ~500 GB RAM, bare Linux, six serving containers using every card (four behind `-fast`, two behind `-long`). The closest thing to the sampling tier as designed. No spare GPU. - **The verification tier** — ten Proxmox 9.x nodes (EPYC 7742, 7B13, 7K62 and 7452; 32 to 64 physical cores, 125 GB to ~500 GB RAM, 1–2 TB NVMe), shared with tenant VMs; five pass thirteen consumer cards through to tenant machines. A VM sized for browser builds sits on the GLM-pool proxy host (open: its core count and whether it should be running). Free capacity is host by host. - **A downloader host** pulls checkpoint shards in parallel across the site's four WAN uplinks onto the NAS, verifies them and publishes a pinned revision for nodes to sync. - **Access.** Root with the jump host's key works on every physical host; the ARM nodes use their own admin key. The gateway's own guest refuses, and is not a compute host. ## Where each piece runs Every role is a guest or a container; nothing is installed on a Proxmox node's own OS. ```mermaid flowchart LR dev["development box
16 cores, shared"] -- "ssh via jump host" --> lanes["2 build lanes
(LXC, 96 threads each)"] dev -- "port forward" --> gw["model gateway
(LiteLLM)"] lanes --> gw gw --> fast["eight-card host
-fast, -long"] gw --> vm["four-card VM
-fast-2"] gw --> fn["2 ARM nodes
flash-next"] gw --> ds["2 ARM nodes
deepseek pair"] lanes --> helpers["compile helpers
(sccache-dist)"] ``` - **Development box.** No GPU, no rig role; holds the port forwards to the gateway. - **Jump host.** A VM on one node; the only route in. Nothing of this project on it. - **Build lanes.** One unprivileged LXC on each of two nodes (`bench/lane-create-lxc.sh`, `bench/lane-provision.sh`), started at node boot. A gate running when a lane stops fails. - **Compile helpers and scheduler.** Four more containers plus builders on the two lanes; scheduler, redis cache and a loopback CONNECT proxy on one lane (`bench/lane-dist.sh`). - **Eight-card host.** Docker containers under a templated unit `folknet-qwen38-vllm-worker@`, behind that host's own HAProxy on port 4000. Not edited by this project. - **ARM nodes, split two ways since 2026-09-24.** - Two serve `qwen3.8-flash-next`, one systemd unit and container each on port 5000 (`bench/sparks/`), behind the proxy host's HAProxy (least connections, eight in flight per replica) — so the alias takes **16 in flight, not 32**; a batch at concurrency 32 queues the rest. Either replica can stop alone. - Two, joined by a direct cable, serve `deepseek-v4.1-flash` as one tensor-parallel pair: one oneshot unit on the head node (`bench/sparks/dsv41/folknet-dsv41-exl3-pair.service`) starts both containers and returns once the API is healthy (about 7.5 minutes cold); stop and start only through that unit, never one node alone. HAProxy `maxconn 2`. - Units are stopped and started through a privileged container that chroots into the host, because the admin account has no sudo. GLM units are disabled on all four. - **Four-card VM (`qwen3.8-27b-fast-2`).** Four 3090s borrowed from a stopped tenant VM on the same node (only one of the two may run; `bench/gpu-vm/`). Four units `folknet-qwen38-vllm-worker@fast-{0..3}` on ports 8101–8104, the eight-card host's `-fast` recipe, HAProxy `maxconn 4` per worker (16 in flight). A separate alias because the `-fast` load balancer lives on the eight-card host. **Open.** Which host carries the four-card VM behind `qwen3.8-27b` (the measured Qwen server), and how it is started and stopped; whether anything restarts the eight-card host's HAProxy or re-pulls its image; what keeps the development box's port forwards up after a reboot; and the gateway config is generated from a site file outside the rig, which needs every hand-added alias too. ## Sharing the hardware The Proxmox nodes are shared with other tenants. Three rules: - **Nothing of this project goes on a node's own OS**, so a guest can be stopped and its hardware handed back without touching the node. - **GPU work runs in a VM with passthrough, never an LXC** (an LXC would need the vendor driver on the node). CPU work runs in an unprivileged LXC, or in docker on non-Proxmox hosts. - **Stopping a project guest frees what it held**, and a lane competes with tenants as an equal under contention. Nothing checks this; it is how the rig is laid out. Limits today: the ARM replicas' HAProxy queues a request at most 30 minutes; power budgets are modelled (`bench/capacity.py`) but not enforced. **Not built yet:** a per-role start/stop/drain wrapper; a drain (a replica's stop gives in-flight replies only 60 s); taking a backend out of HAProxy before stopping it; a narrow sudo rule for the ARM nodes' admin account. ## Build lane Two unprivileged containers (96 logical CPUs — three quarters of a 64-core, 128-thread node — 96 GB, 200 GB disk), each a build lane and oracle host: the Rust toolchain, Node 22, the repository cloned from the rig's forge ("Where the repository is stored" below), Playwright's three browsers, and a PulseAudio null sink so a realtime `AudioContext` runs. The scripts are the record and are re-runnable: `bench/lane-create-lxc.sh`, `bench/lane-provision.sh`, `bench/lane-nas.sh`. **Running a gate there.** ```mermaid sequenceDiagram participant W as worktree (dev box) participant L as lane participant F as rig forge W->>L: ssh: git bundle of unpushed commits + diff + untracked files L->>F: fetch the lane's mirror (only if needed, or older than 10 min) L->>L: lane-apply.sh: check out exact commit, apply diff, run command L-->>W: streamed output (and --pull directories) ``` `bench/remote-lane.sh ` does this. Each lane root keeps one bare mirror (`/.git-mirror`) with one git worktree per track; it fetches only when the bundle cannot supply the commit or the last fetch is older than `NEV_LANE_FETCH_MAX_AGE` (600 s), because the public forge the mirrors first fetched from rate-limits. The lane address comes from `NEV_LANE_SSH` or `bench/lane.local.json`; the mirror URL from `NEV_LANE_GIT_URL`, that file, or the worktree's `origin`. Examples: ``` bench/remote-lane.sh . cargo test --workspace bench/remote-lane.sh --pull corpus/absolute-ground-truth . \ cargo run --release -p generation --bin absolute_ground_truth_cache ``` Why a bundle rather than rsync: uploads from the development box to the lane run at about 18.6 KB/s (downloads 435 KB/s), so rsyncing a worktree took ~315 s of a ~424 s run. **Where the repository is stored.** Two forges, with different jobs: | forge | role | holds | |---|---|---| | the rig's Forgejo | intermediate storage, on the rig network | everything the lanes need: `main`, job branches `job/` and their results `result/` (`specs/job-submit-land.md`) | | tangled | persistent storage, public | `main` and the `spec//` landing tags only | The Forgejo repository is a regular repository, not a mirror of tangled. The lanes clone and fetch from it: each lane root's `.git-mirror` and the lane user's clone fetch from `http://:@//.git`, and `bench/lane.local.json` `git_url` (or `NEV_LANE_GIT_URL`) names the same URL. The user and token come from the primary checkout's `.env` (`GENERA_FORGE_USER`, `GENERA_FORGE_TOKEN`, still under the old prefix; `docs/rename-to-nev.md`). The development box reaches Forgejo through a local port forward, as the remote `rig` (`http://:@127.0.0.1://.git`), which the job scripts use by default (`JOBS_FORGE_REMOTE`). `origin` fetches from tangled and has push URLs for both forges, so `git push origin main` updates both; job and result branches go to `rig` only and are deleted there after a landing. Each of those URLs carries the token: they live in `.git/config` and the gitignored files, never in a committed file, a log line or a message. **The track gate runs only on the build lane.** `tracks.json` → `verify` runs `bench/verify.sh`, which hands itself to `bench/on-lane.sh --lane-only` and, once there, runs `cargo test --workspace`, the job event store's tests (`jobs/events/*.test.mjs` under `node --test`, then `bench/job-events-lane-test.sh`), and then `bench/generated-scores.sh`. The other `*.test.mjs` and `bench/*-test.sh` files are run by hand, not by the gate. With no lane configured or none answering (`ssh -o BatchMode=yes -o ConnectTimeout=5` fails) the gate exits 3 and runs nothing on the development box; a lane that answers but fails the tests or a score is a failed gate — no local retry. Measured before the scores were added: a new worktree's first test run 106 s (124 s with a fresh mirror), a warm rerun 22 s, local fallback 78 s. A clean `cargo test --workspace` compiles in 61 s on a lane and 57 s on the development box — the build is bound by the dependency chain through Stylo, not by cores; the lane's value is that it takes load off the shared box and runs several gates at once. **Unattended landings never fall back to the development box.** `bench/on-lane.sh` without `--lane-only` runs its command locally when no lane answers, and `bench/land.sh` runs each `landChecks` command that way, and `verify` directly. Setting `NEV_LANE_STRICT=1` in `bench/land.sh`'s environment (the scheduled `jobs/scripts/poll-and-land.sh` gets it from `tracks.json`'s `specWatch.pollEnv`) makes `verify` go through `bench/on-lane.sh --lane-only` too and every check `--lane-only`, so with no lane answering the landing is refused with "verify failed on the tip (exit 3: no build lane answered ...)" and nothing of the gate runs here. Unset, as in interactive use, a check keeps its local fallback. `bench/verify.sh` itself is lane-only either way; the one thing that would run it on the development box is `NEV_ON_LANE` set there by hand. **Already on a lane, `bench/on-lane.sh` runs the command in place.** With `NEV_ON_LANE` set, `bench/on-lane.sh` (and `bench/remote-lane.sh`) runs its command in the named worktree on the machine it is on, `--lane-only` or not, reads no lane settings, and ignores `--pull`, since the outputs are already where they belong; the command's exit status is the run's. This is the same check `bench/verify.sh` makes, so a spec's `bench/on-lane.sh --lane-only "$PWD" ...` works unchanged inside a Dagu job, whose clone has no `.env` and no `bench/lane.local.json` and whose writer gets `NEV_ON_LANE=1` from `jobs/scripts/pi-session.sh`. As with `bench/verify.sh`, a `NEV_ON_LANE` set by hand on the development box would run the command there. `bench/on-lane-test.sh` covers both sides. The cost: with both lanes down nothing lands until one answers. `bench/land-test.sh` scenario `s1` covers both settings. **The gate scores every generated function against its own corpus.** A committed file under `crates/layout/src/generated/` can be wrong while every test passes, because the tests exercise in-tree reference rules, not the generated function. `bench/generated-scores.sh` clears and rebuilds each corpus with its `*_ground_truth_cache` binary (the browser oracle runs on the lane), then `generated_score_check` scores each generated function through its own runner and fails it against its floor. The floor is two numbers in `crates/layout/src/generated/.floor.json` beside the function's provenance record: `cases_passed`, how many cases it passes, and `cases_run`, how many cases its corpus holds (only cases three engines agreed on). It fails when the passing count drops, when the corpus shrinks even though the passing count holds, and when a function passes no case at all (a lane missing a browser leaves fewer than three engines to vote, so every case drops; a floor of zero cannot hide a function that scores nothing). The provenance record describes the model batch that admitted the function and is never rewritten; the floor is never below its `verification.cases_passed`, and a function with no floor file yet is held to that alone. A placeholder record, such as the hand-written `table` one (0 recorded), therefore floors at its real score once a floor file exists. **Floors rise by themselves, after a landing.** `bench/after-land.sh` ends with `bench/generated-floors-update.sh`, which runs `bench/generated-scores.sh --print-raises` on the build lane, writes each floor that the fresh score is above (a function with no floor file gets one), and commits only those files as one commit on `main`, the way the map and dashboard commits are made. It refuses on uncommitted changes, writes nothing when the lane does not answer or a score fails, never lowers a number, and a failure is one line and does not change the landing. Raising happens after the landing rather than in the gate because the gate runs on the lane's copy, where a commit cannot reach the track, and because a track never edits a floor: `bench/land.sh` refuses a diff that touches any `*.floor.json`, so an unattended track cannot lower one. The cost is a second scoring run on the lane per landing, and a window between a landing that improves a function and the raise in which the old floor holds. A floor comes down only by a person's reviewed edit of its file. **One place to add a generated function.** A function is discovered from its `.provenance.json`; its runner and the cache binary that writes its corpus are one entry in `FUNCTIONS` in `crates/generation/src/bin/generated_score_check.rs`, and a provenance record with no entry fails the gate with that instruction. `bench/generated-scores.sh` takes the caches to rebuild from `generated_score_check --list-caches`, so nothing under `bench/` changes and a track adding a function can land unattended. `bench/generated-scores-test.sh` exercises the scripts, including the raise, without a browser. **Model batches run only on a lane.** `bench/absolute-mixed-batch.sh` and `bench/overflow-mixed-run.sh` are started from the development box like any script (`NEV_MODELS=qwen3.8-27b-fast:32:32 bench/absolute-mixed-batch.sh`) and re-run themselves on the lane through `bench/on-lane.sh --lane-only` (`bench/batch-lane.sh`): the `NEV_*` settings go along, credentials and lane settings excepted, and the batch's results come back with `--pull`. With no lane configured or none answering the script exits 3 and runs nothing locally; `bench/lane-apply.sh` exports `NEV_ON_LANE=1`, which is how a script knows it is already there. Each run also records its phase timings and how many of the lane's CPUs each phase kept busy (`bench/cpu-sample.sh`, `bench/cpu-phases.py`; `docs/rig-capacity.md` → "A real batch on a build lane"). **A stray directory at a worktree's lane path breaks the gate.** `bench/lane-apply.sh` tests only for `//.git`; a leftover directory without it makes `git worktree add` fail, and the gate cannot pass until it is removed by hand (check `git -C worktree list` first). Not yet fixed in the script. **Cleanup.** Every worktree run on a lane leaves its copy (`/`, `target/` and all, 3–12 GB) and `/.payload-` there; left alone they filled a lane's disk. `bench/lane-sweep.sh` removes, on every lane in `bench/lane.local.json` `lanes[]`, the copies whose track has no worktree in the primary checkout (`--dry-run` lists them with sizes). It keeps the primary's own copy, any copy with a process running in it (a spec writer runs inside its copy), and directories that are not worktrees of the lane's mirror. Untracked data under a copy's `corpus/` (links excepted: they point into the NAS already) goes to the NAS first, as one tar file at `archive/corpus//