Description
cudaq.adjoint(kernel, ...) fails at launch with
[error] [NVQIR.cpp:875] could not autogenerate the adjoint of a kernel
for a kernel that is purely unitary (only x and ry, no measurement or
reset, and verified unitary by dense-matrix reconstruction) when
conditional x gates conjugate a rotation inside nested loops — the
standard "flip control pattern / rotate / unflip" idiom.
Each ingredient adjoints fine in isolation (loops with computed bounds,
if branches, controlled rotations on register views, list arguments);
only the combination below fails.
Steps to reproduce
The script runs the same roundtrip twice: once with the autogenerated
adjoint (fails at launch), once with the adjoint written out by hand
(runs, and is the exact inverse). The hand-written
undo_conjugated_rotations is the same loops in reverse order with the
rotation negated — the X conjugation is its own inverse — demonstrating
the adjoint is well-defined and expressible in the kernel language; only
autogeneration refuses it.
import numpy as np
import cudaq
cudaq.set_target("qpp-cpu")
@cudaq.kernel
def conjugated_rotations(qubits: cudaq.qview):
for layer in range(1, qubits.size()):
for branch in range(1 << layer):
for bit in range(layer):
if ((branch >> bit) & 1) == 0:
x(qubits[layer - 1 - bit])
ry(0.1, qubits[layer])
for bit in range(layer):
if ((branch >> bit) & 1) == 0:
x(qubits[layer - 1 - bit])
# The adjoint written out by hand: same loops in reverse order, negated
# rotation; the X conjugation is its own inverse.
@cudaq.kernel
def undo_conjugated_rotations(qubits: cudaq.qview):
n = qubits.size()
for reverse_layer in range(1, n):
layer = n - reverse_layer
branches = 1 << layer
for reverse_branch in range(branches):
branch = branches - 1 - reverse_branch
for bit in range(layer):
if ((branch >> bit) & 1) == 0:
x(qubits[layer - 1 - bit])
ry(-0.1, qubits[layer])
for bit in range(layer):
if ((branch >> bit) & 1) == 0:
x(qubits[layer - 1 - bit])
@cudaq.kernel
def roundtrip_manual():
qubits = cudaq.qvector(3)
conjugated_rotations(qubits)
undo_conjugated_rotations(qubits)
@cudaq.kernel
def roundtrip_autogen():
qubits = cudaq.qvector(3)
conjugated_rotations(qubits)
cudaq.adjoint(conjugated_rotations, qubits)
manual = np.asarray(cudaq.get_state(roundtrip_manual))
print(f"hand-written undo: |<000|state>| = {abs(manual[0]):.16f}")
auto = np.asarray(cudaq.get_state(roundtrip_autogen)) # raises
Observed
hand-written undo: |<000|state>| = 1.0000000000000004
cudaq.adjoint roundtrip: RuntimeError: could not autogenerate the adjoint of a kernel
(from NVQIR.cpp:875, at first launch of roundtrip_autogen).
Expected
The adjoint of a unitary kernel with statically-bounded control flow to be
generated: the loop bounds derive only from qubits.size() and loop
constants, the branch conditions are pure classical arithmetic on loop
variables, and the hand-written inverse above shows the reversed kernel is
directly expressible in the same language.
Note: possibly related to the silent-wrong-adjoint case in #4897 — that
one has no conditional gates and produces a wrong circuit instead of an
error.
Environment
- CUDA-Q 0.15.0 (built from
releases/v0.15.0)
- Python 3.12, Linux x86_64,
qpp-cpu target
Description
cudaq.adjoint(kernel, ...)fails at launch withfor a kernel that is purely unitary (only
xandry, no measurement orreset, and verified unitary by dense-matrix reconstruction) when
conditional
xgates conjugate a rotation inside nested loops — thestandard "flip control pattern / rotate / unflip" idiom.
Each ingredient adjoints fine in isolation (loops with computed bounds,
ifbranches, controlled rotations on register views, list arguments);only the combination below fails.
Steps to reproduce
The script runs the same roundtrip twice: once with the autogenerated
adjoint (fails at launch), once with the adjoint written out by hand
(runs, and is the exact inverse). The hand-written
undo_conjugated_rotationsis the same loops in reverse order with therotation negated — the X conjugation is its own inverse — demonstrating
the adjoint is well-defined and expressible in the kernel language; only
autogeneration refuses it.
Observed
(from
NVQIR.cpp:875, at first launch ofroundtrip_autogen).Expected
The adjoint of a unitary kernel with statically-bounded control flow to be
generated: the loop bounds derive only from
qubits.size()and loopconstants, the branch conditions are pure classical arithmetic on loop
variables, and the hand-written inverse above shows the reversed kernel is
directly expressible in the same language.
Note: possibly related to the silent-wrong-adjoint case in #4897 — that
one has no conditional gates and produces a wrong circuit instead of an
error.
Environment
releases/v0.15.0)qpp-cputarget