Skip to content

cudaq.adjoint fails ('could not autogenerate the adjoint of a kernel') on a unitary kernel with conditionally-conjugated rotations in nested loops #4898

Description

@wsttiger

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

Metadata

Metadata

Labels

stale-notifiedStale notification has already fired for this issue

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions