Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
eb396c4
Record time as well as money, and stop the ledger vanishing with its …
jonfroehlich Aug 19, 2026
7919219
A compute ledger: GPU-hours and dollars from sacct, not from prose (#…
jonfroehlich Aug 19, 2026
97db49d
Promote the rule to CLAUDE.md, and write down what the ledgers now ho…
jonfroehlich Aug 19, 2026
b550c65
Reconcile the ledger against the bill, so a silent no-write is caught…
jonfroehlich Aug 19, 2026
33321aa
Back-fill the klone compute ledger: 2,684 GPU-hours, and sacct -D is …
jonfroehlich Aug 19, 2026
37b7faa
Put #139's recovered $70.41 in the ledger, marked as a recovery (#143)
jonfroehlich Aug 20, 2026
b190da0
Total the compute ledger through the last row per key, not every row …
jonfroehlich Sep 3, 2026
749d345
Report a recorded recovery separately from an unexplained gap (#143)
jonfroehlich Sep 3, 2026
baf0b49
Read .env from the main checkout as well, not just the running worktr…
jonfroehlich Sep 3, 2026
fbba3b7
Date the two ledger status lines, and say which prices carry a source…
jonfroehlich Sep 3, 2026
aa4a142
Say in ledger.py what reconciliation now does with a recovered row (#…
jonfroehlich Sep 3, 2026
7a3474a
Merge origin/main into feat/cost-and-time-accounting-143: keep both l…
jonfroehlich Sep 17, 2026
2beb175
Re-derive the 496.5 GPU-hour figure as the baseline-only snapshot it …
jonfroehlich Sep 17, 2026
d7bedf1
reconcile: tokens logged against a model the bill never saw are not "…
jonfroehlich Sep 17, 2026
a064d49
slurm_usage: a dump must name its account; never stamp the local logi…
jonfroehlich Sep 17, 2026
dfaa8e2
ledger_totals reads the last row per job, and a PENDING row is not an…
jonfroehlich Sep 17, 2026
2b3452b
Pin the sacct dump by sha256, and say how big the compute ledger real…
jonfroehlich Sep 17, 2026
59a31db
CLAUDE.md: date the ~200x ratio and say where the 0.02% and 62% come …
jonfroehlich Sep 17, 2026
7b5ef17
compute_cost.md: restore the line continuations and the opening parag…
jonfroehlich Sep 17, 2026
4a40669
model_comparison.md: the --from-file example needs --user now (#143)
jonfroehlich Sep 17, 2026
b5c5b46
Pin the sacct dump and both ledgers -text, so the sha256 survives an …
jonfroehlich Sep 17, 2026
3321dd2
compute_cost.md: 07:07 is when the 496.5 line was written, not commit…
jonfroehlich Sep 17, 2026
3d10e8d
compare.py: write the usage row through ledger.append_rows (#143)
jonfroehlich Sep 17, 2026
410278e
parse_sacct: say how many lines it dropped, and show the first (#143)
jonfroehlich Sep 17, 2026
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
11 changes: 11 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ stage_two/run_a_84_events/events.out.tfevents.* binary
# the cluster wrote them, on every platform.
docs/data/rampnet1_stage1_run/* -text

# The committed sacct dump is the replication input for analysis_out/compute_log.jsonl
# and its sha256 is pinned in docs/compute_cost.md and asserted by
# tests/test_slurm_usage.py. Same reasoning: a core.autocrlf=true clone would check it
# out with CRLF and the pin would fail for a file that is byte-correct (#143).
docs/data/compute/* -text

# Both ledgers are append-only and written LF by rampnet.ledger.append_rows on every
# platform. Without this, an autocrlf=true clone checks them out CRLF and every later
# append leaves a mixed-ending file on disk.
analysis_out/*.jsonl -text

# The manifests themselves are ours, and stay LF. Listed after the rule above
# because the last matching pattern wins.
docs/data/rampnet1_stage2_run/SHA256SUMS text eol=lf
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ analysis_out/op/*
# becomes unrecoverable rather than merely inconvenient. Append-only; never rewrite
# a past line, since each one is a measurement.
!analysis_out/usage_log.jsonl
# The compute half of the same record (scripts/analysis/slurm_usage.py, #143): one
# row per Slurm job allocation -- elapsed, GPUs, GPU-hours, and dollars on the
# clusters that bill. Unlike the token ledger this one is partly back-fillable from
# `sacct`, but only while the cluster still retains the job records. Same rules:
# append-only, one row per (cluster, job id, start), last row per key wins.
!analysis_out/compute_log.jsonl
# Crop-window sizing eval summaries (#114): per-rule containment/context stats plus
# the sha256 of the per-box CSV each summarizes (the CSVs themselves regenerate).
# The glob also covers per-bundle runs against box_gallery extent gold (#116),
Expand Down
47 changes: 47 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,53 @@ When a task involves human raters, assume from the start that **a second rater w
the two will be compared** — per-rater files, a stable item list, and an agreement script, not a
single blob that has to be reverse-engineered later.

## Record what every experiment cost — time *and* money — as it runs

**Any experiment on a non-free model or non-free compute records both its wall-clock time and
its spend, at the time it runs.** A paper reports cost alongside accuracy, and neither number
survives being left for later: a re-run reads the detection cache, makes no calls, and returns
in seconds, so it reproduces neither the token counts nor the runtime.

- **Paid APIs.** `compare.py` appends one row per leg to `analysis_out/usage_log.jsonl`
(committed): tokens, dollars, wall-clock, seconds-per-panorama, the rig, and the build that
actually served it. `--usage-log none` requires `--allow-unrecorded-spend`. Prices live in
`scripts/model_comparison/pricing.py` and are **verified-only** — each rate carries the date
it was checked and, for compute, the page it came from, never memory.
- **Free models still cost time.** OWLv2, Grounding DINO, Qwen, Molmo and the YOLO arms bill no
tokens and burn real GPU-hours. They write the same row with `paid: false`, because "what did
this cost" has two units and only one of them is dollars.
- **Cluster jobs.** `scripts/analysis/slurm_usage.py` scrapes `sacct` into
`analysis_out/compute_log.jsonl` — elapsed, GPUs, GPU-hours, and dollars on the clusters that
bill. Run it after a cluster run, not at the end of the project.
- **A ledger is not a commit, and it has to outlive the run.** Both default into the **main
checkout**, never a scratch worktree: a worktree is deleted when its session ends, and that is
exactly how one leg spent $70.41 and left no row (#139, #143). Committing the ledger is part
of finishing the experiment.
- **A missing record is an emergency with a deadline.** Vertex telemetry retains ~6 weeks
(`scripts/analysis/vertex_usage.py`) and `sacct` retention is finite too, so recover a missing
number the day you notice it. Recovery is per-model per-day, so **per-split attribution is
permanently gone** even after a successful pull — layer 3 rescues the total, never the
breakdown.
- **A recovered number belongs in the ledger, and belongs marked.** Write it as a row with
`kind: "recovered"` (`rampnet.ledger.RECOVERED`) carrying the billed total *minus what the
surviving rows already account for*, so the ledger sums to the bill rather than double
counting. Two rules follow and they pull opposite ways: **cost totals include it** — omitting
#139's $70.41 under-reported the benchmark's Claude spend by ~200x when it was found (measured
Claude rows in `analysis_out/usage_log.jsonl` totalled $0.34 on 2026-08-19; with the Laurens
legs of 2026-09-04 they total $13.12 and the same omission would be ~5x) — but **reconciliation must
never count it as logged**, because a recovered row was read off that same bill, so counting it
that way compares the bill against itself and reports `ok` for the exact gap the check exists to
find. `--reconcile` totals it in a separate column and subtracts it before the verdict, so a gap
already found and written down reads as `ok (1 recovered)` and only the unexplained remainder is
called out — a check that reports the same handled gap as an emergency every run is a check
people stop reading. A recovered row carries no `bundle` and no `elapsed_s`; those were never in
the telemetry.
- **Token counts and GPU-hours are the durable facts; dollars are estimates** and the billing
console is authoritative. Estimate input tokens from geometry when you must (deterministic —
it came within 0.02% for a 984-panorama leg); **never estimate output**, which is thinking
plus box count and ran 62% high when tried. Both figures are from the #139 recovery, worked in
the body of #146; they are not re-derived by any committed script.

## Environment & commands

- Conda env (Linux + CUDA 11.8; env file pins linux-64 packages — this does not run natively on Windows):
Expand Down
Loading
Loading