Skip to content

Commit 24e27bd

Browse files
[runtime] Option C alias + library-mode unittests + CI fixes
Introduce `using measure_result = measure_handle;` in MLIR mode (library mode keeps the legacy class) so existing source names get the new deferred-measurement semantics without generating host wrappers for device-only handle signatures. This is the Option C direction confirmed in the 2026-04-30 runtime sync; rationale and the rejected alternatives live in `.cursor/measure-handle-rename-evaluation.md`. Library-mode CMake routing for `unittests/` (`add_compile_definitions (CUDAQ_LIBRARY_MODE)`) keeps host-side overloads in `qpe_ftqc.cpp` and other measurement-using GTests that cannot be intercepted by the bridge. Temporary workaround until library mode itself is removed. CI green-up across the bridge, marshalling, ODS verifiers, and the expand-measurements / QIR conversion test suites for the new types. Squashed from four `*`-prefixed WIP commits per PR NVIDIA#4409 history cleanup; original SHAs: d84f6d8, 8fbe92d, 0e05f4c, ec5eac5. Signed-off-by: Pradnya Khalate <pkhalate@nvidia.com>
1 parent f3f8a61 commit 24e27bd

23 files changed

Lines changed: 277 additions & 214 deletions

docs/sphinx/examples/cpp/sample_to_run_migration.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ __qpu__ void reset_pattern() {
3131

3232
// [Begin Example1]
3333
struct simple_conditional {
34-
auto operator()() __qpu__ {
34+
bool operator()() __qpu__ {
3535
cudaq::qvector q(2);
3636
h(q[0]);
3737
auto r = mz(q[0]);

lib/Frontend/nvqpp/ASTBridge.cpp

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -73,18 +73,6 @@ static bool hasAnyQubitTypes(FunctionType funcTy) {
7373
return false;
7474
}
7575

76-
// Returns true if any input or result type of `funcTy` transitively contains
77-
// `!cc.measure_handle`.
78-
static bool hasMeasureHandleInSignature(FunctionType funcTy) {
79-
for (auto ty : funcTy.getInputs())
80-
if (cudaq::cc::containsMeasureHandle(ty))
81-
return true;
82-
for (auto ty : funcTy.getResults())
83-
if (cudaq::cc::containsMeasureHandle(ty))
84-
return true;
85-
return false;
86-
}
87-
8876
// Remove the Itanium mangling "_ZTS" prefix. This is to match the name returned
8977
// by `typeid(TYPE).name()`.
9078
static std::string
@@ -654,14 +642,27 @@ void ASTBridgeAction::ASTBridgeConsumer::HandleTranslationUnit(
654642
if ((!hasAnyQubitTypes(func.getFunctionType())) &&
655643
(!cudaq::ASTBridgeAction::ASTBridgeConsumer::isCustomOpGenerator(
656644
fdPair.second))) {
645+
auto hasMH = [](FunctionType ft) {
646+
for (auto ty : ft.getInputs())
647+
if (cudaq::cc::containsMeasureHandle(ty))
648+
return true;
649+
for (auto ty : ft.getResults())
650+
if (cudaq::cc::containsMeasureHandle(ty))
651+
return true;
652+
return false;
653+
};
654+
if (hasMH(func.getFunctionType())) {
655+
if (auto *md = dyn_cast<clang::CXXMethodDecl>(fdPair.second);
656+
md && md->getOverloadedOperator() == clang::OO_Call) {
657+
cudaq::details::reportClangError(
658+
fdPair.second, mangler,
659+
"measure_handle cannot cross the host-device boundary; "
660+
"entry-point kernels must discriminate first");
661+
}
662+
continue;
663+
}
657664
// Flag func as an entry point to a quantum kernel.
658665
func->setAttr(entryPointAttrName, unitAttr);
659-
if (hasMeasureHandleInSignature(func.getFunctionType())) {
660-
cudaq::details::reportClangError(
661-
fdPair.second, mangler,
662-
"measure_handle cannot cross the host-device boundary; "
663-
"entry-point kernels must discriminate first");
664-
}
665666
// Generate a declaration for the CPU C++ function.
666667
addFunctionDecl(fdPair.second, visitor, func.getFunctionType(),
667668
entryName, func.empty());

lib/Frontend/nvqpp/ConvertDecl.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,9 @@ bool QuakeBridgeVisitor::interceptRecordDecl(clang::RecordDecl *x) {
161161
return pushType(quake::StateType::get(ctx));
162162
if (name == "pauli_word")
163163
return pushType(cc::CharspanType::get(ctx));
164-
if (name == "measure_handle")
164+
// NB: Accommodating the spec request, to keep the change minimally
165+
// invasive to the user make these two equivalent.
166+
if (name == "measure_handle" || name == "measure_result")
165167
return pushType(cc::MeasureHandleType::get(ctx));
166168
if (name == "qkernel") {
167169
auto *cts = cast<clang::ClassTemplateSpecializationDecl>(x);

lib/Optimizer/Builder/Marshal.cpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -761,10 +761,10 @@ void cudaq::opt::marshal::populateCallbackBuffer(
761761

762762
bool cudaq::opt::marshal::hasLegalType(FunctionType funTy) {
763763
for (auto ty : funTy.getInputs())
764-
if (quake::isQuantumType(ty))
764+
if (quake::isQuantumType(ty) || cc::containsMeasureHandle(ty))
765765
return false;
766766
for (auto ty : funTy.getResults())
767-
if (quake::isQuantumType(ty))
767+
if (quake::isQuantumType(ty) || cc::containsMeasureHandle(ty))
768768
return false;
769769
return true;
770770
}
@@ -790,6 +790,11 @@ std::pair<bool, func::FuncOp> cudaq::opt::marshal::lookupHostEntryPointFunc(
790790
// No host entry point needed.
791791
return {false, func::FuncOp{}};
792792
}
793+
// Device-only kernels with signatures that cannot cross the host boundary
794+
// have no host-side entry point to rewrite.
795+
if (!funcOp->hasAttr(cudaq::entryPointAttrName) &&
796+
!cudaq::opt::marshal::hasLegalType(funcOp.getFunctionType()))
797+
return {false, func::FuncOp{}};
793798
if (auto *decl = module.lookupSymbol(mangledEntryPointName))
794799
if (auto func = dyn_cast<func::FuncOp>(decl)) {
795800
func.eraseBody();

lib/Optimizer/Dialect/CC/CCOps.cpp

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -460,25 +460,35 @@ LogicalResult cudaq::cc::CastOp::verify() {
460460
}
461461

462462
namespace {
463-
// There are a number of series of casts that can be fused. For now, fuse
464-
// pointer cast chains.
463+
// Fold cast cascades whose endpoints are both pointer types. Two shapes:
464+
// - ptr -> ptr -> ptr (eliminate the intermediate pointer view)
465+
// - ptr -> int -> ptr (eliminate the integer round-trip)
466+
// `cc.cast` is a value-preserving type-only view, so collapsing either shape
467+
// to a single `ptr -> ptr` cast is always safe. The integer-round-trip case
468+
// also matters for the QIR profile pipelines: the discriminate / measurement
469+
// patterns build `Result* -> i64 -> ... -> ptr<i1>` chains, and the trailing
470+
// `Result* -> i64` lowers to `llvm.ptrtoint`, which the NVQIR profile
471+
// verifier rejects (only `bitcast`, `inttoptr`, etc. are allow-listed for
472+
// pointer-typed operands). After this fold + DCE the chain collapses to a
473+
// single `ptr -> ptr` cast that lowers to `llvm.bitcast`.
465474
struct FuseCastCascade : public OpRewritePattern<cudaq::cc::CastOp> {
466475
using OpRewritePattern::OpRewritePattern;
467476

468477
LogicalResult matchAndRewrite(cudaq::cc::CastOp castOp,
469478
PatternRewriter &rewriter) const override {
470-
if (auto castToCast = castOp.getValue().getDefiningOp<cudaq::cc::CastOp>())
471-
if (isa<cudaq::cc::PointerType>(castOp.getType()) &&
472-
isa<cudaq::cc::PointerType>(castToCast.getType())) {
473-
// %4 = cc.cast %3 : (!cc.ptr<T>) -> !cc.ptr<U>
474-
// %5 = cc.cast %4 : (!cc.ptr<U>) -> !cc.ptr<V>
475-
// ────────────────────────────────────────────
476-
// %5 = cc.cast %3 : (!cc.ptr<T>) -> !cc.ptr<V>
477-
rewriter.replaceOpWithNewOp<cudaq::cc::CastOp>(castOp, castOp.getType(),
478-
castToCast.getValue());
479-
return success();
480-
}
481-
return failure();
479+
auto castToCast = castOp.getValue().getDefiningOp<cudaq::cc::CastOp>();
480+
if (!castToCast)
481+
return failure();
482+
if (!isa<cudaq::cc::PointerType>(castOp.getType()))
483+
return failure();
484+
if (!isa<cudaq::cc::PointerType>(castToCast.getValue().getType()))
485+
return failure();
486+
auto middleTy = castToCast.getType();
487+
if (!isa<cudaq::cc::PointerType, IntegerType>(middleTy))
488+
return failure();
489+
rewriter.replaceOpWithNewOp<cudaq::cc::CastOp>(castOp, castOp.getType(),
490+
castToCast.getValue());
491+
return success();
482492
}
483493
};
484494

runtime/cudaq/qis/execution_manager.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include "cudaq/algorithms/policies.h"
1616
#include "cudaq/host_config.h"
1717
#include "cudaq/operators.h"
18+
#include "cudaq/qis/measure_handle.h"
1819
#include <deque>
1920
#include <string_view>
2021
#include <vector>
@@ -70,8 +71,9 @@ class measure_result {
7071
}
7172
};
7273
#else
73-
/// When compiling with MLIR, we default to a boolean.
74-
using measure_result = bool;
74+
// In MLIR mode, keep the existing `measure_result` API name as a compatibility
75+
// alias for `measure_handle`.
76+
using measure_result = measure_handle;
7577
#endif
7678

7779
/// The ExecutionManager provides a base class describing a concrete sub-system

runtime/cudaq/qis/measure_handle.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,17 @@
1515
#include <cstdint>
1616
#include <limits>
1717

18-
namespace cudaq {
19-
20-
namespace details {
18+
namespace cudaq::details {
2119
/// Tag type used to dispatch the index-taking `measure_handle` constructor,
2220
/// so `measure_handle{42}` cannot be compiled in user code. The tag surface is
2321
/// reserved for internal runtime use. Inside `__qpu__` regions the
2422
/// bridge never calls this constructor because `mz`/`mx`/`my` produce
2523
/// `!cc.measure_handle` SSA values directly.
2624
struct handle_index_t {};
2725
inline constexpr handle_index_t handle_index{};
28-
} // namespace details
26+
} // namespace cudaq::details
27+
28+
namespace cudaq {
2929

3030
/// @brief Handle for a measurement event with deferred discrimination.
3131
///

0 commit comments

Comments
 (0)