Skip to content

kernel.apply_call raises AttributeError for a value-returning @cudaq.kernel #5164

Description

@udsy19

Issue draft — NVIDIA/cuda-quantum (not filed)

Title: kernel.apply_call raises AttributeError for any value-returning @cudaq.kernel

Labels: applied automatically by the form (triage)


Required prerequisites

  • Consult the security policy. If reporting a security vulnerability, do not report the bug using this form. Use the process described in the policy to report the issue.
  • Make sure you've read the documentation. Your issue may be addressed there.
  • Search the issue tracker to verify that this hasn't already been reported. +1 or comment there if it has.
  • If possible, make a PR with a failing test to give us a starting point to work on!

Describe the bug

Passing a @cudaq.kernel that returns a value to kernel_builder's apply_call always fails with an AttributeError from inside the builder. Kernels that return nothing work fine, so the failure depends only on whether the callee has a return type.

The closure that wraps the decorator kernel is built in PyKernel.resolve_callable_arg. In the branch that handles a callee with a result it takes .result off the func.CallOp and then asks that OpResult for .results:

# python/cudaq/kernel/kernel_builder.py:1481-1483 (on main @ 228f099a0)
if funcTy.results:
    call = func.CallOp(fn, vs).result
    cc.ReturnOp(call.results)

func.CallOp(...).result is an OpResult, which has no .results, so the line is unreachable-in-practice code that was never exercised — there is no test in python/tests where a builder apply_calls a value-returning decorator kernel.

There is a second defect hiding behind the first, which is why this is not a one-token fix. Once the closure genuinely returns a value, the lambda's !cc.callable signature has a result, but __applyControlOrAdjoint hardcodes an empty result list on the call:

# python/cudaq/kernel/kernel_builder.py:729
cc.CallCallableOp([], otherFuncCloned, mlirValues)

cudaq::cc::CallCallableOp::verify() (lib/Optimizer/Dialect/CC/CCOps.cpp:2544-2551) checks coarity, so fixing only the AttributeError swaps the crash for an invalid module (see below). Both spots need the fix.

Steps to reproduce the bug

import cudaq


@cudaq.kernel
def flipAndMeasure(q: cudaq.qubit) -> bool:
    x(q)
    return mz(q)


flipAndMeasure.compile()

kernel = cudaq.make_kernel()
q = kernel.qalloc()
kernel.apply_call(flipAndMeasure, q)
kernel.mz(q)
print(cudaq.sample(kernel))

Output on CUDA-Q 0.15.1 (macOS arm64, cuda-quantum-cu13 wheel):

Traceback (most recent call last):
  File "repro.py", line 14, in <module>
    kernel.apply_call(flipAndMeasure, q)
  File ".../cudaq/kernel/kernel_builder.py", line 1428, in apply_call
    target = self.resolve_callable_arg(self.insertPoint,
                                       DecoratorCapture(target))
  File ".../cudaq/kernel/kernel_builder.py", line 1481, in resolve_callable_arg
    cc.ReturnOp(call.results)
                ^^^^^^^^^^^^
AttributeError: 'cudaq.mlir._mlir_libs._mlir.ir.OpResult' object has no attribute 'results'

Dropping the return annotation (def flipAndMeasure(q: cudaq.qubit):) makes the same program work, which isolates the trigger to the callee's return type.

For the second defect: changing only func.CallOp(fn, vs).result to func.CallOp(fn, vs) gets past the AttributeError and then produces a module that does not verify:

error: 'cc.call_callable' op call has incorrect coarity
RuntimeError: pass pipeline failed

Expected behavior

kernel.apply_call(valueReturningKernel, ...) should apply the callee just like any other kernel. The callee's return value is not observable through apply_call (it returns None, and the plain func.call path for builder-to-builder calls already discards the callee result the same way), but the call itself must be emitted and the module must verify.

A related API question for the maintainers, which I have deliberately left out of the fix: should apply_call return a QuakeValue for the callee's result so the builder can consume it? That would be a new API surface rather than a bug fix, so the fix below keeps apply_call returning None and simply stops the crash / invalid IR. Happy to follow up if that is wanted.

Is this a regression? If it is, put the last known working version (or commit) here.

Not a regression as far as I can tell. apply_call learned to accept @cudaq.kernel decorators in #3667 / #3693, and this branch of the closure builder appears never to have worked.

Environment

  • CUDA-Q version: 0.15.1 (cuda-quantum-cu13 wheel, commit aca5853); code inspected at 228f099a0 on main
  • Python version: 3.13
  • C++ compiler: n/a (pip wheel)
  • Operating system: macOS 15 (arm64)

Suggestions

Two changes in python/cudaq/kernel/kernel_builder.py:

  1. In resolve_callable_arg, keep the op and forward its results:

    call = func.CallOp(fn, vs)
    cc.ReturnOp(call.results)
  2. In __applyControlOrAdjoint, carry the callable's result types over to the call so cc.call_callable verifies:

    lambdaTy = FunctionType(TypeAttr(target.attributes['function_type']).value)
    otherFTy = lambdaTy.inputs
    otherResTy = lambdaTy.results
    ...
    cc.CallCallableOp(otherResTy, otherFuncCloned, mlirValues)

Only apply_call reaches the cc.create_lambda / cc.call_callable path — adjoint and control pass their target straight to __cloneOrGetFunction — so this does not affect those.

I have a PR ready with these two changes plus a regression test in
python/tests/builder/test_kernel_builder.py.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions