Repository navigation
refactor(frontend): record source nodes on HIR noise and readout entries - #623
Conversation
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>
🐤 Performance CanaryPossible regression detected: 1 of 22 benchmarks was at least 10% slower. 1 benchmark comparison was inconclusive. This does not block merging.
Compared Environment and method
|
bachase-agent
left a comment
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
[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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
[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.
There was a problem hiding this comment.
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>
Summary
Lands the
source_nodecleanup deferred from #599.NoiseSiteandReadoutNoiseEntrycarrysource_node, the index of the traced circuit node that produced them. Directly constructed sites default tokUnknownSourceNode. Node indices distinguish expandedREPEATbodies and separate channels on the same record, which source lines cannot.trace()loses theTraceSiteOrigins*out-parameter added in refactor(noncomp): harden fixed execution semantics and bindings #599 and returns to its earlier signature. It throwsstd::overflow_errorif a circuit has more nodes than theuint32_tindex range.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
.textgrowth movedPackedBitColumns::bitacross 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
ctest --test-dir build --output-on-failure): Debug build, 1191/1191uv run pytest tests/python/ -v): 7904 passed, 1304 skipped on a Debug extension viaSKBUILD_CMAKE_BUILD_TYPE=Debug,-m "not expensive"as in CIuv run pytest --codeblocks docs/ README.md -v): 52 passed, 8 skippeduv run --frozen --only-group dev pre-commit run --all-files --show-diff-on-failure)Contributor agreement
submit it) and I license it under this project's
Apache-2.0 license.
and included an
Assisted-by:git trailer in the commit message.🤖 Generated with Claude Code