fixed an issue that arose due to custom_control/custom_inverse using … - #883
Open
positr0nium wants to merge 1 commit into
Open
positr0nium wants to merge 1 commit into
positr0nium wants to merge 1 commit into
Conversation
…two different tracing mechanisms (jit vs make_jaxpr)
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Closure operands can still be silently miswired, and the new signature check accepts incompatible array abstract values.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Moves custom inverse/control closure conversion to registration time while preserving Jaspr metadata.
Changes:
- Adds closure-conversion and signature-validation helpers.
- Simplifies inversion/control transformations.
- Adds regression tests and corrects retraced constant handling.
File summaries
| File | Description |
|---|---|
tests/jax_tests/test_custom_inverse.py |
Adds signature and nested transformation tests. |
src/qrisp/jasp/jasp_expression/jaxpr_utils.py |
Adds closure-conversion utilities. |
src/qrisp/jasp/jasp_expression/inv_transform.py |
Removes late inverse rewrapping. |
src/qrisp/jasp/jasp_expression/control_transform.py |
Removes late control rewrapping and adds typing. |
src/qrisp/jasp/jasp_expression/centerclass.py |
Makes signature comparison length-aware. |
src/qrisp/jasp/interpreter_tools/abstract_interpreter.py |
Preserves newly hoisted constants. |
src/qrisp/environments/custom_inversion_environment.py |
Normalizes and validates registered inverses. |
src/qrisp/environments/custom_control_environment.py |
Normalizes and validates registered controls. |
documentation/source/general/changelog/changelog-dev.rst |
Updates the bug-fix explanation. |
Review details
- Files reviewed: 9/9 changed files
- Comments generated: 4
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+121
to
+126
| Note that the *values* of the folded constvars (``consts[:n_extra]``) are | ||
| dropped: the folded vars become plain invars, and it is the caller's | ||
| responsibility to supply equivalent values positionally. That holds precisely | ||
| because Jax's own closure conversion put the same values in the same leading | ||
| positions on the forward path - see ``closure_convert_jaspr``, which pairs | ||
| this rewrite with a signature check against the forward Jaspr. |
| """ | ||
| if len(invars_1) != len(invars_2): | ||
| return False | ||
| return all(type(v1.aval) is type(v2.aval) for v1, v2 in zip(invars_1, invars_2)) |
| __slots__ = ("base_jaspr", "ctrl_state") | ||
|
|
||
| def __init__(self, base_jaspr, ctrl_state, stop_recursion=False): | ||
| def __init__(self, base_jaspr: Jaspr, ctrl_state: int | str, stop_recursion: bool = False) -> None: |
Comment on lines
+211
to
+216
| # The retrace can hoist values that it closes over into constvars of its | ||
| # own. Those sit in front of the input's own constvars (which were | ||
| # appended behind them just above), so their consts have to go in front | ||
| # here as well - otherwise the result is a ClosedJaxpr whose consts no | ||
| # longer line up with its constvars. | ||
| res = ClosedJaxpr(res, list(retraced.consts) + list(jaxpr.consts)) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fix the custom_inversion / custom_control calling convention at its source
Description
custom_inversionandcustom_controlregister a second Jaspr that stands in for thedecorated function when it is inverted or controlled. The two Jasprs are traced by
different mechanisms and the mechanisms disagree on how closed-over values are passed:
qache, i.e.jax.jit, so Jax performs its ownclosure conversion. Values the function captures become leading invars of the callee
and extra leading operands of the wrapping
jitequation.make_jaspr, i.e.jax.make_jaxpr, which doesnot closure convert. The same values stay behind in
constvars/consts, so thevariant exposes fewer invars than the
jitequation supplies, and it holds livetracers in
consts.Until now this was patched up at every use:
invert_eqnandcontrol_eqnfolded theconstvars back into invars and rewrapped the result with
Jaspr(normalized). That rewrapproduces a fresh object and silently drops everything the decorators had registered on
the Jaspr. PR #881 worked around one consequence by copying
inv_jaspracross therewrap;
ctrl_jasprwas still dropped by the same two lines, and on the control side therewrap downgraded a
ControlledJasprto a plainJaspr, which no amount of fieldcopying can repair.
This PR fixes the mismatch where it originates. The variant is brought into the
jitcalling convention once, where it is traced, and the transformation passes go back to a
plain
.inverse()/.control(1). Since nothing is rewrapped at use time, nothing can belost.
Related Issues
Closes #
Related to #881 (this replaces the workaround introduced there)
Type of Change
Mostly a refactoring that removes the workaround, but it does fix two live defects on top
of what #881 addressed:
ctrl_jasprwas dropped by the same rewrap, and aControlledJasprwas downgraded to a plainJaspr, losing itsbase_jaspr/ctrl_state,its
inverse()override and its efficient nestedcontrol().Breaking Change?
If yes, describe the impact and migration path:
Not breaking for working code. Code that was already broken now fails differently: a
custom_inversion/custom_controlwhose registered variant does not take the samearguments as the function itself now raises at trace time, where the pair is registered,
instead of surfacing later as
ValueError: Tried to evaluate jaxpr with insufficient argumentsor as silently miswired operands.What was changed?
src/qrisp/jasp/jasp_expression/jaxpr_utils.pyfold_extra_constvars_into_invarsmoved here frominv_transform.py, with adocstring that describes the actual cause (the two tracing mechanisms) rather than
"retracing reclassifies invars".
closure_convert_jaspr(jaspr, insert_at=0): folds every constvar into an invarand rewraps without loss, carrying over
inv_jaspr,ctrl_jaspr,permeability,isqfreeandenvs_flattened.src/qrisp/environments/custom_inversion_environment.py: the inverse is closureconverted and its signature checked against the forward Jaspr before being cached as
inv_jaspr.src/qrisp/environments/custom_control_environment.py: same forctrl_jaspr, withinsert_at=1so the folded invars land behind the control qubit.src/qrisp/jasp/jasp_expression/inv_transform.py:invert_eqnis back toparams["jaxpr"] = eqn.params["jaxpr"].inverse(). The fold ininvert_jaspr, whichnormalizes the derived inverse after its retrace, stays.
src/qrisp/jasp/jasp_expression/control_transform.py:control_eqnis back tonew_params["jaxpr"] = eqn.params["jaxpr"].control(1). This is what lets aControlledJasprstay one.src/qrisp/jasp/jasp_expression/centerclass.py:check_aval_equivalenceis nowlength-aware (it used
zip, which truncates, so it could not detect an arity mismatch)and is now actually used. It had no callers before.
src/qrisp/jasp/interpreter_tools/abstract_interpreter.py:reinterpretdiscarded theconsts of its own retrace while keeping the input's, so any hoisting produced a
ClosedJaxprwhose consts no longer lined up with its constvars. The two const listsare now concatenated in the same order as the constvars.
inv_transform.pyandcontrol_transform.py, theControlledJasprmethods, the twohelpers in
jaxpr_utils.pyandcheck_aval_equivalence.tests/jax_tests/test_custom_inverse.py: two new tests (below). The three tests fromPreserve the custom inverse when folding constvars back into invars #881 are kept unchanged.
described a mechanism that no longer exists.
How was it tested?
No linked issue, so no Test-IDs. Run locally against the pinned
jax==0.7.1.tests/jax_tests/test_custom_inverse.py(6 tests, incl. the 3 from #881)tests/jax_tests/test_control_flow_interpretation.py(incl. the qswitch invert/control regression test)tests/jax_tests/test_jasp_qswitch.py, prepare and control modules (20 tests)tests/jax_testsfull suiteruff format --diffon the changed filesruff checkon the changed filesThe 31 failures are all
ModuleNotFoundErrorfor optional dependencies that were notinstalled locally (
catalyst,stim,xdsl), intest_mlir*,test_stim_simulation,test_catalyst_interface,test_qubit_array_fusion,test_buffered_quantum_stateandtest_to_qc_in_tracing_context. None are related to this change.ruff checkreports thesame per-file counts as before the change for the test file (pre-existing
F405star-import noise) and strictly fewer for every source file.
Two new tests:
test_registered_inverse_matches_forward_signaturewalks the Jaspr of apreparewithrun-time amplitudes and asserts that every registered inverse carries no
constvars/consts and takes exactly the arguments of the function it inverts. This pins
the invariant directly rather than a downstream symptom.
test_double_inversion_of_prepare_under_control_round_tripsappliesinvert(control(invert(invert(prepare)))), so the outer inversion acts on an alreadycontrolled equation and the inverse callee is actually invoked.
Both were confirmed not to be vacuous: with the normalization temporarily disabled they
fail with
ValueError: Tried to evaluate jaxpr with insufficient arguments: expected 7 (including 3 consts), got 10- the three leaked constvars. Note that the three tests from#881 still pass in that state, because two adjacent inversions cancel through the
back-pointer without ever invoking the inverse callee.
Screenshots / Output (if applicable)
n/a
Checklist
Notes on the unchecked items:
has not run yet.
prepareaboutincompatibility with
invert/controlwas already removed earlier; the changes hereare to internals and their docstrings.
Reviewer Notes
relies on the caller supplying equivalent values positionally. That holds because Jax's
own closure conversion put the same values in the same leading positions on the forward
path. It was previously unchecked anywhere;
check_aval_equivalencenow compares thetwo signatures where both Jasprs are in scope. It compares aval types, so it catches
an arity mismatch or a Qubit lined up against an array, not two same-typed arrays in
swapped order. Worth a second opinion on whether a stronger comparison is wanted.
reinterpretchange is the one part with no test coverage, because the paththat hoists consts there does not currently fire. It is included because the remaining
fold in
invert_jasprsits directly behind it and would otherwise slice a consts listof the wrong length.
Jaspr(normalized)had to go rather than be made non-lossy. Copying theattributes across would still not preserve
ControlledJaspr, since the rewrap changesthe class. Normalizing at creation avoids the rewrap entirely.
Generated with Claude Code