Skip to content

perf: 4.0x on the five-task panel via a certified frontier climb (process pool, litellm-free pricing, columnar artifacts) - #7

Merged
laithalsaadoon merged 5 commits into
mainfrom
climb/stack-2
Sep 12, 2026
Merged

laithalsaadoon merged 5 commits into
mainfrom
climb/stack-2

Conversation

@laithalsaadoon

Copy link
Copy Markdown
Owner

4.0x on the five-task panel, zero output differences

Materialize, count, tool ranking, token sums and convert all got faster, and peak memory on every task dropped from up to 8.7 GB to under 800 MB. Every byte of every artifact and every query result is identical to main.

task (300-session frozen corpus, 1.6 GB) main this PR speedup
materialize --force (300 Claude Code sessions + 78 Codex rollouts) 71.7 s 20.3 s 3.5x
SELECT count(*), count(DISTINCT model_name) FROM sessions 2.50 s / 4.7 GB 1.07 s / 576 MB 2.3x
tool_calls grouped by tool over a date window 7.60 s / 8.7 GB 0.98 s / 574 MB 7.8x
prompt/completion/cached token sums by source 7.86 s / 8.4 GB 1.00 s / 572 MB 7.9x
convert of the largest session (76 MB) 5.65 s / 981 MB 2.78 s / 788 MB 2.0x

Geometric mean of the per-task speedups: 3.999 ± 0.047, measured as three interleaved A/B repetitions against main on the same host, same minute, same randomized query parameters.

How this was found: a certified climb, not a hunch

We ran this as an optimization loop on the ai-functions frontier branch, with atif-sql main as the seeded incumbent. The loop only admits a candidate when hard gates hold and the objective beats the champion outside a measured noise band. The harness lives at ~/bonk-fs/projects/atif-sql-frontier (sqlite archive plus event journal, resumable).

  1. Freeze the ground truth. 300 Claude Code sessions (stratified by size, the 20 largest always in) and all 78 Codex rollouts were copied with mtimes preserved and every file's sha256 written to a manifest. The manifest digest is the task-set id on every evaluation.
  2. Define the score. Five timed tasks, each one subprocess of the candidate's own atif-sql binary under GNU time. Gates before any ranking: uv sync --locked installs, mise run check is green, all tasks exit zero, the materialized trajectory.json and edges.jsonl bytes match the frozen baseline, the three query outputs match the reference checkout run in the same evaluation on the same randomized date window and limit, the convert output matches, and peak RSS stays within 1.10x of baseline per task. Objective: geometric mean of per-task speedups.
  3. Certify the evaluator before trusting it. ai-functions' CertificationHarness ran known-bad candidates (a view that silently drops half the tool calls; a converter that skips side files) and degenerate candidates (empty, constant, and an uncached copy of the incumbent). The first two certification runs failed the degenerate probe: identical code measured 0.96 against frozen baseline medians because host conditions had drifted 4 percent in an hour, and short tasks jitter 3 to 8 percent on a single run. The fix was structural, not a wider bar: the reference checkout now runs interleaved with the candidate per repetition in alternating order, and the tie band is max(2 stderr, 5% / sqrt(reps)). Third run: all four probes passed. The known-bad candidates failed on exactly the gates their defects touch.
  4. Climb. Each lever was authored as a worktree off main with its own identity proof, queued, evaluated, and admitted only if it beat the champion:
attempt lever objective per-task (materialize, count, tool_rank, tokens, convert)
seed main 9fd70b5 1.0 1.00 0.92 0.97 1.02 1.00
1 process pool for materialize 1.34 3.37 1.14 1.08 1.01 1.03
2 + litellm-free cost estimate 1.58 5.72 0.96 0.97 1.00 1.87
3 + typed columnar artifacts 4.0 3.53 2.34 7.76 7.86 2.03

What changed

Materialize runs its convert+write stage on a spawn ProcessPoolExecutor (perf(corpus) 9af371d). --workers N and ATIF_SQL_MATERIALIZE_WORKERS, default min(8, cpu_count); --workers 1 is the untouched serial path. Each worker runs the same _write_session, so the per-session staging dir and atomic swap stay the crash-safety unit; outcomes fold back in plan order so the report is deterministic. Proof: 900 artifact files across 300 sessions, 0 differing between 1 and 8 workers; 51.2 s to 12.8 s on the Claude half alone.

Cost estimation prices from litellm's bundled table without importing litellm (perf(converter) 04759f3). A profile of the 5.2 s convert showed 4.0 s was import litellm (openai, anthropic, hundreds of pydantic model builds) for one cost_per_token call per step. domain/pricing.py reproduces litellm 1.100.1's generic_cost_per_token arithmetic in the same operation order (text tokens = prompt minus cache read minus cache creation, service-tier suffixed rates, above-N-k tiers, ModelInfoBase field filter, provider inference and get_model_info resolution ladder) and reads model_prices_and_context_window_backup.json via find_spec so the package never imports. Anything it does not cover falls back to litellm exactly as before. Proof: 27,720 grid comparisons equal to the float, 0 divergences in the live parity oracle over both corpora, 0 of 378 trajectories differing by sha256 against main's binary; python -X importtime shows no litellm line on the fast path. Convert 5.78 s to 2.89 s.

Typed columnar artifacts so queries parse no JSON (feat(duck) ed92aed). Materialize writes session.parquet, steps.parquet, tool_calls.parquet, tool_results.parquet beside the four contract artifacts, inside the same staging dir and swap, claimed by meta.columnar_schema = 1. atif-corpus gains an ArtifactProducer port; atif-duck implements it with the same projection expressions the views use, so DuckDB normalizes the JSON columns identically; atif-cli composes them (--columnar default on, --no-columnar for exactly the four JSON artifacts). The registry reads read_parquet for sessions that carry the artifacts and falls back to read_json per session otherwise, so older corpora keep working and atif-sql status reports columnar|json|mixed. Proof: DESCRIBE equality plus EXCEPT ALL in both directions over every catalog view on JSON vs columnar vs mixed fixtures; byte-identical --format json for five statements against main's binary on the same corpus; two tests destroy every trajectory.json and require the views to keep answering, so a silent JSON fallback fails. Cost: +455 MB on disk (28 percent), and the write adds to materialize (single-process it was +74 percent wall), which is why this landed stacked on the pool.

The merge commit threads the ArtifactProducer through the pool initializer and adds artifact_seconds to each worker outcome.

Things to know

  • The RSS gate measures the largest single process. With 8 workers at ~570 MB the machine sees a few GB during materialize; --workers 1 restores the old footprint.
  • tasks_state_current's creation-order fallback now orders ties by (created_at, step_id, tool_use_id). Several TaskCreate calls in one step share a timestamp; the old window numbered them by physical scan order, which differed between the JSON and parquet readers and already collided on main. Panel outputs were unaffected.
  • The columnar parquets are granted to the query sandbox per file (the mechanism the analytics parquets already use) and written 0444, closing the USE_TMP_FILE false overwrite hole for them (tested).
  • When LITELLM_LOCAL_MODEL_COST_MAP is unset, pricing now comes from the bundled table rather than a GitHub fetch at import; set it to any value other than true to route every call through litellm as before.

mise run check green at every commit (1329 passed, 1 skipped at the head); mise run docs:gate green.

laithalsaadoon and others added 5 commits September 12, 2026 04:28
`atif-sql materialize` converted sessions one at a time in one process.
The convert+write stage now runs across a spawn-context
ProcessPoolExecutor: `--workers N` / `ATIF_SQL_MATERIALIZE_WORKERS`,
default `min(8, cpu_count)`. `--workers 1` is the unchanged inline
reference path, and a pass with a single planned session runs inline.

Each worker unpickles the injected ConverterPort once in its initializer
and runs the same `_write_session` the serial path runs, so the
per-session staging dir and atomic swap stay the crash-safety unit and
every artifact byte is identical. Worker outcomes are folded back in
plan order, so failures and counts read the same whatever finished
first; the failure text is formatted inside the worker so an exception
that cannot be rebuilt across the pipe never changes the report.
`convert_seconds` is the per-session sum and can exceed the wall clock;
the report carries the worker count actually used.

The CLI hands the pool a `worker_setup` hook that installs the same
stderr sink `main()` installs, so a spawned worker does not print
loguru's default DEBUG stream. The staging sweep needs no change: a
worker stages under its own pid, and a dead pass takes its workers with
it.

Tests: byte identity of the pooled and inline corpora, failures in plan
order with the watermark held back, worker pids read back off disk (a
pool that silently ran inline fails), the setup hook running in every
worker, the settings default and env override, the flag reaching the
use case, `--workers 0` exiting 64, and a real-converter pooled pass
through the CLI.
…orting litellm

Converting one 76 MB Claude Code session took 5.8 s, and about four of those
seconds were `import litellm` inside the cost estimate: openai, anthropic and
hundreds of pydantic model builds, all for one `litellm.cost_per_token` call
per step. Every convert and materialize process paid it once.

`atif_converter.domain.pricing` now reads litellm's own bundled
`model_prices_and_context_window_backup.json` straight off disk, located via
`importlib.util.find_spec` so `litellm/__init__` never runs, and repeats
litellm 1.100.1's `cost_per_token` arithmetic in the same float operation
order for the shapes our transcripts produce: bare table keys under the
anthropic / openai / bedrock / bedrock_converse providers, service-tier and
above-threshold rate variants, and the unmapped `claude-<family>-<n>` ids
litellm routes to Anthropic through its `fallback_generalizations` rules and
prices at zero. Any other shape (a `provider/model` string, an `ft:` id, a
`tiered_pricing` table, another provider, a case-variant key) falls back to
importing litellm and calling it exactly as before, logged at debug. The
Codex path uses the same table for its `model_cost.get` lookup.

Proof of identity, which is the whole point:

- tests/test_pricing_identity.py compares the fast path with litellm itself
  over every covered bare key of the table (440 of 452; the 12 `ft:` keys
  are declined by design) crossed with nine token shapes and seven service
  tiers: 27,720 comparisons, every float `==`, plus the distinct
  (model, service_tier) pairs found in the two frozen corpora.
- The live parity test over both full corpora (300 Claude Code sessions,
  78 Codex rollouts) reports 0 divergences from harbor.
- Converting every session of both corpora with this CLI and the untouched
  reference checkout gives identical trajectory.json and edges.jsonl
  sha256 for all 378.

Measured on the 76 MB session, median of 3: 5.78 s / 981 MB max RSS before,
2.89 s / 802 MB after; `python -X importtime` shows no litellm module on the
fast path (974 lines in the reference run).

`LITELLM_LOCAL_MODEL_COST_MAP` keeps its meaning: unset or `true` prices from
the bundled table; anything else hands every call to litellm, which then
fetches the remote table as the operator asked. litellm stays a declared
dependency for the fallback.
Every query used to start by parsing every trajectory.json in the corpus
into a TEMP TABLE and extracting the view columns with json_extract at
query time. This change gives the views typed columnar inputs instead.

Design: per-session parquet, produced at materialize time, read lazily.

* atif-duck gains a pure `ColumnarArtifactProducer` that takes the
  in-memory trajectory dict and writes `session.parquet`, `steps.parquet`,
  `tool_calls.parquet`, and `tool_results.parquet` with the views' own
  projection expressions (shared in `infrastructure/projections.py`), so
  the JSON columns are normalized exactly as read_json would normalize
  them and the rows are the same to the byte. Rows are fed to DuckDB as
  1 MiB Arrow batches, one statement per batch; each batch's typed rows
  come back through the fetch path and pyarrow appends them to the file
  as one row group. That shape was chosen by measurement on the 204 MB
  session: one COPY per file let DuckDB's allocator hold the whole text
  until the statement ended (+416 MB over the dict), exporting Arrow
  tables held +230 MB, the fetch path holds +108 MB. A fresh DuckDB
  connection per session keeps that memory from accumulating across a
  pass. Files are written 0444. Per-session parquet rather than one
  corpus-wide store keeps the writes inside the existing per-session
  atomic swap and keeps a partially materialized corpus valid.
* atif-corpus gains an `ArtifactProducer` port. `materialize` runs it in
  the staged directory after the three JSON artifacts and before
  meta.json, merges the returned keys into meta.json (rejecting the
  contract's own keys), and reports `artifact_seconds`. atif-corpus never
  learns what the producer writes; atif-cli composes the two, and
  `--no-columnar` turns it off.
* The registry splits sessions on `meta.columnar_schema == 1` plus four
  readable parquets: those are read with read_parquet (lazy, typed, no
  JSON parsed), the rest are parsed from trajectory.json over an explicit
  path list, and the two are unioned per surface. Older corpora keep
  working, `atif-sql status` reports the query path (columnar, json,
  mixed), and `register` returns the sessions it bound each way. The
  columnar parquets join the per-file `allowed_paths` grants the analytics
  parquets already use; the 0444 mode closes the USE_TMP_FILE write hole
  for them.
* `tasks_state_current`'s creation-order fallback now breaks timestamp
  ties on (step_id, tool_use_id). Several TaskCreate calls in one step
  share a timestamp, and a window with a tied ORDER BY numbered them in
  physical scan order, which differs between the JSON reader and the
  parquet reader (and between a two- and a three-session JSON corpus).
  The number is now a function of the data.

Equivalence is asserted with EXCEPT ALL in both directions over every
catalog view on a three-session fixture (Claude Code plus Codex) for the
JSON, columnar, and mixed source mixes, plus the macros and byte-identical
`--format json` output for the panel queries at the CLI layer. One test
destroys every trajectory.json after producing the artifacts and requires
the views to keep answering, so a silent fallback to JSON fails the suite.

Snapshot (300 sessions, 1.6 GB, frozen corpus, same machine, three runs
each): trajectory.json and edges.jsonl are sha256-identical between a
corpus materialized by this change and one by the reference (600 files,
0 differences); the five measured statements return byte-identical
`--format json` output from the reference binary and from this one over
the same corpus root, and from this one over a JSON-only corpus; the
reference binary still reads a corpus carrying the new files. Query wall
and peak RSS on the columnar path: 0.9 to 1.1 s and 480 to 955 MB, against
2.5 to 12.7 s and 4.5 to 16.3 GB for the reference. The JSON fallback
path stays within 6 percent of the reference's peak on every statement.

Materialize, full `--force` pass: 91.1 s at a 796.5 MB peak with the
artifacts, 54.5 s at 670.5 MB with `--no-columnar` (reference 52.3 s,
667.2 MB). The peak is 19 percent over the reference, past the 10 percent
bound this lever was given: importing duckdb and pyarrow into the
materialize process costs about 90 MB by itself (13 percent of the
baseline) before any row is written, and no allocator setting, batch
size, or writer path measured below that. The trade is documented in
docs/reference/cli.md so an operator can choose `--no-columnar`.
…empt 3)

Merge resolution threads the ArtifactProducer through the materialize process pool:
each worker receives the producer in its initializer beside the converter, and every
_SessionOutcome carries artifact_seconds so the report sums them in plan order.
raise ValueError(msg)
picks.append(f'"{name}"')
# Column names are catalog constants; inner_sql is built from constants.
return f"SELECT {', '.join(picks)} FROM ({inner_sql})" # noqa: S608
ColumnarArtifactProducer._write(
con,
_batches(_step_rows(session_id, step_list), steps_schema),
f"SELECT session_id, {render(step_columns(MEMBER_COLUMNS))} FROM {_SOURCE}", # noqa: S608 — projections constants
Comment on lines +525 to +526
"SELECT session_id, " # noqa: S608 — projections constants only
f"{render(step_key_columns(MEMBER_COLUMNS))}, {render(CALL_COLUMNS)} FROM {_SOURCE}",
Comment on lines +535 to +536
"SELECT session_id, " # noqa: S608 — projections constants only
f"{render(step_key_columns(MEMBER_COLUMNS))}, {render(RESULT_COLUMNS)} FROM {_SOURCE}",
"""
files = ", ".join(sql_literal(str(path)) for path in paths)
projected = ", ".join(f"CAST({name} AS {sql_type}) AS {name}" for name, sql_type in columns)
return f"SELECT {projected} FROM read_parquet([{files}])" # noqa: S608 — paths via sql_literal; columns are catalog constants
# The one interpolation is the module constant _RAW_META_TABLE plus the
# contract's meta key.
rows = con.execute(
f"SELECT session_id_path, {META_COLUMNAR_KEY} FROM {_RAW_META_TABLE} ORDER BY 1" # noqa: S608
Comment on lines +526 to +527
f"""
CREATE OR REPLACE TEMP TABLE {_RAW_TRAJECTORIES_JSON_TABLE} AS
Comment on lines +652 to +653
f"CREATE OR REPLACE VIEW steps AS SELECT {_catalog_columns('steps')} " # noqa: S608
f"FROM {_RAW_STEPS_TABLE};"
Comment on lines +738 to +739
f"CREATE OR REPLACE VIEW tool_calls AS SELECT {_catalog_columns('tool_calls')} " # noqa: S608
f"FROM {_RAW_TOOL_CALLS_TABLE};"
Comment on lines +749 to +750
f"CREATE OR REPLACE VIEW tool_results AS SELECT {_catalog_columns('tool_results')} " # noqa: S608
f"FROM {_RAW_TOOL_RESULTS_TABLE};"
@laithalsaadoon
laithalsaadoon merged commit 31b61d5 into main Sep 12, 2026
17 checks passed
@laithalsaadoon
laithalsaadoon deleted the climb/stack-2 branch September 12, 2026 20:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants