diff --git a/cudaq/include/cudaq/Optimizer/Transforms/Passes.h b/cudaq/include/cudaq/Optimizer/Transforms/Passes.h index ece22027a5b..621ae42a3d3 100644 --- a/cudaq/include/cudaq/Optimizer/Transforms/Passes.h +++ b/cudaq/include/cudaq/Optimizer/Transforms/Passes.h @@ -26,7 +26,8 @@ namespace cudaq::opt { /// Add a pass pipeline to transform call between kernels to direct calls that /// do not go through the runtime layers, inline all calls, and detect if calls /// to kernels remain in the fully inlined into entry point kernel. -void addAggressiveInlining(mlir::OpPassManager &pm, bool fatalCheck = false); +void addAggressiveInlining(mlir::OpPassManager &pm, bool fatalCheck = false, + bool lowerUnwind = true); void registerAggressiveInliningPipeline(); void registerUnrollingPipeline(); diff --git a/cudaq/include/cudaq/Target/CompileTarget.h b/cudaq/include/cudaq/Target/CompileTarget.h index b6058bb7dfd..1d2d6d1d692 100644 --- a/cudaq/include/cudaq/Target/CompileTarget.h +++ b/cudaq/include/cudaq/Target/CompileTarget.h @@ -44,6 +44,10 @@ struct CompileTarget { /// Optional pass pipeline to run after code generation. std::string postCodeGenPasses; + /// How the AOT pipeline handles unwind operations. The `none` mode causes + /// compilation to fail if unwind operations remain in the IR. + std::string aotUnwindMode = "cfg"; + /// Whether to disable qubit mapping. bool disableQubitMapping = false; diff --git a/cudaq/include/cudaq/Target/TargetConfig.h b/cudaq/include/cudaq/Target/TargetConfig.h index 3e0fce242fb..9505eecbf0d 100644 --- a/cudaq/include/cudaq/Target/TargetConfig.h +++ b/cudaq/include/cudaq/Target/TargetConfig.h @@ -105,6 +105,10 @@ struct BackendEndConfigEntry { std::string JITHighLevelPipeline; std::string JITMidLevelPipeline; std::string JITLowLevelPipeline; + /// How the AOT pipeline handles unwind operations: `cfg`, `dataflow`, + /// `none`. The `none` mode performs no unwind lowering, causing compilation + /// to fail if unwind operations remain in the IR. + std::string AOTUnwindMode; /// Exact cudaq-opt passes for pseudo-targets std::string TargetPassPipeline; /// Codegen emission configuration (hardware REST QPU) diff --git a/cudaq/include/cudaq/Target/TargetConfigYaml.h b/cudaq/include/cudaq/Target/TargetConfigYaml.h index 346a150069c..0df6f27d7a9 100644 --- a/cudaq/include/cudaq/Target/TargetConfigYaml.h +++ b/cudaq/include/cudaq/Target/TargetConfigYaml.h @@ -52,6 +52,8 @@ struct MappingTraits { template <> struct MappingTraits { static void mapping(IO &io, cudaq::config::BackendEndConfigEntry &info); + static std::string validate(IO &io, + cudaq::config::BackendEndConfigEntry &info); }; template <> struct MappingTraits { diff --git a/cudaq/lib/Optimizer/Transforms/AggressiveInlining.cpp b/cudaq/lib/Optimizer/Transforms/AggressiveInlining.cpp index 519b8de6b28..b5b25ef9244 100644 --- a/cudaq/lib/Optimizer/Transforms/AggressiveInlining.cpp +++ b/cudaq/lib/Optimizer/Transforms/AggressiveInlining.cpp @@ -160,16 +160,18 @@ class CheckKernelCalls static void defaultInlinerOptPipeline(OpPassManager &pm) {} /// Run the passes in the correct order. -/// 1) Lower unwind control flow before creating call-site scopes. +/// 1) Optionally lower unwind control flow before creating call-site scopes. /// 2) Convert calls between kernels to direct calls (on the QPU). /// 3) Aggressively inline all calls. /// 4) Detect if kernel inlining has failed and left behind calls to kernels. /// Such a failure is most likely a sign that there is a cycle in the call /// graph. [This check is a bad idea: this should be deferred to final codegen /// when translating the final Quake IR.] -void cudaq::opt::addAggressiveInlining(OpPassManager &pm, bool fatalChecks) { +void cudaq::opt::addAggressiveInlining(OpPassManager &pm, bool fatalChecks, + bool lowerUnwind) { llvm::StringMap opPipelines; - pm.addNestedPass(cudaq::opt::createUnwindLowering()); + if (lowerUnwind) + pm.addNestedPass(cudaq::opt::createUnwindLowering()); pm.addPass(cudaq::opt::createConvertToDirectCalls()); pm.addPass(createInlinerPass(opPipelines, defaultInlinerOptPipeline)); if (fatalChecks) @@ -192,6 +194,10 @@ struct AggressiveInliningPipelineOptions *this, "fatal-check", llvm::cl::desc("run checker and produce fatal errors immediately"), llvm::cl::init(false)}; + PassOptions::Option lowerUnwind{ + *this, "lower-unwind", + llvm::cl::desc("lower unwind operations before inlining"), + llvm::cl::init(true)}; }; } // namespace @@ -200,6 +206,6 @@ void cudaq::opt::registerAggressiveInliningPipeline() { "aggressive-inlining", "Convert calls between kernels to direct calls and inline functions.", [](OpPassManager &pm, const AggressiveInliningPipelineOptions &opt) { - addAggressiveInlining(pm, opt.runFatalChecker); + addAggressiveInlining(pm, opt.runFatalChecker, opt.lowerUnwind); }); } diff --git a/cudaq/lib/Optimizer/Transforms/Pipelines.cpp b/cudaq/lib/Optimizer/Transforms/Pipelines.cpp index 9c17c3f5175..d39116b6b26 100644 --- a/cudaq/lib/Optimizer/Transforms/Pipelines.cpp +++ b/cudaq/lib/Optimizer/Transforms/Pipelines.cpp @@ -13,6 +13,8 @@ using namespace mlir; namespace { +enum class AOTUnwindMode { CFG, Dataflow, None }; + struct TargetPrepPipelineOptions : public PassPipelineOptions { PassOptions::Option eraseNoise{ @@ -50,6 +52,17 @@ struct TargetFinalizationPipelineOptions }; struct PythonAOTOptions : public PassPipelineOptions { + PassOptions::Option unwindMode{ + *this, "unwind-mode", + llvm::cl::desc("Select how the AOT pipeline handles unwind operations."), + llvm::cl::init(AOTUnwindMode::CFG), + llvm::cl::values( + clEnumValN(AOTUnwindMode::CFG, "cfg", + "Lower unwind operations to control-flow operations."), + clEnumValN(AOTUnwindMode::Dataflow, "dataflow", + "Lower unwind operations using structured data flow."), + clEnumValN(AOTUnwindMode::None, "none", + "Do not lower unwind operations."))}; PassOptions::Option autoGenRunStack{ *this, "gen-run-stack", llvm::cl::desc("Autogenerate the cudaq::run dispatch stack."), @@ -298,7 +311,16 @@ static void createPythonAOTPipeline(OpPassManager &pm, const PythonAOTOptions &options) { // NB: This pipeline should be kept in synch with the pipeline in nvq++. pm.addNestedPass(cudaq::opt::createVariableCoalesce()); - pm.addNestedPass(cudaq::opt::createUnwindLowering()); + switch (options.unwindMode) { + case AOTUnwindMode::CFG: + pm.addNestedPass(cudaq::opt::createUnwindLowering()); + break; + case AOTUnwindMode::Dataflow: + pm.addNestedPass(cudaq::opt::createUnwindByDataFlow()); + break; + case AOTUnwindMode::None: + break; + } pm.addNestedPass(createCanonicalizerPass()); pm.addNestedPass(cudaq::opt::createInjectImplicitOutput()); pm.addNestedPass(cudaq::opt::createAddDeallocs()); @@ -315,7 +337,10 @@ static void createPythonAOTPipeline(OpPassManager &pm, pm.addPass(cudaq::opt::createGenerateKernelExecution(gkeOpts)); if (options.autoGenRunStack) pm.addPass(cudaq::opt::createRunSemanticsHackery()); - cudaq::opt::addAggressiveInlining(pm); + // `none` leaves unwind operations in the IR so downstream lowering causes + // compilation to fail if the source contains unwinding control flow. + cudaq::opt::addAggressiveInlining(pm, /*fatalCheck=*/false, + options.unwindMode != AOTUnwindMode::None); pm.addNestedPass(cudaq::opt::createQuakeAddMetadata()); pm.addNestedPass(cudaq::opt::createConstantPropagation()); pm.addNestedPass(cudaq::opt::createLiftArrayAlloc()); @@ -339,7 +364,7 @@ void cudaq::opt::createPythonAOTPipeline(OpPassManager &pm, static void registerPythonAOTPipeline() { PassPipelineRegistration( - "aot-prep-pipeline", + "python-aot-pipeline", "Pipeline to lower code for simulation or JIT compilation.", [](OpPassManager &pm, const PythonAOTOptions &options) { ::createPythonAOTPipeline(pm, options); diff --git a/cudaq/lib/Optimizer/Transforms/UnwindByDataFlow.cpp b/cudaq/lib/Optimizer/Transforms/UnwindByDataFlow.cpp index 89a457c5694..558cbf96a65 100644 --- a/cudaq/lib/Optimizer/Transforms/UnwindByDataFlow.cpp +++ b/cudaq/lib/Optimizer/Transforms/UnwindByDataFlow.cpp @@ -217,6 +217,11 @@ class UnwindByDataFlowPass auto i8Ty = rewriter.getI8Type(); rewriter.setInsertionPoint(&func.getBody().front().front()); dfJump = cudaq::cc::AllocaOp::create(rewriter, loc, i8Ty); + // Generated guards may load `dfJump` before an unwind occurs, so start + // the control state at `JumpKind::None` rather than leaving it undefined. + const Value noJump = arith::ConstantIntOp::create( + rewriter, loc, static_cast(JumpKind::None), 8); + cudaq::cc::StoreOp::create(rewriter, loc, noJump, dfJump); for (auto *unwind : analysis.unwindOps) if (failed(genDominatingVars(rewriter, unwind, landingPadMap, diff --git a/cudaq/lib/Target/CompileTarget.cpp b/cudaq/lib/Target/CompileTarget.cpp index 909836f9e75..671301a3dd6 100644 --- a/cudaq/lib/Target/CompileTarget.cpp +++ b/cudaq/lib/Target/CompileTarget.cpp @@ -67,6 +67,9 @@ cudaq::CompileTarget cudaq::CompileTarget::createFromConfig( const auto &backendConfig = targetConfig.BackendConfig.value_or(defaultConfig); + if (!backendConfig.AOTUnwindMode.empty()) + target.pipelineConfig.aotUnwindMode = backendConfig.AOTUnwindMode; + auto prepPipeline = [&](const std::string &stage, const std::string &stageName) { std::string pipeline = stage; @@ -128,5 +131,5 @@ std::size_t std::hash::operator()( return cudaq::detail::hashVal( pc.overridePassPipeline, pc.highLevelPipeline, pc.midLevelPipeline, pc.lowLevelPipeline, pc.codegenTranslation, pc.postCodeGenPasses, - pc.disableQubitMapping, pc.replaceStateWithKernel); + pc.aotUnwindMode, pc.disableQubitMapping, pc.replaceStateWithKernel); } diff --git a/cudaq/lib/Target/Yaml/TargetConfigYaml.cpp b/cudaq/lib/Target/Yaml/TargetConfigYaml.cpp index 49d509bf12d..ed9a306e635 100644 --- a/cudaq/lib/Target/Yaml/TargetConfigYaml.cpp +++ b/cudaq/lib/Target/Yaml/TargetConfigYaml.cpp @@ -64,6 +64,9 @@ static std::string processSimBackendConfig( output << "JIT_LOW_LEVEL_PIPELINE=\"" << configValue.JITLowLevelPipeline << "\"\n"; + if (!configValue.AOTUnwindMode.empty()) + output << "AOT_UNWIND_MODE=\"" << configValue.AOTUnwindMode << "\"\n"; + if (!configValue.TargetPassPipeline.empty()) output << "TARGET_PASS_PIPELINE=\"" << configValue.TargetPassPipeline << "\"\n"; @@ -284,8 +287,11 @@ cudaq::config::parseTargetConfig(std::string yamlContent, auto substitutedYamlContent = cudaq::config::substitutePluginRoot(std::move(yamlContent), pluginRoot); cudaq::config::TargetConfig config; - llvm::yaml::Input Input(substitutedYamlContent.c_str()); - Input >> config; + llvm::yaml::Input input(substitutedYamlContent.c_str()); + input >> config; + if (const auto error = input.error()) + throw std::runtime_error("Invalid target configuration: " + + error.message()); return config; } @@ -381,6 +387,7 @@ void MappingTraits::mapping( io.mapOptional("jit-high-level-pipeline", info.JITHighLevelPipeline); io.mapOptional("jit-mid-level-pipeline", info.JITMidLevelPipeline); io.mapOptional("jit-low-level-pipeline", info.JITLowLevelPipeline); + io.mapOptional("unwind-mode", info.AOTUnwindMode); io.mapOptional("target-pass-pipeline", info.TargetPassPipeline); io.mapOptional("codegen-emission", info.CodegenEmission); io.mapOptional("post-codegen-passes", info.PostCodeGenPasses); @@ -397,6 +404,14 @@ void MappingTraits::mapping( io.mapOptional("rules", info.ConditionalBuildConfigs); } +std::string MappingTraits::validate( + IO &io, cudaq::config::BackendEndConfigEntry &info) { + if (info.AOTUnwindMode.empty() || info.AOTUnwindMode == "cfg" || + info.AOTUnwindMode == "dataflow" || info.AOTUnwindMode == "none") + return {}; + return "'unwind-mode' must be one of: cfg, dataflow, none."; +} + void MappingTraits::mapping( IO &io, cudaq::config::BackendFeatureMap &info) { io.mapRequired("name", info.Name); diff --git a/cudaq/test/Transforms/aot_run_vector_copy.qke b/cudaq/test/Transforms/aot_run_vector_copy.qke index e7439987fd4..15144d5d7fb 100644 --- a/cudaq/test/Transforms/aot_run_vector_copy.qke +++ b/cudaq/test/Transforms/aot_run_vector_copy.qke @@ -5,7 +5,8 @@ // the terms of the Apache License 2.0 which accompanies this distribution. // // ========================================================================== // -// RUN: cudaq-opt --pass-pipeline='builtin.module(aot-prep-pipeline)' %s | FileCheck %s +// RUN: cudaq-opt --pass-pipeline='builtin.module(python-aot-pipeline)' %s \ +// RUN: | FileCheck %s // Regression test for the Python AOT pipeline's vector-return handling. // RunSemanticsHackery must remove the heap copy from the generated `.run` diff --git a/cudaq/test/Transforms/aot_unwind_modes.qke b/cudaq/test/Transforms/aot_unwind_modes.qke new file mode 100644 index 00000000000..66c45557ca9 --- /dev/null +++ b/cudaq/test/Transforms/aot_unwind_modes.qke @@ -0,0 +1,52 @@ +// ========================================================================== // +// Copyright (c) 2026 NVIDIA Corporation & Affiliates. // +// // +// This source code and the accompanying materials are made available under // +// the terms of the Apache License 2.0 which accompanies this distribution. // +// ========================================================================== // + +// RUN: cudaq-opt --dump-pass-pipeline \ +// RUN: --pass-pipeline='builtin.module(python-aot-pipeline{unwind-mode=dataflow})' \ +// RUN: %s 2>&1 | FileCheck %s --check-prefix=DATAFLOW +// RUN: cudaq-opt --dump-pass-pipeline \ +// RUN: --pass-pipeline='builtin.module(python-aot-pipeline{unwind-mode=cfg})' \ +// RUN: %s 2>&1 | FileCheck %s --check-prefix=CFG +// RUN: cudaq-opt --dump-pass-pipeline \ +// RUN: --pass-pipeline='builtin.module(python-aot-pipeline{unwind-mode=none})' \ +// RUN: %s 2>&1 | FileCheck %s --check-prefix=NONE +// RUN: not cudaq-opt \ +// RUN: --pass-pipeline='builtin.module(python-aot-pipeline{unwind-mode=invalid})' \ +// RUN: %s 2>&1 | FileCheck %s --check-prefix=INVALID +// RUN: not cudaq-opt \ +// RUN: --pass-pipeline='builtin.module(python-aot-pipeline{unwind-mode=none},lower-to-cfg)' \ +// RUN: %s 2>&1 | FileCheck %s --check-prefix=NONE-ERROR + +module { + func.func @early_return(%arg0: i1) -> i32 { + %c1 = arith.constant 1 : i32 + cc.if(%arg0) { + cc.unwind_return %c1 : i32 + } + %c0 = arith.constant 0 : i32 + return %c0 : i32 + } +} + +// DATAFLOW: Pass Manager with 28 passes: +// DATAFLOW: variable-coalesce +// DATAFLOW: unwind-by-dataflow +// DATAFLOW: canonicalize + +// CFG: Pass Manager with 28 passes: +// CFG: variable-coalesce +// CFG: unwind-lowering +// CFG: canonicalize + +// NONE: Pass Manager with 26 passes: +// NONE-NOT: unwind-lowering +// NONE-NOT: unwind-by-dataflow +// NONE: get-concrete-matrix + + +// INVALID: Cannot find option named 'invalid' +// NONE-ERROR: error: 'cc.unwind_return' op diff --git a/cudaq/tools/nvq++/nvq++.in b/cudaq/tools/nvq++/nvq++.in index 83af2eba716..97a2b6b2f4d 100644 --- a/cudaq/tools/nvq++/nvq++.in +++ b/cudaq/tools/nvq++/nvq++.in @@ -584,6 +584,7 @@ ENABLE_ARRAY_CONVERSION=true ENABLE_VARIABLE_COALESCE=true ENABLE_LEGACY_ADJOINT=false ENABLE_UNWIND_LOWERING=true +AOT_UNWIND_MODE=cfg ENABLE_DEVICE_CODE_LOADER=true ENABLE_REALTIME_LOWERING=false ENABLE_KERNEL_EXECUTION=true @@ -1036,8 +1037,21 @@ if ${ENABLE_VARIABLE_COALESCE}; then OPT_PASSES="func.func(variable-coalesce)" fi if ${ENABLE_UNWIND_LOWERING}; then - RUN_OPT=true - OPT_PASSES=$(add_pass_to_pipeline "${OPT_PASSES}" "func.func(unwind-lowering)") + case "${AOT_UNWIND_MODE}" in + cfg) + RUN_OPT=true + OPT_PASSES=$(add_pass_to_pipeline "${OPT_PASSES}" "func.func(unwind-lowering)") + ;; + dataflow) + RUN_OPT=true + OPT_PASSES=$(add_pass_to_pipeline "${OPT_PASSES}" "func.func(unwind-by-dataflow)") + ;; + none) + ;; + *) + error_exit "Invalid AOT unwind mode: (${AOT_UNWIND_MODE})" + ;; + esac fi if ${ENABLE_INJECT_IMPLICIT_OUTPUT}; then RUN_OPT=true @@ -1078,9 +1092,15 @@ if ${ENABLE_KERNEL_EXECUTION}; then fi fi if ${ENABLE_AGGRESSIVE_INLINE}; then + AGGRESSIVE_INLINE_OPTIONS= + # Do not let aggressive-inlining lower unwind operations after the AOT + # unwind stage deliberately left them in the IR. + if [[ "${AOT_UNWIND_MODE}" == "none" ]] || [[ "${ENABLE_UNWIND_LOWERING}" == "false" ]]; then + AGGRESSIVE_INLINE_OPTIONS="{lower-unwind=false}" + fi RUN_OPT=true if ${DO_LINK}; then - OPT_PASSES=$(add_pass_to_pipeline "${OPT_PASSES}" "aggressive-inlining") + OPT_PASSES=$(add_pass_to_pipeline "${OPT_PASSES}" "aggressive-inlining${AGGRESSIVE_INLINE_OPTIONS}") else OPT_PASSES=$(add_pass_to_pipeline "${OPT_PASSES}" "indirect-to-direct-calls,inline") fi diff --git a/python/cudaq/kernel/ast_bridge.py b/python/cudaq/kernel/ast_bridge.py index ecee44356b6..cccc7d985e9 100644 --- a/python/cudaq/kernel/ast_bridge.py +++ b/python/cudaq/kernel/ast_bridge.py @@ -34,7 +34,7 @@ from .utils import (Color, boundaryDiagnostic, containsMeasureHandle, globalRegisteredOperations, globalRegisteredTypes, nvqppPrefix, mlirTypeFromAnnotation, mlirTypeFromPyType, - getMLIRContext, is_recovered_value_ok, + getAOTPassPipeline, getMLIRContext, is_recovered_value_ok, recover_annotation_of_or_none, recover_value_of_or_none, cudaq__unique_attr_name, mlirTryCreateStructType) @@ -6284,8 +6284,7 @@ def compile_to_mlir(uniqueId, astModule, signature: KernelSignature, defFrame, # The `cudaq.pipeline.aot` span is the marker tooling uses to identify # pass events as AOT-pipeline (paired with `cudaq.pipeline.jit` emitted # from `QPU.cpp` `lower_to_qir_llvm`). - pm = PassManager.parse("builtin.module(aot-prep-pipeline)", - context=bridge.ctx) + pm = PassManager.parse(getAOTPassPipeline(), context=bridge.ctx) try: with trace.span("cudaq.pipeline.aot"): cudaq_runtime.runPassManager(pm, bridge.module) diff --git a/python/cudaq/kernel/kernel_builder.py b/python/cudaq/kernel/kernel_builder.py index e7c4cde03ae..aac8d8b918a 100644 --- a/python/cudaq/kernel/kernel_builder.py +++ b/python/cudaq/kernel/kernel_builder.py @@ -34,10 +34,10 @@ from .kernel_decorator import DecoratorCapture, LinkedKernelCapture, isa_kernel_decorator from .quake_value import QuakeValue from .utils import (boundaryDiagnostic, containsMeasureHandle, emitFatalError, - emitWarning, nvqppPrefix, getMLIRContext, recover_func_op, - mlirTypeToPyType, cudaq__unique_attr_name, - mlirTypeFromPyType, emitErrorIfInvalidPauli, - globalRegisteredOperations) + emitWarning, nvqppPrefix, getAOTPassPipeline, + getMLIRContext, recover_func_op, mlirTypeToPyType, + cudaq__unique_attr_name, mlirTypeFromPyType, + emitErrorIfInvalidPauli, globalRegisteredOperations) kDynamicPtrIndex: int = -2147483648 @@ -1712,6 +1712,8 @@ def apply_noise(self, noise_channel, *args): def clearCache(self): if hasattr(self, 'qkeModule'): del self.qkeModule + if hasattr(self, '_cached_aot_pipeline_hash'): + del self._cached_aot_pipeline_hash if hasattr(self, '_compiled_module_cache'): del self._compiled_module_cache @@ -1748,11 +1750,14 @@ def compile(self): A `PyKernel` can be dynamically extended up until it is reified to be used in a launch scenario. We reify the kernel as-is here. """ - if not hasattr(self, 'qkeModule'): + # Track the AOT pipeline used to compile `qkeModule` so target changes + # cannot reuse a module compiled with an incompatible pipeline. + aot_pipeline_hash = cudaq_runtime.get_aot_pipeline_hash() + if (not hasattr(self, 'qkeModule') or getattr( + self, '_cached_aot_pipeline_hash', None) != aot_pipeline_hash): self.qkeModule = cudaq_runtime.cloneModule(self.module) ctx = getMLIRContext() - pm = PassManager.parse("builtin.module(aot-prep-pipeline)", - context=ctx) + pm = PassManager.parse(getAOTPassPipeline(), context=ctx) try: with trace.span("cudaq.pipeline.aot"): cudaq_runtime.runPassManager(pm, self.qkeModule) @@ -1762,6 +1767,7 @@ def compile(self): self.qkeModule.operation.attributes.__setitem__( cudaq__unique_attr_name, StringAttr.get(self.uniqName, context=ctx)) + self._cached_aot_pipeline_hash = aot_pipeline_hash def __call__(self, *args): """ diff --git a/python/cudaq/kernel/kernel_decorator.py b/python/cudaq/kernel/kernel_decorator.py index b9bad65b38b..731eab75afd 100644 --- a/python/cudaq/kernel/kernel_decorator.py +++ b/python/cudaq/kernel/kernel_decorator.py @@ -147,6 +147,9 @@ def __init__(self, self.name = kernelName self.verbose = verbose self.disable_quantum_optimization = disable_quantum_optimization + # Track the `AOT` pipeline used to compile `_cached_qkeModule` so target + # changes cannot reuse a module compiled with an incompatible pipeline. + self._cached_aot_pipeline_hash = None self._atomic_quantum_region = atomic_quantum_region self.defFrame = _recover_defining_frame() # Whether we are currently resolving arguments to self. Used to detect @@ -183,6 +186,8 @@ def __init__(self, self.uniqName = kernelName self._cached_qkeModule = module + self._cached_aot_pipeline_hash = ( + cudaq_runtime.get_aot_pipeline_hash()) self.astModule = None self.signature = KernelSignature.parse_from_mlir( self.qkeModule, self.uniqName) @@ -228,7 +233,7 @@ def __del__(self): @ensure_compiled def qkeModule(self): """ - A target independent Quake MLIR representation of the kernel. + An `AOT` Quake MLIR representation for the current compile target. """ return self._cached_qkeModule @@ -265,12 +270,14 @@ def _ensure_compiled(self): """ Ensure that the kernel is compiled. """ - if self._cached_qkeModule is None: + if not self.is_compiled(): self.compile() def is_compiled(self): """Whether the kernel has already been compiled.""" - return self._cached_qkeModule is not None + return (self._cached_qkeModule is not None and + self._cached_aot_pipeline_hash + == cudaq_runtime.get_aot_pipeline_hash()) def supports_compilation(self): """Whether the kernel can be compiled for the current target.""" @@ -292,6 +299,7 @@ def compile(self): f"'{cudaq_runtime.get_target().name}' does not support " f"compilation") + aot_pipeline_hash = cudaq_runtime.get_aot_pipeline_hash() self._cached_qkeModule = compile_to_mlir( id(self), self.astModule, @@ -304,6 +312,7 @@ def compile(self): cudaqAliases=getattr(self, 'cudaqAliases', None), disable_quantum_optimization=self.disable_quantum_optimization, atomic_quantum_region=self.atomic_quantum_region) + self._cached_aot_pipeline_hash = aot_pipeline_hash # recursively compile any captured kernels if required for captured_arg in self.signature.captured_args: diff --git a/python/cudaq/kernel/utils.py b/python/cudaq/kernel/utils.py index 189664cfdf7..5bdb9e57d18 100644 --- a/python/cudaq/kernel/utils.py +++ b/python/cudaq/kernel/utils.py @@ -45,6 +45,11 @@ "entry-point kernels must discriminate first") +def getAOTPassPipeline(): + mode = cudaq_runtime.get_aot_unwind_mode() + return f"builtin.module(python-aot-pipeline{{unwind-mode={mode}}})" + + def containsMeasureHandle(ty, _seen=None): """Return True iff ``ty`` is ``!cc.measure_handle`` or transitively contains one. The walk stops at callable / function-type boundaries: a diff --git a/python/runtime/cudaq/platform/py_alt_launch_kernel.cpp b/python/runtime/cudaq/platform/py_alt_launch_kernel.cpp index a0512b3c7da..8e4f8d69b5b 100644 --- a/python/runtime/cudaq/platform/py_alt_launch_kernel.cpp +++ b/python/runtime/cudaq/platform/py_alt_launch_kernel.cpp @@ -1074,8 +1074,8 @@ nanobind::object cudaq::marshal_and_launch_module( std::shared_ptr cache) { // Marker span identifying every nested pass / scoped trace as part of the // JIT-time pipeline. Paired with the cudaq.pipeline.aot span emitted around - // aot-prep-pipeline in compile_to_mlir; tooling reads the trace ancestry to - // attribute pass events to AOT vs JIT. + // python-aot-pipeline in compile_to_mlir; tooling reads the trace ancestry + // to attribute pass events to AOT vs JIT. // // This site is the funnel for kernel-call / sample / observe / // estimate_resources execution paths: each ultimately calls diff --git a/python/runtime/cudaq/target/py_compile_target.cpp b/python/runtime/cudaq/target/py_compile_target.cpp index ed5c847bed5..41ee0b7a8dc 100644 --- a/python/runtime/cudaq/target/py_compile_target.cpp +++ b/python/runtime/cudaq/target/py_compile_target.cpp @@ -29,7 +29,8 @@ pipelineConfigRepr(const cudaq::CompileTarget::PipelineConfig &pc) { << ", mid_level_pipeline=" << reprStr(pc.midLevelPipeline) << ", low_level_pipeline=" << reprStr(pc.lowLevelPipeline) << ", codegen_translation=" << reprStr(pc.codegenTranslation) - << ", post_code_gen_passes=" << reprStr(pc.postCodeGenPasses); + << ", post_code_gen_passes=" << reprStr(pc.postCodeGenPasses) + << ", aot_unwind_mode=" << reprStr(pc.aotUnwindMode); } os << ")"; return os.str(); @@ -54,6 +55,7 @@ void cudaq::bindCompileTarget(nanobind::module_ &mod) { .def_rw("low_level_pipeline", &PipelineConfig::lowLevelPipeline) .def_rw("codegen_translation", &PipelineConfig::codegenTranslation) .def_rw("post_code_gen_passes", &PipelineConfig::postCodeGenPasses) + .def_rw("aot_unwind_mode", &PipelineConfig::aotUnwindMode) .def_rw("disable_qubit_mapping", &PipelineConfig::disableQubitMapping) .def(nanobind::self == nanobind::self) .def("__hash__", std::hash()) @@ -106,4 +108,14 @@ void cudaq::bindCompileTarget(nanobind::module_ &mod) { nanobind::arg("target"), "Compile kernels with the given `CompileTarget` instead of the one the " "active target's QPU provides."); + + mod.def("get_aot_unwind_mode", []() { + return cudaq::get_compile_target(cudaq::other_policies{}) + .pipelineConfig.aotUnwindMode; + }); + + mod.def("get_aot_pipeline_hash", []() { + return std::hash()( + cudaq::get_compile_target(cudaq::other_policies{}).pipelineConfig); + }); } diff --git a/python/tests/backends/test_experimental_compile_target.py b/python/tests/backends/test_experimental_compile_target.py index 53aea4fd92d..3d2c864f28e 100644 --- a/python/tests/backends/test_experimental_compile_target.py +++ b/python/tests/backends/test_experimental_compile_target.py @@ -218,6 +218,37 @@ def kernel() -> bool: # ---------------------------------------------------------------------------- # +def test_pipeline_config_aot_unwind_mode(): + config = PipelineConfig() + assert config.aot_unwind_mode == "cfg" + config.aot_unwind_mode = "none" + assert config.aot_unwind_mode == "none" + set_compile_target(CompileTarget(config)) + assert cudaq_runtime.get_aot_unwind_mode() == "none" + + +def test_aot_module_cache_tracks_pipeline_config(): + + @cudaq.kernel + def early_return(return_early: bool) -> int: + if return_early: + return 1 + return 0 + + builder = cudaq.make_kernel() + cfg_decorator_module = early_return.qkeModule + builder.compile() + cfg_builder_module = builder.qkeModule + + config = PipelineConfig() + config.aot_unwind_mode = "dataflow" + set_compile_target(CompileTarget(config)) + + assert early_return.qkeModule is not cfg_decorator_module + builder.compile() + assert builder.qkeModule is not cfg_builder_module + + def test_pipeline_config_equality_and_hash(): a = PipelineConfig() b = PipelineConfig() diff --git a/runtime/cudaq/platform/default/rest/helpers/quantum_machines/quantum_machines.yml b/runtime/cudaq/platform/default/rest/helpers/quantum_machines/quantum_machines.yml index c925c217314..d5020a1a207 100644 --- a/runtime/cudaq/platform/default/rest/helpers/quantum_machines/quantum_machines.yml +++ b/runtime/cudaq/platform/default/rest/helpers/quantum_machines/quantum_machines.yml @@ -22,6 +22,8 @@ config: jit-high-level-pipeline: "func.func(add-measurements),expand-measurements" jit-mid-level-pipeline: "prepare-for-wireset{add-wireset=true unroll-only-aliasing-quantum-access-loops=true},qubit-mapping{device=bypass},decomposition{basis=h,s,t,r1,rx,ry,rz,x,y,z,z(1),x(1)}" jit-low-level-pipeline: "symbol-dce" + # Preserve structured control flow for the remote Quake payload. + unwind-mode: dataflow # Tell the rest-qpu that the required output format is quake. codegen-emission: nop # Library mode is only for simulators, physical backends must turn this off diff --git a/unittests/nvqpp/backends/quake_backend/QuakeStartServerAndTest.sh.in b/unittests/nvqpp/backends/quake_backend/QuakeStartServerAndTest.sh.in index 3bcc31c8455..98ad794d1eb 100644 --- a/unittests/nvqpp/backends/quake_backend/QuakeStartServerAndTest.sh.in +++ b/unittests/nvqpp/backends/quake_backend/QuakeStartServerAndTest.sh.in @@ -62,6 +62,22 @@ else fi fi +# Structured unwind regression test. +PATH=@CMAKE_BINARY_DIR@/bin:$PATH nvq++ --target quake_fake @CMAKE_SOURCE_DIR@/unittests/nvqpp/backends/quake_backend/test_unwind_app.cpp -o test_unwind_app +if [ $? -ne 0 ]; then + echo ":x: nvq++ compilation failed for test_unwind_app" + test_err_sum=$((test_err_sum+1)) +else + echo ":white_check_mark: Successfully compiled test_unwind_app with nvq++" + ./test_unwind_app + if [ $? -ne 0 ]; then + echo ":x: test_unwind_app failed" + test_err_sum=$((test_err_sum+1)) + else + echo ":white_check_mark: Successfully ran test_unwind_app" + fi +fi + # Scalar-return test with client-side qubit mapping enabled explicitly. PATH=@CMAKE_BINARY_DIR@/bin:$PATH nvq++ --target quake_fake --mapping-file @CMAKE_SOURCE_DIR@/unittests/nvqpp/backends/quake_backend/mapping_device.txt @CMAKE_SOURCE_DIR@/unittests/nvqpp/backends/quake_backend/test_mapping_app.cpp -o test_mapping_app if [ $? -ne 0 ]; then @@ -150,6 +166,24 @@ else echo ":white_check_mark: Successfully ran Python test_app.py" fi +# Structured unwind regression through the Python AOT path. +PYTHONPATH=@CMAKE_BINARY_DIR@/python @Python_EXECUTABLE@ @CMAKE_SOURCE_DIR@/unittests/nvqpp/backends/quake_backend/test_unwind_app.py +if [ $? -ne 0 ]; then + echo ":x: Python test_unwind_app.py failed" + test_err_sum=$((test_err_sum+1)) +else + echo ":white_check_mark: Successfully ran Python test_unwind_app.py" +fi + +# Run representative Python frontend syntax through the remote server. +PYTHONPATH=@CMAKE_BINARY_DIR@/python @Python_EXECUTABLE@ @CMAKE_SOURCE_DIR@/unittests/nvqpp/backends/quake_backend/syntax_check.py +if [ $? -ne 0 ]; then + echo ":x: Python syntax_check.py failed" + test_err_sum=$((test_err_sum+1)) +else + echo ":white_check_mark: Successfully ran Python syntax_check.py" +fi + # Python loop remote execution test. CUDAQ_DUMP_JIT_IR validates that the Python # client preserves cc.loop operations for the nop codegen path. CUDAQ_DUMP_JIT_IR=1 PYTHONPATH=@CMAKE_BINARY_DIR@/python @Python_EXECUTABLE@ @CMAKE_SOURCE_DIR@/unittests/nvqpp/backends/quake_backend/test_loop_app.py > test_loop_app.py.out 2>&1 diff --git a/unittests/nvqpp/backends/quake_backend/mock_server.py b/unittests/nvqpp/backends/quake_backend/mock_server.py index 1b7940859e3..39eae940b0a 100644 --- a/unittests/nvqpp/backends/quake_backend/mock_server.py +++ b/unittests/nvqpp/backends/quake_backend/mock_server.py @@ -11,7 +11,7 @@ import uvicorn, uuid, base64, ctypes, sys, re from llvmlite import binding as llvm from cudaq.mlir.passmanager import PassManager -from cudaq.mlir.ir import Module +from cudaq.mlir.ir import Module, WalkResult from cudaq.kernel.utils import getMLIRContext from cudaq.mlir.dialects import func from cudaq.mlir.dialects import llvm as mlir_llvm @@ -35,7 +35,7 @@ "func.func(" "memtoreg,canonicalize,cc-loop-normalize," "cc-loop-unroll{maximum-iterations=1024 " - "signal-failure-if-any-loop-cannot-be-completely-unrolled=true " + "signal-failure-if-any-loop-cannot-be-completely-unrolled=false " "allow-early-exit=true}," "canonicalize" ")," @@ -46,27 +46,83 @@ "lower-to-cfg,symbol-dce,cc-to-llvm" ")") - -def verifyValueSemanticsPayload(decoded_payload): - required_tokens = ["quake.wire_set", "quake.borrow_wire"] - for token in required_tokens: - if token not in decoded_payload: +REQUIRED_QUAKE_OPERATIONS = {"quake.wire_set", "quake.borrow_wire"} + +SUPPORTED_FUNC_OPERATIONS = { + "func.call", + "func.func", + "func.return", +} + +SUPPORTED_QUAKE_OPERATIONS = { + "quake.borrow_wire", + "quake.discriminate", + "quake.exp_pauli", + "quake.h", + "quake.log_output", + "quake.mx", + "quake.my", + "quake.mz", + "quake.phased_rx", + "quake.r1", + "quake.reset", + "quake.return_wire", + "quake.rx", + "quake.ry", + "quake.rz", + "quake.s", + "quake.swap", + "quake.t", + "quake.u2", + "quake.u3", + "quake.wire_set", + "quake.x", + "quake.y", + "quake.z", +} + +WIRE_SEMANTICS_OPERATIONS = SUPPORTED_QUAKE_OPERATIONS - {"quake.log_output"} + + +def isCompatibleOperation(operation_name): + if operation_name.startswith(("arith.", "cc.")): + return True + if operation_name.startswith("func."): + return operation_name in SUPPORTED_FUNC_OPERATIONS + if operation_name.startswith("quake."): + return operation_name in SUPPORTED_QUAKE_OPERATIONS + if operation_name.startswith("cf."): + return False + return True + + +def collectOperationNames(module): + operation_names = [] + + def visit(operation): + operation_names.append(operation.name) + return WalkResult.ADVANCE + + module.operation.walk(visit) + return operation_names + + +def verifyCompatibilityPayload(decoded_payload, operation_names): + operation_name_set = set(operation_names) + if operation_name_set & WIRE_SEMANTICS_OPERATIONS: + for operation_name in REQUIRED_QUAKE_OPERATIONS: + if operation_name not in operation_name_set: + raise RuntimeError( + f"Remote payload is missing `{operation_name}`. The server must" + " receive value-semantics MLIR with an assigned wireset.") + + for operation_name in operation_names: + if not isCompatibleOperation(operation_name): raise RuntimeError( - f"Remote payload is missing `{token}`. The server must receive" - " value-semantics MLIR with an assigned wireset.") - - forbidden_tokens = [ - "quake.alloca", - "quake.extract_ref", - "quake.subveq", - "quake.concat", - "quake.relax_size", - "quake.unwrap", - "quake.wrap", - "!quake.ref", - "!quake.veq", - ] - for token in forbidden_tokens: + f"Remote frontend does not support operation `{operation_name}`." + ) + + for token in ["!quake.ref", "!quake.veq"]: if token in decoded_payload: raise RuntimeError( f"Remote payload still contains reference-semantics token" @@ -137,8 +193,9 @@ def verifyModule(module, stage): def lowerValueSemanticsPayloadForExecution(recovered_mod, ctx): # The client/server contract is checked before this point. The client has # already run the target JIT pipeline through `wireset` assignment. For - # execution, the mock server fully unrolls the submitted value-semantic IR - # and lowers the `wireset` directly to QIR. + # execution, the mock server unrolls eligible value-semantic loops, lowers + # residual structured classical control flow, and lowers the `wireset` + # directly to QIR. pm = PassManager.parse(SERVER_EXECUTION_PIPELINE, context=ctx) try: pm.run(recovered_mod.operation) @@ -165,11 +222,11 @@ async def postJob(request: Request): "Input MLIR contains malloc or memcpy calls. These should have been" " eliminated by the eliminate-dead-heap-copy pass.") - verifyValueSemanticsPayload(decoded_payload) - ctx = getMLIRContext() recovered_mod = Module.parse(decoded_payload, context=ctx) verifyModule(recovered_mod, "submitted") + verifyCompatibilityPayload(decoded_payload, + collectOperationNames(recovered_mod)) pm = PassManager.parse( "builtin.module(canonicalize,distributed-device-call,cse)", context=ctx) try: @@ -191,6 +248,20 @@ async def postJob(request: Request): verifyExpectedMapping(decoded_payload, entry_func_name) verifyExpectedLoopCount(decoded_payload, entry_func_name) + # These kernels validate the submitted frontend IR only. Other applications + # in this test exercise the server's lowering and execution paths. + if "syntax_check_" in entry_func_name: + newId = str(uuid.uuid4()) + createdJobs[newId] = ("HEADER\tschema_id\tlabeled\n" + "HEADER\tschema_version\t1.0\n" + "START\n" + "METADATA\tentry_point\n" + "METADATA\tqir_profiles\tadaptive_profile\n" + "METADATA\trequired_num_qubits\t0\n" + "METADATA\trequired_num_results\t0\n" + "END\t0\n") + return ({"id": newId}, 201) + # Lower the module to LLVM IR. qir_code = lowerValueSemanticsPayloadForExecution(recovered_mod, ctx) m = llvm.module.parse_assembly(qir_code) diff --git a/unittests/nvqpp/backends/quake_backend/quake_fake.yml b/unittests/nvqpp/backends/quake_backend/quake_fake.yml index 948e81f3a78..64b466ccd2b 100644 --- a/unittests/nvqpp/backends/quake_backend/quake_fake.yml +++ b/unittests/nvqpp/backends/quake_backend/quake_fake.yml @@ -25,6 +25,8 @@ config: jit-high-level-pipeline: "func.func(add-measurements),expand-measurements" jit-mid-level-pipeline: "prepare-for-wireset{add-wireset=true unroll-only-aliasing-quantum-access-loops=true},qubit-mapping{device=bypass},decomposition{basis=h,s,t,r1,rx,ry,rz,x,y,z,z(1),x(1)}" jit-low-level-pipeline: "symbol-dce%QUAKE_EMULATE_SUFFIX%" + # Preserve structured control flow for the remote Quake payload. + unwind-mode: dataflow # Tell the rest-qpu that we are simply dumping CUDA-Q MLIR code. codegen-emission: nop # Library mode is only for simulators, physical backends must turn this off diff --git a/unittests/nvqpp/backends/quake_backend/syntax_check.py b/unittests/nvqpp/backends/quake_backend/syntax_check.py new file mode 100644 index 00000000000..328803a7bb7 --- /dev/null +++ b/unittests/nvqpp/backends/quake_backend/syntax_check.py @@ -0,0 +1,197 @@ +# ============================================================================ # +# Copyright (c) 2026 NVIDIA Corporation & Affiliates. # +# All rights reserved. # +# # +# This source code and the accompanying materials are made available under # +# the terms of the Apache License 2.0 which accompanies this distribution. # +# ============================================================================ # + +import math +import sys + +import cudaq + +cudaq.set_target("quake_fake") + +# The fake server recognizes the `syntax_check_` prefix and only validates the +# submitted IR for these kernels. It does not lower or execute them, so this +# file tests frontend syntax coverage rather than runtime results. + + +@cudaq.kernel +def syntax_check_quantum_control() -> bool: + q = cudaq.qvector(1) + total = 0 + for i in range(4): + if i < 2: + total = total + 1 + x(q[0]) + if total > 1: + x(q[0]) + return mz(q[0]) + + +@cudaq.kernel +def syntax_check_real_state(values: list[float]) -> list[int]: + q = cudaq.qvector(values) + return mz(q) + + +@cudaq.kernel +def syntax_check_complex_state() -> list[int]: + q = cudaq.qvector([0. + 0j, 1. + 0j]) + return mz(q) + + +@cudaq.kernel +def syntax_check_integer_operators(value: int) -> int: + result = value * 2 + result = (result << 2) >> 1 + result = (result & 15) | 1 + result = result ^ 3 + result += 5 + result -= 2 + return result + + +@cudaq.kernel +def syntax_check_float_operators(a: float, b: float) -> float: + result = a * b + result += math.pi + return result / 2 + + +@cudaq.kernel +def syntax_check_float_vector(values: list[float], index: int) -> float: + return values[index] + + +@cudaq.kernel +def syntax_check_float_literal(index: int) -> float: + return [1., 2.][index] + + +@cudaq.kernel +def syntax_check_bits_to_integer(values: list[bool]) -> int: + result = 0 + for index, value in enumerate(values): + result = result | (value << index) + return result + + +@cudaq.kernel +def syntax_check_integer_sequence(values: list[int]) -> list[int]: + for index, value in enumerate(values): + values[index] = value * value + return values.copy() + + +@cudaq.kernel +def syntax_check_bool_sequence(values: list[bool]) -> list[bool]: + return values.copy() + + +@cudaq.kernel +def syntax_check_float_sequence(values: list[float]) -> list[float]: + return values.copy() + + +@cudaq.kernel +def syntax_check_bool_to_int(value: bool) -> int: + return value + + +@cudaq.kernel +def syntax_check_int_to_bool(value: int) -> bool: + return value + + +@cudaq.kernel +def syntax_check_bool_to_float(value: bool) -> float: + return value + + +@cudaq.kernel +def syntax_check_float_to_bool(value: float) -> bool: + return value + + +@cudaq.kernel +def syntax_check_int_to_float(value: int) -> float: + return value + + +@cudaq.kernel +def syntax_check_float_to_int(value: float) -> int: + return value + + +@cudaq.kernel +def syntax_check_for_search(target: int) -> int: + found = -1 + for i in range(6): + if i == target: + found = i + return found + + +@cudaq.kernel +def syntax_check_while_return(target: int) -> int: + i = 0 + while i < 6: + if i == target: + return i + i += 1 + return -1 + + +@cudaq.kernel +def syntax_check_while_comparisons(value: int) -> int: + while value >= 10: + value -= 20 + while value <= -10: + value += 20 + return value + + +def check_translation(kernel, *args): + try: + cudaq.run(kernel, *args, shots_count=1) + except RuntimeError as error: + if str(error) == "Invalid size value": + return + raise + + +def syntax_check(): + check_translation(syntax_check_quantum_control) + check_translation(syntax_check_real_state, [0., 1.]) + check_translation(syntax_check_complex_state) + + check_translation(syntax_check_integer_operators, 3) + check_translation(syntax_check_float_operators, 2., 3.) + check_translation(syntax_check_float_vector, [3.073, 1.719], 1) + check_translation(syntax_check_float_literal, 0) + + check_translation(syntax_check_bits_to_integer, [True, False, True]) + check_translation(syntax_check_integer_sequence, [1, 2, 3]) + check_translation(syntax_check_bool_sequence, [True, False]) + check_translation(syntax_check_float_sequence, [2.547, 1.32]) + + check_translation(syntax_check_bool_to_int, True) + check_translation(syntax_check_int_to_bool, -1) + check_translation(syntax_check_bool_to_float, True) + check_translation(syntax_check_float_to_bool, 1.2) + check_translation(syntax_check_int_to_float, -2) + check_translation(syntax_check_float_to_int, -1.2) + + check_translation(syntax_check_for_search, 4) + check_translation(syntax_check_while_return, 4) + check_translation(syntax_check_while_comparisons, 25) + + +try: + syntax_check() +except Exception as error: + print(error) + sys.exit(1) diff --git a/unittests/nvqpp/backends/quake_backend/test_unwind_app.cpp b/unittests/nvqpp/backends/quake_backend/test_unwind_app.cpp new file mode 100644 index 00000000000..57f131e8716 --- /dev/null +++ b/unittests/nvqpp/backends/quake_backend/test_unwind_app.cpp @@ -0,0 +1,75 @@ +/******************************************************************************* + * Copyright (c) 2026 NVIDIA Corporation & Affiliates. * + * All rights reserved. * + * * + * This source code and the accompanying materials are made available under * + * the terms of the Apache License 2.0 which accompanies this distribution. * + ******************************************************************************/ + +#include +#include + +__qpu__ int foo(bool returnEarly) { + int i = 0; + + for (; i < 4; ++i) { + if (returnEarly) + return 1; + } + + return i; +} + +__qpu__ int branchReturns(bool firstBranch) { + if (firstBranch) + return 7; + else + return 9; +} + +__qpu__ int loopControl(int skip, int stop) { + int i = 0; + int total = 0; + + while (i < 6) { + if (i == stop) + break; + if (i == skip) { + ++i; + continue; + } + total += i; + ++i; + } + + return total; +} + +bool hasExpectedResult(const std::vector &results, int expected) { + return results.size() == 1 && results.front() == expected; +} + +int main() { + const auto earlyResults = cudaq::run(1, foo, true); + if (!hasExpectedResult(earlyResults, 1)) + return 1; + + const auto loopResults = cudaq::run(1, foo, false); + if (!hasExpectedResult(loopResults, 4)) + return 2; + + const auto firstBranchResults = cudaq::run(1, branchReturns, true); + if (!hasExpectedResult(firstBranchResults, 7)) + return 3; + + const auto secondBranchResults = cudaq::run(1, branchReturns, false); + if (!hasExpectedResult(secondBranchResults, 9)) + return 4; + + const auto loopControlResults = cudaq::run(1, loopControl, 2, 5); + if (!hasExpectedResult(loopControlResults, 8)) + return 5; + + const auto completeLoopResults = cudaq::run(1, loopControl, 9, 9); + return hasExpectedResult(completeLoopResults, 15) ? 0 : 6; +} diff --git a/unittests/nvqpp/backends/quake_backend/test_unwind_app.py b/unittests/nvqpp/backends/quake_backend/test_unwind_app.py new file mode 100644 index 00000000000..02f374ad139 --- /dev/null +++ b/unittests/nvqpp/backends/quake_backend/test_unwind_app.py @@ -0,0 +1,68 @@ +# ============================================================================ # +# Copyright (c) 2026 NVIDIA Corporation & Affiliates. # +# All rights reserved. # +# # +# This source code and the accompanying materials are made available under # +# the terms of the Apache License 2.0 which accompanies this distribution. # +# ============================================================================ # + +import cudaq +import sys + +cudaq.set_target("quake_fake") + + +@cudaq.kernel +def early_return(return_early: bool) -> int: + i = 0 + while i < 4: + if return_early: + return 1 + i += 1 + return i + + +@cudaq.kernel +def branch_returns(first_branch: bool) -> int: + if first_branch: + return 7 + else: + return 9 + + +@cudaq.kernel +def loop_control(skip: int, stop: int) -> int: + i = 0 + total = 0 + while i < 6: + if i == stop: + break + if i == skip: + i += 1 + continue + total += i + i += 1 + return total + + +def check_result(kernel, expected, *args): + results = cudaq.run(kernel, *args, shots_count=1) + assert len(results) == 1 + assert results[0] == expected, f"expected {expected}, got {results[0]}" + + +def main(): + check_result(early_return, 1, True) + check_result(early_return, 4, False) + check_result(branch_returns, 7, True) + check_result(branch_returns, 9, False) + check_result(loop_control, 8, 2, 5) + check_result(loop_control, 15, 9, 9) + + +if __name__ == "__main__": + try: + main() + except Exception as error: + print(error) + sys.exit(1) diff --git a/unittests/target_config/TargetConfigTester.cpp b/unittests/target_config/TargetConfigTester.cpp index 8ef9621e9f9..d009e8852c4 100644 --- a/unittests/target_config/TargetConfigTester.cpp +++ b/unittests/target_config/TargetConfigTester.cpp @@ -6,6 +6,7 @@ * the terms of the Apache License 2.0 which accompanies this distribution. * ******************************************************************************/ +#include "cudaq/Target/CompileTarget.h" #include "cudaq/Target/TargetConfigYaml.h" #ifdef CUDAQ_ENABLE_PYTHON #include "LinkedLibraryHolder.h" @@ -72,6 +73,32 @@ cudaq-version: "0.9.0-rc2+build.1" EXPECT_EQ(config.CudaqVersion, "0.9.0-rc2+build.1"); } +TEST(TargetConfigTester, configuresAOTUnwindMode) { + const auto config = cudaq::config::parseTargetConfig(R"( +name: unwind-mode-test +description: AOT unwind mode test +config: + unwind-mode: dataflow +)"); + ASSERT_TRUE(config.BackendConfig.has_value()); + EXPECT_EQ(config.BackendConfig->AOTUnwindMode, "dataflow"); + const auto compileTarget = cudaq::CompileTarget::createFromConfig(config, {}); + EXPECT_EQ(compileTarget.pipelineConfig.aotUnwindMode, "dataflow"); + EXPECT_NE(cudaq::config::processRuntimeArgs(config, {}) + .find("AOT_UNWIND_MODE=\"dataflow\""), + std::string::npos); +} + +TEST(TargetConfigTester, rejectsInvalidAOTUnwindMode) { + EXPECT_THROW((void)cudaq::config::parseTargetConfig(R"( +name: unwind-mode-test +description: Invalid AOT unwind mode test +config: + unwind-mode: invalid +)"), + std::runtime_error); +} + TEST(TargetConfigTester, missingTargetConfigThrows) { const auto missingPath = std::filesystem::temp_directory_path() / "cudaq-missing-target-config.yml";