Lumbridge Bench
CI / verify (push) Successful in 24s
CI / deploy (push) Failing after 1m14s

This commit is contained in:
Karti Tripathi
2026-08-04 00:44:07 -07:00
commit 006feee0f7
65 changed files with 13516 additions and 0 deletions
+200
View File
@@ -0,0 +1,200 @@
# Decisions
Why this repo is shaped the way it is. The reasoning is not recoverable from
the code, which is the only reason this file exists. Append, don't rewrite --
a superseded decision is more useful with its replacement attached than
deleted.
---
## D1 — Depend on `inspect_ai`, do not fork it
**Decision.** `bench.karti.ai` pip-installs `inspect-ai` and `inspect-evals`.
It is not a fork.
**Why.** The obvious move is to fork [inspect_ai][1] and grow from there. The
numbers say otherwise: 407 MB, ~2 commits/day, pushed daily. Forking means
either abandoning that stream of fixes or rebasing against it forever, and we
would be paying that for nothing — we are not changing the execution engine,
the scorer protocol, the sandbox, or the viewer. We are adding tasks, and
tasks are the documented extension point.
The decisive evidence is upstream's own layout: `inspect_evals` is a
**separate package that depends on `inspect_ai`**, holding 200+ evals, none of
them in a fork of core. The maintainers already built the seam. This repo is
simply our private `inspect_evals`.
**When to revisit.** If we ever need to change core execution behaviour. Then
fork from a known-good tag, for a stated reason. Not speculatively.
**Rejected alternative — build our own harness.** Reimplements sandboxed tool
execution, retry/rate-limit handling, parallel rollouts, log format, viewer,
and judge protocol, to arrive somewhere functionally identical. All of this
bench's value is in the task data; none is in the runner.
[1]: https://github.com/UKGovernmentBEIS/inspect_ai
---
## D2 — A target is `(model × host × quant × serving config × checkpoint)`
**Decision.** The registry is keyed on the full tuple, not on model name.
See `kbench/registry/__init__.py`.
**Why.** On unified-memory boxes the serving config moves throughput more than
a model swap does. The live your-node server runs `--enforce-eager` (no CUDA
graphs) and `--gpu-memory-utilization 0.55` (45% of unified memory unused,
capping KV cache and therefore the concurrency ceiling). A result recorded
against "Qwen3.6-35B-A3B" without those flags is not reproducible and not
comparable to anything.
`checkpoint` is in the key from day one because once we fine-tune, we will
compare `karti-7b@step2000` against `karti-7b@step4000` far more often than we
compare Qwen against Llama. Adding it later would invalidate every result
recorded before it.
**Consequence.** Every run snapshots the full target into its result file.
Referencing a registry id would go stale the first time `models.yaml` is
edited.
---
## D3 — Three-tier data split, and the eval set stays private
**Decision.** `data/public/` (a handful of shown examples), `data/private/`
(the real holdout), `data/canary/` (GUID strings embedded in samples).
**Why private — contamination, not secrets.** The instinctive reason to keep
eval data private is that samples derived from real tool calls might contain
sensitive material. That is true but secondary, and it has its own fix (see
below). The primary reason is that **a public test set gets scraped into the
next training corpus and stops measuring capability**. That is how MMLU,
GSM8K, and HumanEval died. Every benchmark with signal left in 2026 keeps a
private holdout. If our samples ever go public, the bench's useful life is
about one training cycle.
**On secrets specifically.** A private repo is not a secret-management
strategy. Anything derived from real workloads is scrubbed **at authoring
time** — no keys, no tokens, no tailscale addresses, no customer data —
because that content would also leak through the model we send it to (often
someone else's API) and through any generated share card. Repo privacy is
defence in depth, not the control.
**Canary tier.** Samples carry embedded GUIDs. Later we can grep a candidate
model for those strings and *prove* contamination rather than suspect it.
Costs nothing now; impossible to add retroactively.
---
## D4 — Per-sample outcomes in git; full outputs stay local
**Decision.** `results/*.json` stores each sample's score, pass/fail, and a
400-char excerpt. Full model outputs stay in the raw Inspect `.eval` log,
which is gitignored.
**Why.** An aggregate score tells you a checkpoint got worse. Per-sample
diffs tell you *which capability broke* — the difference between a leaderboard
and a debugging tool, and the whole reason this is usable for fine-tuning.
Storing complete transcripts, though, would bloat the repo (thousands of
samples × multi-KB reasoning traces × hundreds of runs) and would persist
sensitive echoes of signal-tier prompts into git history forever. Git carries
the shape of the failure; the local log carries the transcript.
---
## D5 — Two task tiers: reference and signal
**Decision.** `reference` tasks come from `inspect_evals` unmodified.
`signal` tasks are ours and private. Both appear on the leaderboard, visually
distinguished, with reference scores captioned *public, likely contaminated,
calibration only*.
**Why.** A bench made only of novel private tasks is unfalsifiable to an
outside reader — nobody, including future us, can tell whether a strange score
means the model is bad or the harness is broken. The reference tier is the
credibility anchor and the smoke test: if our IFEval lands far from published
values, the harness is wrong.
**Why IFEval first.** Its scorer is deterministic, so it needs no judge model
— no per-run cost, and no judge drift between runs to confound comparisons.
It is fast enough for a tight debug loop. And instruction adherence is the
property that actually decides whether a small local model can hold a system
prompt in production, which is the live question about `brain`.
---
## D6 — Self-contained perf load generator, not `vllm bench serve`
**Decision.** `kbench/perf.py` is an async httpx load generator against any
OpenAI-compatible endpoint.
**Why.** Submissions are the point of the public site, so we cannot assume
vLLM — the harness must measure SGLang, llama.cpp, ollama, or whatever a
submitted model ends up served under. It also needs nothing installed on the
target box, and it lets us fix the metric definitions so numbers stay
comparable across engines instead of inheriting each engine's conventions.
**Two traps it exists to avoid.** Both produce great-looking meaningless
numbers:
1. **Prefix caching.** vLLM caches shared prompt prefixes. Identical
concurrent prompts make prefill nearly free after the first request and
overstate throughput badly. Every request gets a unique leading nonce.
2. **Variable output length.** If the model decides when to stop, concurrency
levels finish at different token counts and tok/s stops being comparable.
We send `ignore_eos` so every request emits exactly `output_tokens`.
---
## D7 — Private gitea now; the harness may open-source later, the data never
**Decision.** `karti/lumbridge-bench` on gitea, private. Results public.
_(Updated 2026-08-01: the repo was `karti/bench.karti.ai`, now archived. "Site public" meant the
standalone bench.karti.ai leaderboard, which is retired — results surface through
`lumbridgecorp.com/bench` instead. The public/private split below is unchanged.)_
**Why.** The leaderboard is only valuable if people can see it; the eval data
is only valuable if they cannot (D3). If we later open-source, the harness
splits out and `data/private/` stays behind — which is why the seam between
`kbench/` (mechanism) and `data/` (content) is kept clean from the start. A
clean seam costs nothing today and keeps the decision reversible.
---
## D8 — Submissions execute untrusted code; sandbox before launch
**Status.** Planned, not yet implemented. Blocking for the public submit form.
Anonymous "drop a HuggingFace link and we'll benchmark it" means running
arbitrary code on our own hardware. HF repos can ship custom modeling files
that execute on load, and `.bin` checkpoints are pickles — remote code
execution by design. Non-negotiable gates:
- `safetensors` only; `trust_remote_code=false`; hard-fail otherwise
- eval runs in a container with **no network egress** and no tailscale route
- pre-flight via the HF API before queueing: reject if params exceed the
target box, if config is unreadable, if the repo is gated
- one box, one queue: show estimated wait, cap submissions per IP per day
---
## D9 — Anonymous submissions get aggregate scores only
**Decision.** Signed-in users (115karti invite) get per-sample breakdowns and
private-by-default results. Anonymous submissions get an aggregate score,
published.
**Why.** This reads like feature gating but it is primarily a contamination
defence: detailed per-sample feedback is exactly how someone would
reverse-engineer the private set (D3) by probing it one sample at a time.
---
## D10 — Public input is a manual suggestion, never an execution queue
**Decision.** The public action is **Suggest a model**. It records a Hugging Face reference and optional context for owner review. It never downloads, imports, schedules, or executes a model. Karti manually inspects the model card, license, files, compatibility, footprint, and serving path, then explicitly decides whether to run it.
**Why.** The product does not need a public code-execution service. Manual approval is the intended workflow and keeps ownership of scarce hardware and private evaluation data clear. The Lumbridge control plane stores the inert review record with status `suggested`; only an owner can advance its lifecycle.
**Supersedes D8 for product behavior.** The sandbox described in D8 is no longer a launch dependency because no public suggestion is automatically executed. If automated third-party execution is ever proposed again, it requires a new explicit decision and the full D8 isolation boundary first.
+106
View File
@@ -0,0 +1,106 @@
# Deploy
**Migration status: COMPLETE (2026-08-01).** `karti/lumbridge-bench` is the canonical and only
repository — `karti/bench.karti.ai` is archived in gitea, fully contained in this repo's `main`.
**The standalone site is retired.** Bench is a Lumbridge product, not its own destination, so
`bench.karti.ai` now serves a permanent redirect to `https://lumbridgecorp.com/bench`. The
`bench-karti.service` backend is stopped and disabled on web-host.
What that leaves on the box:
- Caddy vhost for `bench.karti.ai` is now `redir https://lumbridgecorp.com/bench permanent`
(a pre-change Caddyfile backup is at `/etc/caddy/Caddyfile.bak-bench-<timestamp>`)
- DNS `bench.karti.ai A 170.9.14.61` and the Let's Encrypt cert stay — the redirect needs both
- checkout at `/home/ubuntu/workspace/bench.karti.ai` remains, now on `origin` =
`karti/lumbridge-bench`; the `bench-karti` unit is stopped and disabled, not removed
**To bring a Bench UI back up**, deploy it as part of Lumbridge rather than reviving the subdomain.
Use `deploy/lumbridge-bench.service` and a fresh checkout; do not re-enable `bench-karti`.
The eval runner (`kbench`) is unaffected by any of this — it is a CLI, not the site.
## Gotchas hit during the first deploy
- **Caddy binds `10.0.0.2`, not loopback.** Testing a vhost from the box with
`curl --resolve host:443:127.0.0.1` returns `000` and looks like an outage.
Use `--resolve host:443:10.0.0.2`.
- **Order matters: DNS before the vhost reload.** Caddy will attempt ACME the
moment the vhost loads; if the A record does not exist yet the challenge
fails with NXDOMAIN and the retry sits in backoff for minutes. Adding DNS
first, or reloading Caddy again after adding it, gets the cert immediately.
- **The `oci` CLI is not on PATH on build-host** — it lives at `~/bin/oci`. And
`oci dns zone list` needs `--compartment-id`, but `oci dns zone get
--zone-name-or-id karti.ai` does not, which is the easier way in.
## Pre-existing, unrelated
Caddy's log shows recurring ACME failures for `www.office.karti.ai` and
`www.og.karti.ai` — NXDOMAIN, no `www` A records exist. Predates this deploy
and does not affect the apex domains, but it means those two sites retry ACME
forever. Either add the `www` records or drop them from their vhosts.
| | |
|---|---|
| repo | `karti/lumbridge-bench` on gitea, **private** |
| host | web-host, port **8909** |
| service | `bench-karti` compatibility service (systemd) |
| checkout | `/home/ubuntu/workspace/bench.karti.ai` compatibility path |
| domain | `bench.karti.ai` → web-host, DNS via oci on build-host (profile `cloud2-sanjose`) |
## One-time setup on web-host
```bash
cd /home/ubuntu/workspace
git clone https://gitea.example.internal:8444/karti/lumbridge-bench.git bench.karti.ai
cd bench.karti.ai/site
cp .env.example .env.local # loopback Lumbridge control plane
npm install && npm run build
sudo cp ../deploy/bench-karti.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now bench-karti
curl -s localhost:8909/api/health
```
Then add the caddy/nginx vhost for `bench.karti.ai``localhost:8909` and the
DNS A record, matching the other `*.karti.ai` sites.
## CI secret
The workflow needs one repo secret, `CLOUD2_SSH_KEY`: the private half of a
deploy key whose public half is in `ubuntu@web-host`'s `authorized_keys`.
Generate a dedicated key rather than reusing another site's.
```bash
ssh-keygen -t ed25519 -f bench-karti-ci-deploy -C "bench-karti-ci-deploy" -N ""
# public half -> web-host authorized_keys; private half -> gitea repo secret
```
## Suggestion inbox
The public form forwards an inert record to the Lumbridge control plane at
`LUMBRIDGE_CONTROL_PLANE_URL`; on web-host this is the private loopback service.
It stores only the Hugging Face URL, a review reason, and optional notes. Contact
data is not collected. The old Supabase migrations remain as history but are not
used by the live suggestion path. Public Bench cards continue to come from
reviewed `results/*.json`.
## Deploying results
Score cards are committed JSON, so publishing a new measurement is a commit:
```bash
kbench run brain --tasks ifeval --limit 100
git add results/ && git commit -m "measure: qwen3.6-35b-a3b @ your-node"
git push
```
CI rebuilds and the board picks it up. There is no database sync step because
there is no database behind the board.
## What is NOT wired yet
- **Automatic submission execution.** Intentionally not part of the product. The public form only records a suggestion for owner review; see `DECISIONS.md#d10`.
- **Auth.** Schema and invite code exist; the signup/login UI does not.
- **Suggestion workflow UI.** The owner console now lists the review inbox. Accept/reject controls and all run scheduling remain manual.