NVIDIA / NVIDIA/cuda-quantum

swap.ctrl loses its control when translating to OpenQASM 2

Open
#5,192 6 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

stale-notified
Dominant language
C++
Stars
1.1k
Forks
455
Avg merge
1d 22h
Merged PRs (30d)
165

Description

Describe the bug

Translating a kernel that uses swap.ctrl to OpenQASM 2 drops the control. The emitted circuit is an unconditional swap, so it implements a different operator than the one CUDA-Q itself simulates, with no error and no warning.

The decomposition is SwapToCX, in lib/Optimizer/Transforms/DecompositionPatterns.cpp. It reads op.getTarget(0) and op.getTarget(1) and emits three CNOTs between them. It never reads op.getControls(), so any controls on the SwapOp are discarded.

Steps to reproduce the bug
import cudaq

@cudaq.kernel
def k():
    q = cudaq.qvector(3)
    x(q[1])
    swap.ctrl(q[0], q[1], q[2])   # control is |0>, so this should do nothing

print(cudaq.get_state(k))
print(cudaq.translate(k, format="openqasm2"))

The state CUDA-Q computes is |010>, which is right: the control is |0>, so the swap does not happen.

The OpenQASM 2 it emits is

qreg var0[3];
x var0[1];
cx var0[2], var0[1];
cx var0[1], var0[2];
cx var0[2], var0[1];

var0[0] does not appear at all. Those three CNOTs are an unconditional swap of var0[1] and var0[2], so running the emitted circuit gives |001>.

I also checked it with MQT QCEC, comparing

x q[1]; cswap q[0], q[1], q[2];

against the emitted version. It returns not_equivalent.

One note on reproducing: the control has to be |0>. With the control in |1> the controlled swap and the unconditional swap agree, so that case passes either way.

Expected behavior

The emitted OpenQASM 2 should implement the same operator as the kernel. For a controlled swap that means a Fredkin, for example cx b,a; ccx c,a,b; cx b,a.

Alternatively the pattern could decline to fire when the op has controls, which is what the other patterns in that file do. HToPhasedRx has

if (!op.getControls().empty())
  return failure();

and the file already has a helper for it, checkNumControls(op, n), used by CHToCX among others. SwapToCX calling checkNumControls(op, 0) would make it bail out and leave the controlled swap to another pattern.

I looked at the other 31 patterns with that signature in the file. SwapToCX is the only one that both ignores controls and is reachable with them from the Python API. ExpPauliDecomposition also never mentions controls, but exp_pauli has no .ctrl in the Python DSL, so I could not produce a case for it and am not claiming one.

Is this a regression?

I do not know. I only tested 0.15.1.

Environment

CUDA-Q 0.15.1, commit aca5853a76d499ecc3d5f97c2e06163ae99d9c75, installed with pip install cudaq. Python 3.11.16, Linux x86_64.

QIR output is not affected: there the controlled swap survives as __quantum__qis__swap__ctl, so this looks specific to paths where the swap has to be decomposed.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in lib/Optimizer/Transforms/DecompositionPatterns.cpp at the SwapToCX pattern, then compare its control handling with HToPhasedRx and CHToCX and the checkNumControls helper. Run the supplied controlled-swap Python example and inspect the OpenQASM 2 output. Done means controlled swaps preserve their control during translation, or the pattern declines to fire when controls are present.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, python
Domain
compilers
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.