Skip to content

refactor(frontend): record source nodes on HIR noise and readout entries - #623

Merged
bachase merged 2 commits into
mainfrom
refactor/hir-source-node
Oct 26, 2026
Merged

bachase merged 2 commits into
mainfrom
refactor/hir-source-node

Conversation

@bachase-agent

Copy link
Copy Markdown
Contributor

Summary

Lands the source_node cleanup deferred from #599.

  • NoiseSite and ReadoutNoiseEntry carry source_node, the index of the traced circuit node that produced them. Directly constructed sites default to kUnknownSourceNode. Node indices distinguish expanded REPEAT bodies and separate channels on the same record, which source lines cannot.
  • trace() loses the TraceSiteOrigins* out-parameter added in refactor(noncomp): harden fixed execution semantics and bindings #599 and returns to its earlier signature. It throws std::overflow_error if a circuit has more nodes than the uint32_t index range.
  • The fixed noncomputational input validation reads the field instead of a parallel vector that had to stay aligned with the HIR tables, and rejects a site whose identity is unknown. Two mutation cases cover that.

Compiled plans and sampling output do not change. The only API change is the C++ trace() signature.

Why it was deferred and why it is safe now. The 20-25% local slowdown that held this back was instruction-fetch alignment of unchanged sampling code, not work added by the fields: identical instruction counts, 1.44B extra op-cache fetches after .text growth moved PackedBitColumns::bit across a 64-byte window in a GCC build without LTO. Details and counters are in #604 (comment). The canary builds with ThinLTO, where those accessors inline. If a sampling row flags anyway, the alignment-equalized rebuild described there is the check.

Related: #604, #599.

Test plan

  • C++ tests pass (ctest --test-dir build --output-on-failure): Debug build, 1191/1191
  • Python tests pass (uv run pytest tests/python/ -v): 7904 passed, 1304 skipped on a Debug extension via SKBUILD_CMAKE_BUILD_TYPE=Debug, -m "not expensive" as in CI
  • Doc examples pass (uv run pytest --codeblocks docs/ README.md -v): 52 passed, 8 skipped
  • Pre-commit checks pass (uv run --frozen --only-group dev pre-commit run --all-files --show-diff-on-failure)

Contributor agreement

  • I confirm this contribution is my original work (or I have the right to
    submit it) and I license it under this project's
    Apache-2.0 license.
  • If this contribution was AI-assisted, I have reviewed the generated code
    and included an Assisted-by: git trailer in the commit message.

🤖 Generated with Claude Code

Replace the TraceSiteOrigins out-parameter of trace() with a source_node
field on NoiseSite and ReadoutNoiseEntry. The frontend sets the field for
every site it emits, directly constructed sites default to
kUnknownSourceNode, and the fixed noncomputational input validation reads
the field instead of a parallel vector that had to stay aligned with the
HIR tables. trace() rejects circuits whose node count exceeds the
uint32_t source-node range.

This change was deferred after a local benchmark shift that turned out to
be instruction-fetch alignment of unchanged sampling code, not work added
by the fields (see #604).

Assisted-by: Claude (Fable 5.1) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

🐤 Performance Canary

Possible regression detected: 1 of 22 benchmarks was at least 10% slower. 1 benchmark comparison was inconclusive. This does not block merging.

Benchmark Base PR Runtime change Assessment
Squeeze 8192 T gates 15.4 us 17.4 us +13.0% Possible regression
Phase pass, 4,096 measurement barriers 2.47 ms 2.54 ms +2.8% No material change
Phase pass, 4,096 Clifford regions 4.31 ms 4.38 ms +1.7% No material change
Phase pass, 32,000 rank-limited rotations 12.6 ms 12.8 ms +1.5% No material change
Phase pass, 4,096 postselected lookahead regions 2.51 ms 2.67 ms +6.3% Notable slowdown
Compile/plan cultivation d5 9.60 ms 9.43 ms -1.8% No material change
Compile+schedule+plan, coherent QEC d5/r5 8.80 ms 8.84 ms +0.5% No material change
Schedule pass, coherent QEC d5/r5 6.77 ms 6.69 ms -1.1% No material change
Schedule pass, cultivation d5 2.74 ms 2.69 ms -1.6% No material change
Schedule dependence build, surface code d7/r7 995 us 1.00 ms +0.9% No material change
QV-10, 100 shots 3.61 ms 3.47 ms -3.9% No material change
Cultivation d5, 1,000 shots 11.7 ms 12.1 ms +3.9% No material change
Early rejection with noisy suffix, 1,000 shots 621 us 588 us -5.4% Inconclusive
Coherent QEC d5/r5, width 13, 100 shots 43.0 ms 45.8 ms +6.3% Notable slowdown
Surface code d7/r7, 10,000 shots 8.70 ms 8.42 ms -3.2% No material change
Surface code d5/r5 high noise, 10,000 shots 19.7 ms 19.8 ms +0.1% No material change
Surface code d11/r11, 1,000 shots 2.77 ms 2.65 ms -4.3% No material change
EXP_VAL 20q/200 probes, 100,000 shots 34.9 ms 35.0 ms +0.4% No material change
external_inputs_nonclifford_packed_256_shots 90.5 us 91.0 us +0.6% No material change
external_inputs_nonclifford_scalar_256_shots 310 us 311 us +0.1% No material change
external_inputs_surface_d7_packed_256_shots 141 us 142 us +1.3% No material change
external_inputs_surface_d7_scalar_256_shots 5.02 ms 5.18 ms +3.3% No material change

Compared main (5fc87ec) with this PR (f789179) on the same runner using workload-local A/B/B/A ordering. Positive changes are slower. Changes under 5% are reported as no material change; changes of at least 5% but under 10% are notable; changes of at least 10% are possible regressions or improvements. Material changes that do not repeat in both pairs with low noise are inconclusive.

Environment and method
  • Runner CPU: AMD EPYC 9V45 96-Core Processor
  • Pinned logical CPU: 0
  • ISA: AVX2 runtime backend (forced)
  • Compiler: clang version 22.1.8
  • Build: Release, x86-64-v2, ThinLTO, lld, and OpenMP enabled.
  • Each pass warms every workload for at least 0.2 seconds, then runs 3 Google Benchmark repetitions with at least 0.5 seconds of measured CPU time each.
  • One unreported full-suite process per revision runs before the measured pairs.
  • Displayed timings are the geometric mean of the two drift-balanced pass medians.
  • Material changes must repeat in both pairs with under 5% within-pass relative standard deviation; otherwise they are inconclusive.
  • Results apply only to this runner and are intended to detect large regressions, not establish release performance.

View workflow run

@bachase-agent bachase-agent left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed commit 0ac1e855f3a338014bd4bf7255da34eee5dc3e5f. I found one C++ compatibility bug and one missing breaking-change declaration; both are detailed inline.

Validation: a fresh Debug build and 134 targeted C++ test cases passed (55,370 assertions). A separate compatibility probe confirmed that the legacy readout aggregate initializer changes probabilities and produces an incorrect sampled bit. No repository files were changed during the review.

Comment thread src/clifft/frontend/hir.h Outdated
double prob_zero_to_one; // Flip probability when the recorded bit is 0
double prob_one_to_zero; // Flip probability when the recorded bit is 1
uint32_t meas_idx; // Absolute measurement index to potentially flip
uint32_t source_node = kUnknownSourceNode; // Original traced circuit node

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve existing readout aggregate initialization

Inserting source_node between the existing fields silently changes ReadoutNoiseEntry{0, 1, 1}: its probabilities become (1, 0) instead of (1, 1), and its source identity becomes 1 instead of unknown. I compiled the same initializer against the base and PR headers to verify the difference. A sampling probe using X 0; M 0 followed by this manually appended readout entry returns 1, whereas the original probabilities require 0. Append the new field after the probabilities, or provide a constructor preserving the original argument order, and cover the legacy three-argument initialization in a regression test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f789179. source_node is now declared after the probabilities, matching where NoiseSite carries it, so {meas, p01, p10} keeps its meaning with source_node left unknown. The two frontend construction sites use designated initializers, so a future reorder fails to compile instead of swapping fields, and test_hir checks the positional three-value form.

For the record, no construction site in the repository used the positional form after the original patch, and with floating-point literals the old order was a narrowing error rather than silent; the integer-literal case in the probe is the one that compiled.

[[nodiscard]] HirModule trace(const Circuit& circuit,
const InstrumentTraceOptions* instruments = nullptr,
TraceSiteOrigins* site_origins = nullptr);
const InstrumentTraceOptions* instruments = nullptr);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Mark the trace API removal as breaking

Removing TraceSiteOrigins and the third trace() parameter breaks existing C++ callers, but the PR title and sole commit use refactor(frontend): and the commit has no BREAKING CHANGE: footer. AGENTS.md explicitly requires both the ! marker and a migration footer for public API removals and signature changes; cliff.toml uses them to identify breaking changes in the generated changelog. Mark the change as refactor(frontend)!: and document replacing the origins vectors with hir.noise_sites[i].source_node and hir.readout_noise[i].source_node.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping refactor(frontend) without the marker. TraceSiteOrigins and the third trace() parameter were added by #599 on 2026-10-22, after v0.12.0 (2026-10-13), and have not shipped in any release; after this PR trace() has the signature v0.12.0 released. The changelog is generated from commits since the last tag, so a BREAKING CHANGE footer here would announce a break of an API no release had. With source_node appended (previous thread), the struct changes are additive relative to v0.12.0 as well.

Two further reasons: the C++ headers are not installed by CMake and no document describes a C++ API, and every prior ! commit in the history marked a Python API or behavioral change.

Positional initializers of ReadoutNoiseEntry written before the field
existed keep their meaning, matching where NoiseSite carries its source
node. The frontend uses designated initializers so a future reorder fails
to compile instead of swapping fields.

Assisted-by: Claude (Fable 5.1) <noreply@anthropic.com>
@bachase
bachase merged commit 3d2f43f into main Oct 26, 2026
20 checks passed
@bachase
bachase deleted the refactor/hir-source-node branch October 26, 2026 15:40
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