arborist/docs/bench-maxing.md
russell@unturf.com 8d6961fcc1
aborist/arborist
modified:   .gitlab-ci.yml
	modified:   bench/qa_questions.txt
	modified:   bench/qa_sweep.py
	modified:   bench/run.sh
	modified:   docs/TICKETS.md
	modified:   docs/_source/README.md
	modified:   docs/_source/_ext/makefile_targets.py
	modified:   docs/_source/api/cli.rst
	modified:   docs/_source/api/distill.rst
	modified:   docs/_source/api/mesh.rst
	modified:   docs/_source/api/qa.rst
	modified:   docs/_source/api/retrieval.rst
	modified:   docs/_source/api/storage.rst
	modified:   docs/_source/api/substrate.rst
	modified:   docs/_source/concepts.rst
	modified:   docs/_source/conf.py
	modified:   docs/_source/cookbook.rst
	modified:   docs/_source/index.rst
	modified:   docs/_source/license.rst
	modified:   docs/_source/quickstart.rst
	modified:   docs/bench-maxing.md
	modified:   docs/benchmarks.md
	modified:   docs/cti-architecture.md
	modified:   docs/diagrams/aborist-modules.dot
	modified:   docs/diagrams/aborist-modules.svg
	modified:   docs/diagrams/mesh-data-flow.dot
	modified:   docs/diagrams/mesh-epoch-lifecycle.dot
	modified:   docs/diagrams/mesh-epoch-lifecycle.svg
	modified:   docs/diagrams/mesh-group-decisions.dot
	modified:   docs/diagrams/mesh-group-decisions.svg
	modified:   docs/diagrams/mesh-identity-stack.dot
	modified:   docs/diagrams/mesh-secret-envelope.dot
	modified:   docs/mesh.md
	modified:   docs/qa-modes-bench.md
	modified:   docs/seven-point-program.md
	modified:   docs/tickets/ticket-000001-retrieval-keywords-audit-gap.md
	modified:   docs/tickets/ticket-000002-reference-frame-polarity-contract.md
	modified:   docs/tickets/ticket-000003-anchor-class-warrant.md
	modified:   docs/tickets/ticket-000005-label-ladder-migration.md
	modified:   docs/tickets/ticket-000006-bench-emergent-findings.md
	modified:   docs/tickets/ticket-000007-query-layer-hyphen-fold.md
	modified:   docs/tickets/ticket-000008-broad-quantifier-preflight-guard.md
	modified:   docs/tickets/ticket-000009-quantifier-preflight-dag-binding.md
	modified:   docs/tickets/ticket-000010-metacognition-preflight-guard.md
	modified:   docs/tickets/ticket-000011-soft-preflight-hint-sidecar.md
	modified:   scripts/backfill_concepts.py
	modified:   scripts/bench_emergent.py
	modified:   tests/crawler/test_async_web_fetcher.py
	modified:   tests/crawler/test_bridge.py
	modified:   tests/crawler/test_web_fetch.py
	modified:   tests/test_bench_qa_sweep.py
	modified:   tests/test_burn.py
	modified:   tests/test_burn_doc.py
	modified:   tests/test_claim_lattice.py
	modified:   tests/test_cli_render.py
	modified:   tests/test_compress.py
	modified:   tests/test_concepts.py
	modified:   tests/test_dag.py
	modified:   tests/test_directives.py
	modified:   tests/test_distill.py
	modified:   tests/test_distill_recursive.py
	modified:   tests/test_evict.py
	modified:   tests/test_frame.py
	modified:   tests/test_grok_source.py
	modified:   tests/test_html_source.py
	modified:   tests/test_ingest.py
	modified:   tests/test_inspect.py
	modified:   tests/test_journal.py
	modified:   tests/test_keys.py
	modified:   tests/test_llm_context_base.py
	modified:   tests/test_merkle.py
	modified:   tests/test_mesh.py
	modified:   tests/test_mesh_aead.py
	modified:   tests/test_mesh_chain.py
	modified:   tests/test_mesh_cli.py
	modified:   tests/test_mesh_cli_pull.py
	modified:   tests/test_mesh_wire.py
	modified:   tests/test_mesh_wire_e2e.py
	modified:   tests/test_metacognition.py
	modified:   tests/test_migration_audit_mode.py
	modified:   tests/test_providence_source.py
	modified:   tests/test_qa.py
	modified:   tests/test_qa_quality_live.py
	modified:   tests/test_quantifier_caps.py
	modified:   tests/test_quantifier_classifier.py
	modified:   tests/test_quantifier_phase4.py
	modified:   tests/test_quantifier_reminder.py
	modified:   tests/test_query.py
	modified:   tests/test_reclassify.py
	modified:   tests/test_repair.py
	modified:   tests/test_resume.py
	modified:   tests/test_snapshot.py
	modified:   tests/test_soft_preflight.py
	modified:   tests/test_tfidf.py
	modified:   tests/test_vcs_source.py
	modified:   tests/test_verify.py
	modified:   tests/test_verify_json.py
	modified:   tests/test_versioned_ingest.py
	modified:   tests/test_warrant.py
	modified:   tests/test_wikipedia_old.py
	modified:   tests/test_wikipedia_xml.py
	modified:   tests/test_wikitext.py
2026-05-07 09:31:49 -04:00

11 KiB
Raw Permalink Blame History

Bench-maxing — measure deltas, not opinions

The QA-quality bench (make bench-qa) and live functional fixtures (make test-live) are not decoration. They are the discipline that keeps prompt + verifier work honest. Patterns established 2026-04-30 during the JSON-mode hardening journey:

  • Bench before AND after every change. Single-sample bench has ±20pp noise; n=3 narrows the band but signal under 5pp is still noise. The only way to know a change moved the needle is to run the same harness on identical inputs immediately before and immediately after. Don't skip this — opinions about whether a prompt change "should help" are routinely wrong.

  • Slight prompt adjustments are fine — local-minima are not. Prompts can be nudged with a worked example or a single-sentence rule. They should NOT be padded with many rules that incentivize empty / over-cautious output ("if uncertain, omit" can collapse the model into refusing valid answers). When in doubt, change one thing per bench cycle.

  • Avoid negation in every prompt. Small instruction-tuned models (Hermes-3-8B observed) struggle with "do NOT X" / "never Y" / "without Z" — the negation often pattern-matches away or inverts under attention. Rewrite every rule to its positive form. Some swaps that work:

    • "Do not invent IDs" → "Use only the IDs that appear in the EVIDENCE blocks above"
    • "Never begin a line with E1:" → "Each line begins with the claim text, then a bracket tag"
    • "No commentary, no preamble" → "Your output begins with { and ends with }"
    • "If no evidence supports a claim, omit the claim" → "Emit only claims that an EVIDENCE block directly supports"
    • "No double-quote characters in text" → "Write text as plain prose; punctuation appears in the source span the runtime renders for you"

    Audit all the *_system_prompt and *_grounding_reminder policy fields for residual negation when a model behaves erratically — it is the cheapest fix in the prompt-iteration toolkit.

  • Bench is the scoreboard; live fixtures are the gates. Bench measures aggregate (STRICT/HYBRID/UNGROUNDED counts across N×Q samples). Live fixtures (tests/test_qa_quality_live.py) gate on specific known-good answers. When a change improves things the bench number climbs AND every fixture passes; when something regresses the bench falls AND named fixtures fail by name, pointing at where the regression landed.

  • Self-heal beats retry. When the model emits malformed output (truncated JSON, trailing comma, partial key), repair the artifact rather than re-running the LLM call. Self-healing preserves whatever partial content the model produced and lands in the unverified bucket honestly. Retrying spends another inference round and may produce identical garbage. See _repair_truncated_json in arborist/qa/verify.py for the pattern: walk once tracking string state + bracket stack, close / drop / balance at end-of-input. Conservative repairs only — never insert content, never fabricate keys.

  • Name the failure → fix in code → re-bench → confirm. Each bench-driven commit should reference the specific failure mode it addresses (e.g. "Apollo runaway: 3/3 → 2/3" or "JSON hallucinated near-miss content-IDs on cross-doc relationships"). When the next bench shows the named failure didn't budge, you fixed the wrong thing. Name it again, try again.

  • Honest verdicts beat optimistic ones. A change that drops STRICT count by 5 but moves those 5 to HYBRID with real grounding is a WIN — false-positive STRICTs are corruption. The bench grade improves when the verifier reports closer to ground truth, not closer to 100%. Trust HYBRID with smell-sidecar warnings over STRICT-with-bogus-citation every time.

  • Per-question fixtures > marker-string assertions. When designing a live fixture, prefer entity-presence checks ("Burns in the answer", "October or 1991") over byte-identical matching. Hermes is non-deterministic; the right entity in the answer is the gate, not the exact phrasing. When a fixture starts failing intermittently, treat it as quality drift on that question shape and tighten retrieval / verifier upstream rather than loosening the test.

  • Old maps vs runtime maps — move authority toward runtime artifacts. Every base model carries old maps: trained patterns from the time the weights froze. It "knows" how API X used to work, what JSON shape Y used to take, which ritual citation phrase was popular. Those priors collide with the present-day runtime — the actual retrieved chunks, the current evidence map, the live policy hash, the ID space the runtime built this minute. When the model and the runtime disagree, the runtime wins. Engineering moves authority OUT of the model's old map and INTO runtime artifacts:

    • Pointer IDs (E1, E2, …) the runtime mints, so the model can't fabricate a content-addressed hash that "looks right."
    • Source spans the runtime interpolates by (chunk_root, offset_start, offset_end), so the model never owns the quote text.
    • Evidence maps the retrieval pipeline assembles per-query, so the model can't self-supply context from training memory.
    • Policy hashes that fold prompt + verifier + retrieval choices into the cache_key, so old governance can't silently reuse new-rule records.
    • Hard checks run by the verifier, never the model self-grading.

    When a defect smells like "model brought a stale map," the fix is to make the runtime more authoritative, not to negotiate with the model's prior. Hermes' content-addressed-evidence-id hallucination (commit bb8450d, 2026-04-30) is the canonical case: the fix was to swap the prompt-facing surface to runtime-minted pointer IDs the model can't fabricate, not to add an "are you sure?" step.

  • Doc captures the journey, not just the end state. docs/qa-modes-bench-2026-04-30.md shows the day's progression (baseline → retry → trim-and-verify → pointer-IDs → stop-sequence → self-heal). docs/qa-modes-bench.md continues the journey through Rule 8 promotion, anchor-class warrant generalization, per-mode context budgets, and the bench-harness upgrades named below. Each row is a named failure + fix + bench delta. Future-you (or a new agent) can read the docs and see WHY each commit landed, not just WHAT.

Bench harness — the speed playbook

Patterns added 2026-05-02. The bench is now the inner-loop tool; treat it like a CI gate, not a once-a-week ritual.

  • Sample-level shuffled scheduling, not cell-grouped. Cell-grouped scheduling (run all n samples of one question/mode back-to-back before the next cell) confounds two things: (1) it correlates the n samples — vLLM's continuous batcher fills with same-cache_key requests, KV-cache locality and queue load become shared signal across "samples 1-3"; (2) it starves the batcher of diverse work, so per-call latency stretches and aggregate throughput drops. Sample-level scheduling — every (q, mode, sample_idx) is its own task, shuffled with a deterministic seed — gives true i.i.d. variance for n>=3 and delivers the batcher uncorrelated requests. Per-cell Lock dict serializes the rare adjacent-same-cell case so burn-then-write on the shared cache_key never races. Measured on 2026-05-02: +58% throughput at the same concurrency.

  • Persistent HTTP client. The OpenAI-compatible client used to build a fresh httpx.Client per chat_completion call, paying a TLS handshake on every request. Move it to __init__ so HTTP/1.1 keep-alive holds the TCP+TLS connection across calls from the same client instance. httpx's pool is thread-safe; one client serves all worker threads. Saved 1-2 min on a 426-call bench, more on longer sweeps.

  • Concurrency tuning, empirically. vLLM has a sweet spot — more concurrent requests fill the batch better, but only up to the point where per-call latency growth outpaces parallelism gain. On hermes.ai.unturf.com the sweep result (15-task smoke fixture, 2026-05-02):

    c=3   102s   8.8 tasks/min  ← peak
    c=4   106s   8.5 tasks/min  ← within 4%; chosen for runs
    c=5   119s   7.6 tasks/min  ← 12% slower
    c=6   185s   4.9 tasks/min  ← 45% slower (saturation)
    

    Run a small smoke at multiple concurrency levels before committing to a long full-bench. The peak shifts when vLLM is upgraded, when the corpus changes the average context size, or when other tenants load the endpoint differently.

  • Per-mode max_context_chars from bench feedback. The bench's "recommended context budget" table (per-mode strict-rate by context-size bucket) is the substrate's voice telling the policy where each mode peaks. As of the 2026-05-02 bench: quote 8-16KB, pointer 16-32KB, JSON 32-64KB. Those defaults live in DEFAULT_QUERY_POLICY["max_context_chars_by_mode"] and fold into governance_policy_hash so any retuning cleanly partitions the cache namespace. Tighten the cap when bench shows degradation past a smaller bucket; loosen it when JSON starts paging in 64-128KB. The harness reports the table on every run — let it drive the policy, not the other way around.

  • --seed for reproducibility. Same --seed reproduces the shuffle. Useful when a regression bench needs to be re-run against the exact same task order; useful when the --resume path needs the post-resume order to align with the pre-kill order. Default --seed 0.

  • --resume <jsonl-path> — stop/start-able. A 60-90 minute full bench is annoying to re-run from zero when something interrupts (Ctrl-C, kernel panic, network outage). The bench reads the existing JSONL, skips already-completed (q, mode, sample_idx) triples, appends fresh rows to the same file, and re-renders the markdown summary from the union. Same --seed required so the remaining tasks come out in the original order. Stops can now be cheap.

  • Smoke fixture for inner-loop. bench/qa_questions_smoke.txt carries 5 questions covering all anchor classes — date, place, relation, entity-list, why-cause — each chosen because pointer mode currently fails 100% on them and JSON aces 100%. Run via make bench-qa-smoke (~1-3 min wall-clock). The smoke is the inner-loop tool for prompt iteration; the full 71-question bench stays the scoreboard. The bench-as-build-gate doctrine: smoke before full.

  • pytest -n auto for the test suite. pytest-xdist lands the test suite in 10s instead of 36s on this hardware (3.6× speedup). Wired via make test. Cycle-time enabler for prompt-iteration loops where the test suite gates the bench.