Skip to content

feat(evolution): 🔍 attribute the layer build behind monoprop_LAYER_PROFILE - #232

Closed
diagonal-hamiltonian wants to merge 1 commit into
mainfrom
pr/layer-profile
Closed

feat(evolution): 🔍 attribute the layer build behind monoprop_LAYER_PROFILE#232
diagonal-hamiltonian wants to merge 1 commit into
mainfrom
pr/layer-profile

Conversation

@diagonal-hamiltonian

Copy link
Copy Markdown
Collaborator

🤖 AI text below 🤖

Summary

Adds monoprop_LAYER_PROFILE: a per-partition attribution of the layer build, off by default.

Before this there was no way to say where a layer build's time went. The instrumented phases summed to ~6% of wall, so a phase split described almost nothing, and every question about the engine had to be answered with the end-to-end wall — an instrument whose observed spread on build_graph is 2.86×, and which needs ~1590 paired repetitions to resolve a 3% effect. The phase timers resolve the same 3% in about 5.

This is the instrument the rest of this work is measured with, and it is useful on its own for anyone profiling the engine.

Changes

Two instruments with deliberately different costs:

  • Phase timers and per-gate counters — O(1) clock reads per gate, with per-term counters kept in locals and folded into the slot once per gate, so the emit loop keeps its registers.
  • A store population sweep (k/d histograms, paired and overflow fractions) — O(n), so it runs only on power-of-two gate indices, which also yields the population's growth curve for free.

layer_ns brackets the whole of build_layer, so layer_ns minus the phases is the unattributed remainder. Without it the split is not falsifiable: phases that sum to a small fraction of wall can be individually accurate and collectively meaningless. index_ns covers the inverted-index rebuild, which is not a layer phase but is charged per gate and had never been attributed to anything.

Slots are heap-allocated per thread and owned by the registry, never freed: a partition master can outlive or predecease the dump, and a dangling slot would be a use-after-free in a diagnostic. The registry dumps from its destructor rather than via atexit so the ordering is sound — slot() evaluates config::get() before Registry::add(), so Settings is constructed first and therefore destroyed last.

Cost when off

slot() returns nullptr when the knob is unset, so every call site is one predictable branch and no clock is read. It changes no result.

Output goes to stderr, which pytest's fd-level capture discards without -s. Documented in docs/content/docs/features/parallelism.mdx.

Verification

ctest -L serial — 208/208 green. pytest tests --with-mpi green at 1, 2 and 4 ranks.

Checklist

  • Tests added or updated to cover the changes
  • Documentation updated (docstrings, docs/, CONTRIBUTING.md) if needed
  • CHANGELOG / release notes updated if applicable — release notes are generated from the PR title

AI/LLM disclosure

  • I used the following tool to help write this PR description: Claude Code (claude-opus-5)
  • I used the following tool to generate or modify code: Claude Code (claude-opus-5)

…OFILE

Before this there was no way to say where a layer build's time went. The
instrumented phases summed to ~6% of wall, so a phase split described
almost nothing and every question about the engine had to be answered
with the end-to-end wall -- an instrument whose observed spread on
build_graph is 2.86x, and which needs ~1590 paired repetitions to
resolve a 3% effect. The phase timers resolve the same 3% in about 5.

Two instruments with deliberately different costs:

  * phase timers and per-gate counters -- O(1) clock reads per gate, with
    per-term counters kept in locals and folded into the slot once per
    gate so the emit loop keeps its registers;
  * a store population sweep (k/d histograms, paired and overflow
    fractions) -- O(n), so it runs only on power-of-two gate indices,
    which also yields the population's growth curve for free.

`layer_ns` brackets the WHOLE of build_layer, so layer_ns minus the
phases is the unattributed remainder. Without that the split is not
falsifiable: phases that sum to a small fraction of wall can be
individually accurate and collectively meaningless. `index_ns` covers
the inverted-index rebuild, which is not a layer phase but is charged
per gate and had never been attributed to anything.

Slots are heap-allocated per thread and owned by the registry, never
freed: a partition master can outlive or predecease the dump, and a
dangling slot would be a use-after-free in a diagnostic. The registry
dumps from its destructor rather than via atexit so the ordering is
sound -- slot() evaluates config::get() before Registry::add(), so
Settings is constructed first and therefore destroyed last.

Off by default and it changes no result: slot() returns nullptr when the
knob is unset, so every call site is one predictable branch and no clock
is read. Output goes to stderr, which pytest's fd-level capture
discards without `-s`.

Assisted-by: ClaudeCode:claude-opus-5
@github-actions github-actions Bot added documentation Improvements or additions to documentation cpp labels Aug 15, 2026
@diagonal-hamiltonian diagonal-hamiltonian added the test-in-draft Run CI even in Draft mode label Aug 15, 2026
@github-actions

Copy link
Copy Markdown

Docs preview: https://pr-232.monoprop-docs.pages.dev

@codecov

codecov Bot commented Aug 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.70%. Comparing base (6abd839) to head (0a66dd7).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #232   +/-   ##
=======================================
  Coverage   97.70%   97.70%           
=======================================
  Files          14       14           
  Lines         742      742           
  Branches       98       98           
=======================================
  Hits          725      725           
  Misses         12       12           
  Partials        5        5           
Flag Coverage Δ
cpp 97.70% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

@sonarqubecloud

Copy link
Copy Markdown

@diagonal-hamiltonian

Copy link
Copy Markdown
Collaborator Author

🤖 AI text below 🤖

Closing this stack to rebuild it around a single-path design. Not abandoned — the work returns as fresh PRs once it is ready.

Why. The stack accumulated runtime knobs as a way to A/B competing paths, and an A/B knob is not a shipping knob. Against main's 3 environment knobs the branch reached 15. The codebase already states the rule this violates, in EnvConfig.h:

monoprop_COMPACT_RECORD, monoprop_SPARSE_RESOLVE and monoprop_INSERT_PREFETCH were removed once they had been measured … A knob that has no losing arm left is dead configuration surface.

What the measurements decided. Several paths were being kept behind default-off knobs on the strength of stale or never-run comparisons:

  • Sparse emit + the positions encode measure ~4% slower in emit_s than the dense path at both P=1 and P=8 (6/6 paired reps, p=0.031), with a Pauli structural null confirming the effect is real rather than code layout. The encode optimisation was genuine — 43 ns/push — but it optimised a path that loses by more than it recovers. Both are deleted.
  • Zobrist routing's recorded 2.8% emit_s regression turned out to be stale: it was measured on an emit path that has since been rewritten, and now reads flat. Its disposition is under a P sweep.
  • The query-dump capture and replay tooling chose the wire format and is not load-bearing for any shipped behaviour, so it does not ship.

What returns. The sparse query record together with the positions-based resolve (this PR's successor), the self-resolve simplification, and the layer profiler. Roughly 1,800 lines are deleted rather than added, and every PR in the new stack is a pure addition — the intermediate record format is never introduced, so nothing is added and then removed.

Reopening as new PRs once the local work is verified.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cpp documentation Improvements or additions to documentation test-in-draft Run CI even in Draft mode

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant