Repository navigation
Conversation
Port the measured tree walk with current opcode validation and dispatch pruning. Expose MeasuredQROM SELECT and SELECT-SWAP without changing unitary QROM consumers, including explicit controls and an optional borrowed-bank echo that restores arbitrary entangled bank states. Separate tree AND, body/routing Toffoli and measurement counts. Document partial-tree full-address behavior, clean-subspace logical inverses, theoretical T-count limits, and CUDA-Q 0.16 executor requirements. Add independent full-history Kraus checks, native complex-input channel regressions, compiled resource checks, a nonzero coherence observable, and runnable lookup documentation. Co-authored-by: Scott Thornton <wsttiger@gmail.com> Signed-off-by: Kevin Mato <kmato@nvidia.com>
Fold the measured interpreter into the shared coherent interpreter: the two measured-uncompute opcodes (22, 23) live next to the coherent table and are dispatched behind captured guards, so a coherent tape never carries their arms; the measured module no longer mints kernels or keeps a second keep-alive registry. Count tree ANDs from the tape instead of aliasing the measurement count, keep the lookup tape and add describe(), require plain bools for controlled/dirty_banks, and spell the SELECT-SWAP echo once. Restore the framework-boundary tests (cudaq.control collapses a superposed control, cudaq.sample rejects direct and nested feedback, cudaq.observe agrees), add an exhaustive measurement-history oracle for MeasuredQROM tapes (SELECT, SELECT-SWAP, dirty banks, controlled), seed the native trajectory tests, drop the zero-measurement parametrizations, pin the SELECT-SWAP price against every candidate tape, and make the vector comparisons strictly absolute. Document what automatic control does to a measured kernel, narrow the module-wide strictly-unitary claims to the coherent primitives, state the keep-alive registry's real cost, and let the example read CUDAQ_DEFAULT_SIMULATOR through state_from. Signed-off-by: Kevin Mato <kmato@nvidia.com>
f995a6d to
b56f2d2
Compare
Signed-off-by: Kevin Mato <kmato@nvidia.com>
Signed-off-by: Kevin Mato <kmato@nvidia.com>
Signed-off-by: Kevin Mato <kmato@nvidia.com>
|
@wsttiger Scott, the completion update is now pushed. This adds whole-cache measurement erasure, outcome-dependent phase correction, measured one-hot cleanup, and the shared planning/feedback infrastructure consumed by #65. The earlier omission of cache erasure is addressed. Could you review this foundation first, followed by #65? Local validation on the pushed tree: 208 host tests passed; 845 native tests passed, with 3 optional-dependency skips. The description now covers the complete implementation and its execution/resource boundaries. |
wsttiger
left a comment
There was a problem hiding this comment.
Kevin, thanks — this is a substantial and careful piece of work. I went through it with a mixed
read-and-run approach: the pure host suite (208 green), every test_primitives_* file on my box (519 green,
2 skipped, 1 failure that is the 0.16-only cudaq.sample pin — expected on my 0.15.1), and #55's
LCUBlockEncoding suite on a tree with #64 merged in (21 green), since #55 builds on
unary_iteration_kernels and QROM. I also re-derived the cache-erasure phase bookkeeping and ran my own
state-vector checks of MeasuredQROM (select, select_swap traverse/measure at K ∈ {1,2,4}, B ∈ {2,4},
multi-bit words, zero words, auto, controlled and uncontrolled, dirty banks) and of the measured walk
with x/y/z/sign and work-register bodies — everything agrees with the ideal channel to ~1e-16, including
relative phases. The host oracle is genuinely independent (expected maps built without the tree or the
measurements, unnormalised branches plus completeness, sentinels that fire), the coherent JSON baselines
reproduce main bit-for-bit, and every resource number in the guide matches the planner. The public
coherent API is source-compatible: #55's imports and _alias_sampling's use of QROM are untouched.
So the physics and the planning are not what I want to discuss. One structural point blocks me, and a
handful of smaller things follow.
1. The measured arms live in the same interpreter kernel as every coherent walk. _mint_interpreter
now dispatches _BASE_OPS | _MEASURED_OPS | _FEEDBACK_OPS in one kernel body, so the source of
QROM.kernel, unary_iteration_kernels().kernel and (through #55) every LCU SELECT contains h; mz; if outcomes[b]: x arms. On 0.15.1 a plain QROM([3,0,1,2,1], 3, 2).kernel already shows 3 quake.mz,
3 quake.h and 43 cc.if in its MLIR; the pipeline folds them away there, but your own docstring says
that on 0.16 the captured guards are lifted into kernel arguments and the arms stay. That makes the
coherent primitives' cudaq.control-compatibility on 0.16 depend on cuda-quantum#5257 — the control
variant generator silently skipping measurements — which is a bug, and one that NVIDIA/cuda-quantum#5630
(in review now) fixes by rejecting control variants of any kernel whose IR contains a measurement,
dead arm or not. When that ships, every coherent primitive minted here stops being controllable, the
module docstring's promise at lines 48-53 breaks, and test_primitives_body_gates.py:284 /
test_primitives_interpreter.py:340 fail. It also re-opens the #5280 CSE hazard the guard pruning was
built to avoid, for the five new multi-control arms. The clean fix is a separate interpreter for the
measured/feedback opcodes, used only by measured_unary_iteration_kernels and MeasuredQROM, so the
coherent kernels' IR is byte-identical on every CUDA-Q version. If you'd rather keep one interpreter, I'd
need at least a 0.16 test that translates an entry kernel calling QROM(...).kernel and asserts there is
no mz in the IR, plus the two docstrings reconciled (see the inline note). Details inline.
2. Scope. A few #65-only paths ship here without being called out: cache_register='output' and
opcode 25, the opcodes reserved for pr/qrom-select-copy (21, 28-31) that the host oracle already
decodes, and two module names in test_no_cudaq that do not exist in this PR. Either drop them from the
foundation or say in the description that they are forward hooks.
3. Coverage of the headline path. Every state-level check of cache_uncompute="measure" runs at
K = 1 or 2 (the K = 4 case tests the reference's own model). I checked production at K = 4 over all
histories and it is right, so this is cheap: a K = 4 host case and one native K = 8 trajectory test
(n=5, B=2, b=4 is 21 qubits).
4. SDK-boundary tests. Three tests pin CUDA-Q behaviour rather than this package;
test_cudaq_sample_rejects_nested_measurement_feedback fails hard on 0.15.1. The repo convention is a
feature-probe skipif; grouping them in an explicitly named module would also make their purpose clear.
Smaller items inline: real widths in the tape validation, the unused _select_swap_cost/_VARIANTS/import
block, exporting QROMResources, a few "existing consumers continue to…" phrasings in user-facing text
(we try to say what a thing is), the 'none' string vs None, the bool-vs-coerce policy, error
messages that drop the offending value, SPDX headers on the new files, a test module importing from
another test module, and the AST-exec test. None of those are blockers.
One process note: CI has not run on the NVIDIA runners yet (copy-pr-bot vetting), so we'll want a 0.16 run on the final shape before merging.
| supported = _BASE_OPS | ||
| from . import _unary_iteration_ir as ir | ||
|
|
||
| supported = _BASE_OPS | _MEASURED_OPS | ir._FEEDBACK_OPS |
There was a problem hiding this comment.
supported = _BASE_OPS | _MEASURED_OPS | ir._FEEDBACK_OPS
This is the line I'd like to change the shape of. Because the four kernel variants below dispatch the
measured and feedback opcodes too, every coherent kernel minted here carries h; mz; if outcomes[b]: x
arms in its source. On my 0.15.1 box the minted QROM([3,0,1,2,1], 3, 2).kernel MLIR has 3 quake.mz,
3 quake.h and 43 cc.if (none before this PR); the compile pipeline folds them away on 0.15, and your
docstring at 356-361 says on 0.16 it does not. The consequence is that cudaq.control of coherent
primitives on 0.16 works only because the control-variant generator skips measurements silently
(cuda-quantum#5257). NVIDIA/cuda-quantum#5630 rejects control variants of any kernel whose IR contains a
measurement, and these arms cannot be accepted by its idiom analysis (Z-basis h; mz, measured qubit is
an argument, inside a conditional) — so once it lands, test_primitives_body_gates.py:284 and
test_primitives_interpreter.py:340 fail and the promise at 48-53 breaks. Suggestion: keep this
interpreter strictly coherent and mint the measured/feedback arms in a separate _mint_measured_interpreter
used only by the measured walk and MeasuredQROM. The measured variants could then come from one template
instead of five copies of the arm block.
| in ``_unary_iteration_measured`` is their only producer. On CUDA-Q 0.16 the | ||
| captured guards are lifted into kernel arguments, so the generic kernel | ||
| source still mentions ``mz`` for coherent walks; the runtime's | ||
| measurement-feedback checks look at entry kernels, and the tests pin that |
There was a problem hiding this comment.
"the runtime's measurement-feedback checks look at entry kernels"
_unary_iteration_measured.py:33-35 says the opposite: "CUDA-Q 0.16's compiler rejects measurement-feedback
kernels, direct or nested, under cudaq.sample". If the nested check is real, then on 0.16 a sampled
entry kernel calling a coherent walk also carries nested mz+feedback in its IR and would be rejected;
if the check is entry-only, the measured docstring is wrong. Which did you observe on 0.16? The test that
backs the coherent claim (test_coherent_tapes_keep_sample_control_and_adjoint) is a two-op tape asserting
behaviour, not IR, so it cannot tell us.
| f"interpreter variant's dispatch set (controlled={controlled}, " | ||
| f"has_work={has_work}); the dispatch loop would silently skip " | ||
| "them") | ||
| # Infer only the minimum wire bounds here; public plans validate against |
There was a problem hiding this comment.
# Infer only the minimum wire bounds here; public plans validate against their actual geometry.
Inferring num_address/num_ladder/num_output/num_work from the tape and then validating the tape against
them means these bounds checks cannot fail. MeasuredQROM knows the real widths (_qrom_measured.py:143-147)
— passing them (or validating in _plan_measured_qrom against BlockGeometry) would catch a wrong
rebase_block_walk offset at mint time. The same tape is also re-validated in _qrom_cache_erasure.py:131-138
and :183-190, and the opcode set is checked here and again in validate_feedback_ops.
| cache_bits: int, | ||
| *, | ||
| cache_offset: int, | ||
| cache_register: Literal['ladder', 'output'] = 'ladder') -> QROMPlan: |
There was a problem hiding this comment.
cache_register: Literal['ladder', 'output'] = 'ladder'
'output' (opcode 25, _OP_MEASURE_RESET_OUTPUT) has no production caller — _emit_selection always
erases a ladder cache — yet the arm is compiled into all four kernels. If this is for #65, I'd rather it
arrive with #65, or be named as a forward hook in the description.
| _OP_CX_ADDR_ADDR = 18 # cx(address[a], address[b]) | ||
| _OP_CCX_ADDR_ADDR = 19 # x.ctrl(address[a], address[b], ladder[c]) | ||
| _OP_CX_LADDER_TARGET = 20 # cx(ladder[a], target[b]) | ||
| # 21 is reserved for the coherent SELECT-COPY extension (pr/qrom-select-copy). |
There was a problem hiding this comment.
# 21 is reserved for the coherent SELECT-COPY extension (pr/qrom-select-copy).
Same point for the reservations (21 here, _OP_CCX_LADDER_LADDER_TARGET at 440-441 with its table rows,
"28--31 remain reserved" at 492): they reference a branch that is not in this repository, and the host
oracle already decodes 21. Drop or declare.
|
|
||
| __all__ = ["MeasuredQROM"] | ||
|
|
||
| _VARIANTS = ("auto", "select", "select_swap") |
There was a problem hiding this comment.
_VARIANTS = ("auto", "select", "select_swap")
Dead code in this block: _VARIANTS, _MEASURED_ADDRESS_OPERANDS, _select_swap_cost (planning lives in
_qrom_measured_plan._price_candidates) and the _ADDRESS_OPERANDS import routed through _qrom. Also
_unary_iteration_measured.py:52-56 imports 15 names of which 9 are unused and :61 redefines
_MEASURED_OPCODES from _unary_iteration_ir.py:438; _qrom.py:261/328 still assign an unused
_toffoli_count; validate_qrom_data runs twice (_qrom_measured.py:119, _qrom_measured_plan.py:161);
and _qrom_measured.py:86-88 mentions a QROM "SELECT-COPY variant" that does not exist.
| @@ -51,11 +55,17 @@ | |||
| subtract_constant, subtract_constant_qft, | |||
| subtract_register) | |||
| from ._qrom import QROM | |||
There was a problem hiding this comment.
from ._qrom import QROM
QROM.resources and MeasuredQROM.resources return QROMResources (with QROMStageResources records) and
the guide cites their fields (measured_qrom.rst:171-174), but neither type is exported here, so
automodule :members: cannot render them. Export both (or document the fields in the guide).
| little-endian views. The ``ladder`` view contains ``num_clean`` clean qubits followed by | ||
| ``num_dirty`` borrowed qubits, with ``num_ladder`` their sum. The default | ||
| requires all workspace clean; blocked lookups also include cache registers. | ||
| Existing ``QROM`` consumers continue to use unitary kernels. |
There was a problem hiding this comment.
"Existing
QROMconsumers continue to use unitary kernels."
House style in the docs is to say what a thing is rather than how it relates to what came before — e.g.
"QROM and unary_iteration_kernels are strictly unitary; MeasuredQROM and
measured_unary_iteration_kernels opt into measurement." Same for :125 ("preserves previous
choices"), primitives/__init__.py:15 ("stay strictly unitary"), and _qrom_measured.py:5-6, 20
("remain unitary", "preserves the original selection").
| raise ValueError( | ||
| "variant must be one of ('auto', 'select', 'select_swap'), " | ||
| f"got {variant!r}") | ||
| if not isinstance(controlled, bool): |
There was a problem hiding this comment.
if not isinstance(controlled, bool):
unary_iteration_kernels and measured_unary_iteration_kernels coerce with bool(...); this rejects
np.True_ and 1. One policy for the package, please (coercion matches the existing factories). Nearby:
:51 resolves cache_uncompute to the string 'none' for SELECT, which is not an accepted input and
shows up in __repr__ — None, like phase_block_size, reads better; :183 drops the offending value
("block_size must be a power of two >= 2", where QROM echoes it), and :216-217 reports
"exceeds available cache or high-address width" even when the cause is K > 2^(h-1) for the chosen B.
| @@ -0,0 +1,355 @@ | |||
| """Independent little-endian NumPy gates and unnormalized measurement channels. | |||
There was a problem hiding this comment.
(no SPDX header)
The seven tests/host/*.py files have no SPDX lines, and _unary_iteration_ir.py, _qrom_plan.py,
_qrom_common.py, _qrom_cache_erasure.py, _qrom_measured_plan.py and
tests/python/test_primitives_measured_cache.py carry only the license line; every other file has both.
While you are in the test tree: test_qrom_measured_selection.py:329 imports check_emitted_plan from
another test module (works only under pytest's default import mode — move it to the reference module),
and test_primitives_qrom_plan.py:80-129 execs production source via AST surgery, which is fragile and
largely duplicates test_default_tapes_match_captured_baseline.
…ract (cudaq >= 0.16) (#52) ## Summary Rebuilds the `select_swap` QROM as composed kernels and promises composition as a supported surface of the primitives subpackage. 1. **`select_swap` as composed kernels.** The lookup is a parent kernel that carves `low`/`high` address views and `lines`/`blocks` ancilla views out of the public `(address, ladder, output)` signature and composes three sub-kernels, `W S C S⁻¹ W`: a plain `unary_iteration_kernels` walk over the block index whose target view is the block registers, a minted Fredkin routing network over `(low address bits, blocks)` and its reversed-list inverse, and the copy CNOTs inline. The flat-tape splice — rebasing the walk's address operands and smuggling the block registers through the walk's ladder view — and the `_OP_CX_LADDER_TARGET` opcode are deleted end to end. Public signature, `num_ladder` and `toffoli_count` are unchanged (`AliasSamplingPrepare` depends on them). 2. **Composition as a supported surface.** The package docstring states the contract: minted kernels are building blocks callable from user kernels, and `cudaq.control` over the whole composition yields the controlled operation (the `controlled=True` factory variants remain the cheaper route for a controlled walk). `tests/python/test_primitives_composition.py` pins both halves: plain composition, and controlled composition three-way against the factory's controlled walk and the analytic state on a superposed control. 3. **Version gate and docstrings.** Controlled composition is pinned on the supported CUDA-Q line through a `controlled_composition` pytest marker (`conftest.py`): the tests skip only below 0.16.0, the package minimum, and fail on a 0.16.x that regresses control propagation. Docstrings describe what composition and the flat tape are. ## Validation - `test_primitives_{qrom,composition,interpreter,unary_iteration,resources,alias_sampling,arithmetic}.py`: 274 passed on cudaq 0.16.0 (nothing skipped); 270 passed / 4 skipped (the marker-gated controlled-composition tests) on cudaq 0.15.1. - The composed `select_swap` reproduces the previous construction exactly: full statevectors identical and equal Toffoli counts on five geometries (block sizes 2–8, one-high-bit and zero-padded cases, 32 entries over 5 bits). - `cudaq.estimate_resources` on the composed kernel equals `toffoli_count` (resource suite, block sizes 2, 4, 8). ## Interplay - #70 (`QROM(..., controlled=True)`) threads its flag through the flat-tape `select_swap`; under this construction the controlled lookup is a `controlled=True` walk sub-kernel taking `(control, high, lines, blocks)` with the routing and copy unchanged, no `cudaq.control` needed (verified: identical conventions and the documented controlled Toffoli price). Whichever lands second carries that ~15-line rewrite; #70's tests use only the public API and transfer unchanged. - #64 (measured QROM) rebuilds `_qrom.py` around a flat IR; its coherent half can take this composed `select_swap` wholesale, and its measured path keeps its own opcode copies. ## Files - `python/cudaq_algorithms/primitives/__init__.py`, `_qrom.py`, `_unary_iteration.py`, `_alias_sampling.py` - `tests/python/conftest.py`, `test_primitives_composition.py` (new), `test_primitives_qrom.py`, `test_primitives_interpreter.py`, `test_primitives_arithmetic.py` --------- Signed-off-by: Scott Thornton <wsttiger@gmail.com>
`QROM` gains `controlled=True`: the externally controlled lookup, with the `unary_iteration_kernels` convention — a leading one-qubit `control` view, identity at control `|0>`, the plain lookup at `|1>`, self-inverse. `controlled` must be a `bool` (same guard and message as the measured-QROM work). **Why it is cheap.** Only the two walk passes carry the control. In the composed `select_swap` (#52) the walk sub-kernel is minted `controlled=True` and the parent passes the control view to both walk passes; the routing network and the copy are the plain kernels. At control `|0>` a controlled walk on a clean ladder writes nothing, so the block registers stay `|0>`, the routing swaps `|0>` registers, the copy fires from a `|0>` slot, and the un-route/un-write are the identity. The Toffoli price is the controlled walk count in place of the plain one (the root gadgets become Toffolis and the root crossings lose the unguarded shortcut: +4 for the 3-bit, 5-entry test table on `select`, +6 on `select_swap` with `B = 2`), with routing and copy unchanged — a fraction of `cudaq.control` over the composition, which adds a control to every gate. `variant="auto"` prices the controlled candidates against each other; the control shifts the optimum toward larger blocks on small tables. Compiled on CUDA-Q 0.16 for eleven 3-bit entries over four address bits, the native controlled lookup costs 19 / 26 / 28 Toffolis (select, select_swap `B = 2`, `B = 4`) against 15 / 18 / 22 plain, where `cudaq.control` over the plain composition costs 42 / 77 / 83. Because the optimum moves to larger blocks under control, `num_ladder` can exceed the plain lookup's; size the ladder from the instance. **What "identity at `|0>`" means.** A clean-ladder statement: every ladder write of a controlled walk is gated by the control or by a ladder line above it, so on a dirty ladder the deeper Toffolis fire. The clean ladder is the kernel's precondition in any case, but `cudaq.control` of the plain lookup is the identity on every input, so the docstrings say which identity this is. **Who needs it.** The alias PREPARE deliberately never puts QROM under control, so the LCU encodings do not. The consumer is the sparse-access oracle path (#53): `from_general_oracles` wraps the oracles in `cudaq.control`, which excludes table-oracle bundles built on walks; a natively controlled lookup is what lets that path accept them (follow-up in #53), under the clean-ladder precondition those bundles already carry. **Tests** (`test_primitives_qrom.py` +9, `test_primitives_resources.py` +3): control conventions at `|0>`, `|1>` and `|+>` and the involution under a superposed control over six geometries — both variants, a one-bit address, a single block-index bit (`B = 2^(address_bits − 1)`), a zero-padded last block at `B = 8` — on superposed addresses with a preloaded output word, the `|1>` branch compared with the table itself; Toffoli counts against the documented contract; `auto` pricing pinned on a table whose optimum moves (plain `B = 2`, controlled `B = 4`); the bool guard; and the resource suite's compiler-measured Toffoli cross-check over the controlled lookups. Existing QROM, unary-iteration, composition and alias-sampling behaviour unchanged. **Note for #64.** The measured-QROM PR rewrites `_qrom.py`; its coherent half can take the composed `select_swap` with this flag wholesale (`MeasuredQROM` already carries a `controlled` flag of the same shape). ## Files - `python/cudaq_algorithms/primitives/_qrom.py` - `python/cudaq_algorithms/primitives/__init__.py` (docstring line) - `tests/python/test_primitives_qrom.py` - `tests/python/test_primitives_resources.py` --------- Signed-off-by: Scott Thornton <wsttiger@gmail.com>
Add measured unary iteration and
MeasuredQROMfor classical-table XOR lookup with SELECT and SELECT-SWAP, including coherent external control, padded addresses, arbitrary complex outputs, and restoration of the declared clean or borrowed workspace.Clean SELECT-SWAP supports
cache_uncompute="measure": after routing is undone, X-measure and reset the cache, then cancel the outcome-dependent parity phase through a measured phase walk and one-hot cleanup.phase_block_sizeselects the correction geometry.cache_uncompute="auto"chooses measurement only for a strict logical-Toffoli saving; traversal remains the default and wins cleanup ties. For 256 two-bit entries with eight address bits, B=4 and K=8, this emits 87 Toffolis and 83 measurements on 14 clean workspace qubits, versus 136 Toffolis and 124 measurements for traversal.The implementation also provides:
controlled=Truekernels and the measured walker's logical inverse. Automaticcudaq.controlandcudaq.adjointare outside the measured-channel contract.Resource records distinguish tree ANDs, all proven temporary ANDs, routing Toffolis, measurements, clean/borrowed workspace, and persistent classical capacity. The four-T temporary-AND model requires compatible synthesis; these are emitted logical counts, not compiled T counts or hardware costs. Native feedback was validated on CUDA-Q 0.16
qpp-cpu; other backends are not established by these checks.Validation: 208 host tests passed; 845 native tests passed, with three optional-dependency skips (OpenFermion/Psi4) and two SDK warnings. Independent host tests check every measurement history for small channels, coherent controls, padding, and borrowed-bank restoration. Native trajectory/resource checks, the example, formatting, and Sphinx with warnings as errors passed.
Land this foundation before #65. The published branches currently have sibling histories based on main; #65 includes the shared code and must be updated and revalidated after #64 lands.