diff --git a/include/cudaq/Optimizer/CodeGen/Passes.td b/include/cudaq/Optimizer/CodeGen/Passes.td index d65f7f62b29..58b4ea3151a 100644 --- a/include/cudaq/Optimizer/CodeGen/Passes.td +++ b/include/cudaq/Optimizer/CodeGen/Passes.td @@ -131,6 +131,16 @@ def QIRToQIRProfile : Pass<"convert-to-qir-profile"> { let constructor = "cudaq::opt::createQIRToQIRProfilePass(\"qir-base\")"; } +def QirInsertArrayRecord : Pass<"qir-insert-array-record", "mlir::ModuleOp"> { + let summary = "Analyze instruction patterns and insert new instructions for QIR"; + let description = [{ + This pass performs analysis on the instruction patterns between QIR prep and + conversion passes, then inserts array recording instruction based on the analysis. + }]; + let dependentDialects = ["quake::QuakeDialect", "cudaq::cc::CCDialect", + "mlir::LLVM::LLVMDialect"]; +} + def RemoveMeasurements : Pass<"remove-measurements"> { let summary = "Remove measurements and output recording calls from a QIR program"; diff --git a/lib/Optimizer/CodeGen/CMakeLists.txt b/lib/Optimizer/CodeGen/CMakeLists.txt index 17841da4ca1..b6d52d85d27 100644 --- a/lib/Optimizer/CodeGen/CMakeLists.txt +++ b/lib/Optimizer/CodeGen/CMakeLists.txt @@ -23,6 +23,7 @@ add_cudaq_library(OptCodeGen OptUtils.cpp Passes.cpp Pipelines.cpp + QirInsertArrayRecord.cpp QuakeToCodegen.cpp QuakeToExecMgr.cpp QuakeToLLVM.cpp diff --git a/lib/Optimizer/CodeGen/ConvertToQIRAPI.cpp b/lib/Optimizer/CodeGen/ConvertToQIRAPI.cpp index 7ddb4aae51e..455f7503b4d 100644 --- a/lib/Optimizer/CodeGen/ConvertToQIRAPI.cpp +++ b/lib/Optimizer/CodeGen/ConvertToQIRAPI.cpp @@ -2326,6 +2326,7 @@ void cudaq::opt::addConvertToQIRAPIPipeline(OpPassManager &pm, StringRef api, QuakeToQIRAPIPrepOptions prepApiOpt{.api = api.str(), .opaquePtr = opaquePtr}; pm.addPass(cudaq::opt::createQuakeToQIRAPIPrep(prepApiOpt)); pm.addPass(cudaq::opt::createLowerToCG()); + pm.addPass(cudaq::opt::createQirInsertArrayRecord()); QuakeToQIRAPIOptions apiOpt{.api = api.str(), .opaquePtr = opaquePtr}; pm.addPass(cudaq::opt::createQuakeToQIRAPI(apiOpt)); pm.addPass(createCanonicalizerPass()); diff --git a/lib/Optimizer/CodeGen/QirInsertArrayRecord.cpp b/lib/Optimizer/CodeGen/QirInsertArrayRecord.cpp new file mode 100644 index 00000000000..26716b907b7 --- /dev/null +++ b/lib/Optimizer/CodeGen/QirInsertArrayRecord.cpp @@ -0,0 +1,213 @@ +/******************************************************************************* + * Copyright (c) 2025 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 "PassDetails.h" +#include "cudaq/Optimizer/Builder/Intrinsics.h" +#include "cudaq/Optimizer/Builder/Runtime.h" +#include "cudaq/Optimizer/CodeGen/Passes.h" +#include "cudaq/Optimizer/CodeGen/QIRAttributeNames.h" +#include "cudaq/Optimizer/CodeGen/QIRFunctionNames.h" +#include "cudaq/Optimizer/Dialect/Quake/QuakeOps.h" +#include "llvm/ADT/SmallSet.h" +#include "mlir/Transforms/GreedyPatternRewriteDriver.h" +#include "mlir/Transforms/Passes.h" + +namespace cudaq::opt { +#define GEN_PASS_DEF_QIRINSERTARRAYRECORD +#include "cudaq/Optimizer/CodeGen/Passes.h.inc" +} // namespace cudaq::opt + +#define DEBUG_TYPE "qir-insert-array-record" + +using namespace mlir; + +namespace { + +// Trace a pointer to back to its corresponding `AllocaOp` +static cudaq::cc::AllocaOp tracePointerToAlloca(Value ptr) { + llvm::DenseSet visited; + while (ptr) { + if (!visited.insert(ptr).second) + return {}; + Operation *defOp = ptr.getDefiningOp(); + if (!defOp) + return {}; + if (auto allocaOp = dyn_cast(defOp)) + return allocaOp; + if (auto castOp = dyn_cast(defOp)) { + ptr = castOp.getValue(); + continue; + } + if (auto computePtrOp = dyn_cast(defOp)) { + ptr = computePtrOp.getBase(); + continue; + } + return {}; + } + return {}; +} + +// Walk a function to identify all the measure-discriminate-store patterns and +// collect the associated `AllocaOp` when the measurement results are stored. +// Collect only unique AllocaOps - since each may correspond to multiple +// measurement operations. When there are no explicit stores, track the first +// measurement operation and the get the total number of measurements. +struct AllocaMeasureStoreAnalysis { + AllocaMeasureStoreAnalysis() = default; + + explicit AllocaMeasureStoreAnalysis(func::FuncOp funcOp) { + size_t totalMeasurementCount = 0; + Operation *firstMeasureOp = nullptr; + DenseMap valueToMeasurement; + llvm::SetVector uniqueAllocaOps; + + // First pass: identify measurements and propagate through uses + funcOp.walk([&](Operation *op) { + if (op->hasTrait()) { + if (op->hasAttr(cudaq::opt::ResultIndexAttrName)) { + totalMeasurementCount++; + if (!firstMeasureOp) + firstMeasureOp = op; + } + for (auto result : op->getResults()) + valueToMeasurement[result] = op; + return WalkResult::advance(); + } + + // TODO: Check if more operations need to be added here. + if (!isa(op)) { + return WalkResult::advance(); + } + + // Find the operands derived from measurements + for (auto operand : op->getOperands()) { + if (valueToMeasurement.count(operand)) { + for (auto result : op->getResults()) + valueToMeasurement[result] = valueToMeasurement[operand]; + } + break; // Checking one operand is enough + } + return WalkResult::advance(); + }); + + // Second pass: find stores of measurement values and trace to `alloca` ops + funcOp.walk([&](cudaq::cc::StoreOp storeOp) { + if (valueToMeasurement.count(storeOp.getValue())) { + Value ptr = storeOp.getPtrvalue(); + auto allocaOp = tracePointerToAlloca(ptr); + if (allocaOp) + uniqueAllocaOps.insert(allocaOp); + } + }); + + if (!uniqueAllocaOps.empty()) { + // Use array sizes when explicit storage exists + for (auto allocaOp : uniqueAllocaOps) { + if (auto arrType = + allocaOp.getElementType().dyn_cast()) { + arraySize += arrType.getSize(); + } else { + arraySize += 1; + } + } + allocaOps.append(uniqueAllocaOps.begin(), uniqueAllocaOps.end()); + } else if (totalMeasurementCount > 0) { + // This could be individual qubit(s) + arraySize = totalMeasurementCount; + firstMeasurementOp = firstMeasureOp; + } + } + + SmallVector allocaOps; + size_t arraySize = 0; + Operation *firstMeasurementOp = nullptr; +}; + +// Inserts a QIR array record output call to declare measurement result storage. +// QIR requires `__quantum__rt__array_record_output()` be called before multiple +// measurements to declare the output array size and type label. This is +// required in `sample` API since it always returns a vector of measurement +// results. Following logic is used to determine the insertion point: +// 1. After first alloca (if explicit array storage exists) +// 2. Before first measurement (if no explicit storage) +// The label string is created as "array" where N is the total number of +// measurement results. The array record output call is created as: +// `__quantum__rt__array_record_output(N, label);` +LogicalResult +insertArrayRecordingCalls(func::FuncOp funcOp, size_t resultCount, + const SmallVector &allocaOps, + Operation *firstMeasureOp) { + if (resultCount == 0) + return success(); + + auto ctx = funcOp.getContext(); + OpBuilder builder(ctx); + mlir::Location loc = funcOp.getLoc(); + // We insert only one array record call + if (!allocaOps.empty()) + builder.setInsertionPointAfter(allocaOps[0]); + else if (firstMeasureOp) + builder.setInsertionPoint(firstMeasureOp); + else + return failure(); + + // Create the label string: "array" + std::string labelStr = "array"; + auto strLitTy = cudaq::cc::PointerType::get(cudaq::cc::ArrayType::get( + builder.getContext(), builder.getI8Type(), labelStr.size() + 1)); + Value lit = builder.create( + loc, strLitTy, builder.getStringAttr(labelStr)); + auto i8PtrTy = cudaq::cc::PointerType::get(builder.getI8Type()); + Value label = builder.create(loc, i8PtrTy, lit); + Value size = builder.create(loc, resultCount, 64); + builder.create(loc, TypeRange{}, + cudaq::opt::QIRArrayRecordOutput, + ArrayRef{size, label}); + + // Add the declaration to the module if it doesn't already exist + auto module = funcOp->getParentOfType(); + if (!module.lookupSymbol(cudaq::opt::QIRArrayRecordOutput)) { + auto irBuilder = cudaq::IRBuilder::atBlockEnd(module.getBody()); + if (failed(irBuilder.loadIntrinsic(module, + cudaq::opt::QIRArrayRecordOutput))) { + return failure(); + } + } + return success(); +} + +struct QirInsertArrayRecordPass + : public cudaq::opt::impl::QirInsertArrayRecordBase< + QirInsertArrayRecordPass> { + + using QirInsertArrayRecordBase::QirInsertArrayRecordBase; + + void runOnOperation() override { + ModuleOp module = getOperation(); + for (auto funcOp : module.getOps()) { + if (!funcOp || funcOp.empty() || + !funcOp->hasAttr(cudaq::entryPointAttrName) || + funcOp->hasAttr(cudaq::runtime::enableCudaqRun)) + continue; + + AllocaMeasureStoreAnalysis analysis(funcOp); + if (analysis.arraySize == 0) + continue; + + LLVM_DEBUG(llvm::dbgs() << "Before adding array recording call:\n" + << *funcOp); + if (failed(insertArrayRecordingCalls(funcOp, analysis.arraySize, + analysis.allocaOps, + analysis.firstMeasurementOp))) + return signalPassFailure(); + LLVM_DEBUG(llvm::dbgs() << "After adding array recording call:\n" + << *funcOp); + } + } +}; +} // namespace diff --git a/runtime/common/RecordLogParser.cpp b/runtime/common/RecordLogParser.cpp index 31f417dc788..6f6d68f2823 100644 --- a/runtime/common/RecordLogParser.cpp +++ b/runtime/common/RecordLogParser.cpp @@ -119,9 +119,14 @@ void cudaq::RecordLogParser::handleOutput( (containerMeta.m_type == ContainerType::ARRAY && containerMeta.elementCount == 0); if (isUninitializedContainer) { - // Currently, our QIR for sampled kernel only has a sequence of RESULT - // records, not wrapped in an ARRAY. Hence, we treat it as an array of - // results. + // NOTE: This is a temporary workaround until all backends consistently + // use the new transformation pass that wraps result records inside an + // array record output. For now, we permit "naked" RESULT records, i.e., + // if the QIR produced by a sampled kernel emits a sequence of RESULT + // records without enclosing them in an ARRAY, we interpret them + // collectively as an array of results. + // NOTE: This assumption prevents us from correctly supporting `run` with + // `qir-base` profile. containerMeta.m_type = ContainerType::ARRAY; containerMeta.elementCount = std::stoul(metadata[ResultCountMetadataName]); diff --git a/test/Translate/array_record_insert.qke b/test/Translate/array_record_insert.qke new file mode 100644 index 00000000000..5d36691c06a --- /dev/null +++ b/test/Translate/array_record_insert.qke @@ -0,0 +1,248 @@ +// ========================================================================== // +// Copyright (c) 2025 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. // +// ========================================================================== // + +// RUN: cudaq-opt --qir-insert-array-record %s | FileCheck %s + +func.func @__nvqpp__mlirgen__function_bell._Z4bellv() attributes {"cudaq-entrypoint", "cudaq-kernel"} { + %0 = quake.alloca !quake.veq<2> {StartingOffset = 0 : i64} + %1 = quake.extract_ref %0[0] : (!quake.veq<2>) -> !quake.ref + quake.h %1 : (!quake.ref) -> () + %2 = quake.extract_ref %0[1] : (!quake.veq<2>) -> !quake.ref + quake.x [%1] %2 : (!quake.ref, !quake.ref) -> () + %3 = cc.alloca !cc.array + %4 = cc.cast %3 : (!cc.ptr>) -> !cc.ptr + %5 = cc.compute_ptr %3[1] : (!cc.ptr>) -> !cc.ptr + %measOut = quake.mz %1 name "r00000" : (!quake.ref) -> !quake.measure {ResultIndex = 0 : i64} + %6 = quake.discriminate %measOut : (!quake.measure) -> i1 + %7 = cc.cast unsigned %6 : (i1) -> i8 + cc.store %7, %4 : !cc.ptr + %measOut_0 = quake.mz %2 name "r00001" : (!quake.ref) -> !quake.measure {ResultIndex = 1 : i64} + %8 = quake.discriminate %measOut_0 : (!quake.measure) -> i1 + %9 = cc.cast unsigned %8 : (i1) -> i8 + cc.store %9, %5 : !cc.ptr + quake.dealloc %0 : !quake.veq<2> + return +} + +func.func @__nvqpp__mlirgen__one_qubit() attributes {"cudaq-entrypoint", "cudaq-kernel"} { + %0 = quake.alloca !quake.veq<1> {StartingOffset = 0 : i64} + %1 = quake.extract_ref %0[0] : (!quake.veq<1>) -> !quake.ref + quake.h %1 : (!quake.ref) -> () + %measOut = quake.mz %1 name "r00000" : (!quake.ref) -> !quake.measure {ResultIndex = 0 : i64} + quake.dealloc %0 : !quake.veq<1> + return +} + +func.func @__nvqpp__mlirgen__three_qubits() attributes {"cudaq-entrypoint", "cudaq-kernel"} { + %0 = quake.alloca !quake.veq<3> {StartingOffset = 0 : i64} + %1 = quake.extract_ref %0[0] : (!quake.veq<3>) -> !quake.ref + %2 = quake.extract_ref %0[1] : (!quake.veq<3>) -> !quake.ref + %3 = quake.extract_ref %0[2] : (!quake.veq<3>) -> !quake.ref + quake.h %1 : (!quake.ref) -> () + quake.x [%1] %2 : (!quake.ref, !quake.ref) -> () + quake.x [%2] %3 : (!quake.ref, !quake.ref) -> () + %measOut = quake.mz %1 name "r00000" : (!quake.ref) -> !quake.measure {ResultIndex = 0 : i64} + %measOut_0 = quake.mz %2 name "r00001" : (!quake.ref) -> !quake.measure {ResultIndex = 1 : i64} + %measOut_1 = quake.mz %3 name "r00002" : (!quake.ref) -> !quake.measure {ResultIndex = 2 : i64} + quake.dealloc %0 : !quake.veq<3> + return +} + +func.func @__nvqpp__mlirgen__one_vector() attributes {"cudaq-entrypoint", "cudaq-kernel"} { + %0 = quake.alloca !quake.veq<2> + %1 = quake.extract_ref %0[0] : (!quake.veq<2>) -> !quake.ref + quake.h %1 : (!quake.ref) -> () + %2 = quake.extract_ref %0[1] : (!quake.veq<2>) -> !quake.ref + quake.h %2 : (!quake.ref) -> () + quake.h %2 : (!quake.ref) -> () + quake.x [%1] %2 : (!quake.ref, !quake.ref) -> () + quake.h %2 : (!quake.ref) -> () + %3 = cc.alloca !cc.array + %measOut = quake.mz %1 name "r00000" : (!quake.ref) -> !quake.measure + %4 = quake.discriminate %measOut : (!quake.measure) -> i1 + %5 = cc.cast %3 : (!cc.ptr>) -> !cc.ptr + %6 = cc.cast unsigned %4 : (i1) -> i8 + cc.store %6, %5 : !cc.ptr + %measOut_0 = quake.mz %2 name "r00001" : (!quake.ref) -> !quake.measure + %7 = quake.discriminate %measOut_0 : (!quake.measure) -> i1 + %8 = cc.compute_ptr %3[1] : (!cc.ptr>) -> !cc.ptr + %9 = cc.cast unsigned %7 : (i1) -> i8 + cc.store %9, %8 : !cc.ptr + quake.dealloc %0 : !quake.veq<2> + return +} + +func.func @__nvqpp__mlirgen__multi_vector() attributes {"cudaq-entrypoint", "cudaq-kernel"} { + %0 = quake.alloca !quake.veq<4> + %1 = quake.extract_ref %0[0] : (!quake.veq<4>) -> !quake.ref + quake.h %1 : (!quake.ref) -> () + %2 = quake.extract_ref %0[1] : (!quake.veq<4>) -> !quake.ref + quake.x [%1] %2 : (!quake.ref, !quake.ref) -> () + %3 = quake.extract_ref %0[3] : (!quake.veq<4>) -> !quake.ref + quake.h %3 : (!quake.ref) -> () + %4 = quake.extract_ref %0[2] : (!quake.veq<4>) -> !quake.ref + quake.h %4 : (!quake.ref) -> () + quake.x [%3] %4 : (!quake.ref, !quake.ref) -> () + quake.h %4 : (!quake.ref) -> () + %5 = cc.alloca !cc.array + %measOut = quake.mz %1 name "r00000" : (!quake.ref) -> !quake.measure + %6 = quake.discriminate %measOut : (!quake.measure) -> i1 + %7 = cc.cast %5 : (!cc.ptr>) -> !cc.ptr + %8 = cc.cast unsigned %6 : (i1) -> i8 + cc.store %8, %7 : !cc.ptr + %measOut_0 = quake.mz %2 name "r00001" : (!quake.ref) -> !quake.measure + %9 = quake.discriminate %measOut_0 : (!quake.measure) -> i1 + %10 = cc.compute_ptr %5[1] : (!cc.ptr>) -> !cc.ptr + %11 = cc.cast unsigned %9 : (i1) -> i8 + cc.store %11, %10 : !cc.ptr + %12 = cc.alloca !cc.array + %measOut_1 = quake.mz %4 name "r00002" : (!quake.ref) -> !quake.measure + %13 = quake.discriminate %measOut_1 : (!quake.measure) -> i1 + %14 = cc.cast %12 : (!cc.ptr>) -> !cc.ptr + %15 = cc.cast unsigned %13 : (i1) -> i8 + cc.store %15, %14 : !cc.ptr + %measOut_2 = quake.mz %3 name "r00003" : (!quake.ref) -> !quake.measure + %16 = quake.discriminate %measOut_2 : (!quake.measure) -> i1 + %17 = cc.compute_ptr %12[1] : (!cc.ptr>) -> !cc.ptr + %18 = cc.cast unsigned %16 : (i1) -> i8 + cc.store %18, %17 : !cc.ptr + quake.dealloc %0 : !quake.veq<4> + return +} + +// CHECK-LABEL: func.func @__nvqpp__mlirgen__function_bell._Z4bellv() attributes {"cudaq-entrypoint", "cudaq-kernel"} { +// CHECK: %[[VAL_0:.*]] = quake.alloca !quake.veq<2> {StartingOffset = 0 : i64} +// CHECK: %[[VAL_1:.*]] = quake.extract_ref %[[VAL_0]][0] : (!quake.veq<2>) -> !quake.ref +// CHECK: quake.h %[[VAL_1]] : (!quake.ref) -> () +// CHECK: %[[VAL_2:.*]] = quake.extract_ref %[[VAL_0]][1] : (!quake.veq<2>) -> !quake.ref +// CHECK: quake.x {{\[}}%[[VAL_1]]] %[[VAL_2]] : (!quake.ref, !quake.ref) -> () +// CHECK: %[[VAL_3:.*]] = cc.alloca !cc.array +// CHECK: %[[VAL_4:.*]] = cc.string_literal "array" : !cc.ptr> +// CHECK: %[[VAL_5:.*]] = cc.cast %[[VAL_4]] : (!cc.ptr>) -> !cc.ptr +// CHECK: %[[VAL_6:.*]] = arith.constant 2 : i64 +// CHECK: call @__quantum__rt__array_record_output(%[[VAL_6]], %[[VAL_5]]) : (i64, !cc.ptr) -> () +// CHECK: %[[VAL_7:.*]] = cc.cast %[[VAL_3]] : (!cc.ptr>) -> !cc.ptr +// CHECK: %[[VAL_8:.*]] = cc.compute_ptr %[[VAL_3]][1] : (!cc.ptr>) -> !cc.ptr +// CHECK: %[[VAL_9:.*]] = quake.mz %[[VAL_1]] name "r00000" : (!quake.ref) -> !quake.measure {ResultIndex = 0 : i64} +// CHECK: %[[VAL_10:.*]] = quake.discriminate %[[VAL_9]] : (!quake.measure) -> i1 +// CHECK: %[[VAL_11:.*]] = cc.cast unsigned %[[VAL_10]] : (i1) -> i8 +// CHECK: cc.store %[[VAL_11]], %[[VAL_7]] : !cc.ptr +// CHECK: %[[VAL_12:.*]] = quake.mz %[[VAL_2]] name "r00001" : (!quake.ref) -> !quake.measure {ResultIndex = 1 : i64} +// CHECK: %[[VAL_13:.*]] = quake.discriminate %[[VAL_12]] : (!quake.measure) -> i1 +// CHECK: %[[VAL_14:.*]] = cc.cast unsigned %[[VAL_13]] : (i1) -> i8 +// CHECK: cc.store %[[VAL_14]], %[[VAL_8]] : !cc.ptr +// CHECK: quake.dealloc %[[VAL_0]] : !quake.veq<2> +// CHECK: return +// CHECK: } + +// CHECK-LABEL: func.func @__nvqpp__mlirgen__one_qubit() attributes {"cudaq-entrypoint", "cudaq-kernel"} { +// CHECK: %[[VAL_0:.*]] = quake.alloca !quake.veq<1> {StartingOffset = 0 : i64} +// CHECK: %[[VAL_1:.*]] = quake.extract_ref %[[VAL_0]][0] : (!quake.veq<1>) -> !quake.ref +// CHECK: quake.h %[[VAL_1]] : (!quake.ref) -> () +// CHECK: %[[VAL_2:.*]] = cc.string_literal "array" : !cc.ptr> +// CHECK: %[[VAL_3:.*]] = cc.cast %[[VAL_2]] : (!cc.ptr>) -> !cc.ptr +// CHECK: %[[VAL_4:.*]] = arith.constant 1 : i64 +// CHECK: call @__quantum__rt__array_record_output(%[[VAL_4]], %[[VAL_3]]) : (i64, !cc.ptr) -> () +// CHECK: %[[VAL_5:.*]] = quake.mz %[[VAL_1]] name "r00000" : (!quake.ref) -> !quake.measure {ResultIndex = 0 : i64} +// CHECK: quake.dealloc %[[VAL_0]] : !quake.veq<1> +// CHECK: return +// CHECK: } + +// CHECK-LABEL: func.func @__nvqpp__mlirgen__three_qubits() attributes {"cudaq-entrypoint", "cudaq-kernel"} { +// CHECK: %[[VAL_0:.*]] = quake.alloca !quake.veq<3> {StartingOffset = 0 : i64} +// CHECK: %[[VAL_1:.*]] = quake.extract_ref %[[VAL_0]][0] : (!quake.veq<3>) -> !quake.ref +// CHECK: %[[VAL_2:.*]] = quake.extract_ref %[[VAL_0]][1] : (!quake.veq<3>) -> !quake.ref +// CHECK: %[[VAL_3:.*]] = quake.extract_ref %[[VAL_0]][2] : (!quake.veq<3>) -> !quake.ref +// CHECK: quake.h %[[VAL_1]] : (!quake.ref) -> () +// CHECK: quake.x {{\[}}%[[VAL_1]]] %[[VAL_2]] : (!quake.ref, !quake.ref) -> () +// CHECK: quake.x {{\[}}%[[VAL_2]]] %[[VAL_3]] : (!quake.ref, !quake.ref) -> () +// CHECK: %[[VAL_4:.*]] = cc.string_literal "array" : !cc.ptr> +// CHECK: %[[VAL_5:.*]] = cc.cast %[[VAL_4]] : (!cc.ptr>) -> !cc.ptr +// CHECK: %[[VAL_6:.*]] = arith.constant 3 : i64 +// CHECK: call @__quantum__rt__array_record_output(%[[VAL_6]], %[[VAL_5]]) : (i64, !cc.ptr) -> () +// CHECK: %[[VAL_7:.*]] = quake.mz %[[VAL_1]] name "r00000" : (!quake.ref) -> !quake.measure {ResultIndex = 0 : i64} +// CHECK: %[[VAL_8:.*]] = quake.mz %[[VAL_2]] name "r00001" : (!quake.ref) -> !quake.measure {ResultIndex = 1 : i64} +// CHECK: %[[VAL_9:.*]] = quake.mz %[[VAL_3]] name "r00002" : (!quake.ref) -> !quake.measure {ResultIndex = 2 : i64} +// CHECK: quake.dealloc %[[VAL_0]] : !quake.veq<3> +// CHECK: return +// CHECK: } + +// CHECK-LABEL: func.func @__nvqpp__mlirgen__one_vector() attributes {"cudaq-entrypoint", "cudaq-kernel"} { +// CHECK: %[[VAL_0:.*]] = quake.alloca !quake.veq<2> +// CHECK: %[[VAL_1:.*]] = quake.extract_ref %[[VAL_0]][0] : (!quake.veq<2>) -> !quake.ref +// CHECK: quake.h %[[VAL_1]] : (!quake.ref) -> () +// CHECK: %[[VAL_2:.*]] = quake.extract_ref %[[VAL_0]][1] : (!quake.veq<2>) -> !quake.ref +// CHECK: quake.h %[[VAL_2]] : (!quake.ref) -> () +// CHECK: quake.h %[[VAL_2]] : (!quake.ref) -> () +// CHECK: quake.x {{\[}}%[[VAL_1]]] %[[VAL_2]] : (!quake.ref, !quake.ref) -> () +// CHECK: quake.h %[[VAL_2]] : (!quake.ref) -> () +// CHECK: %[[VAL_3:.*]] = cc.alloca !cc.array +// CHECK: %[[VAL_4:.*]] = cc.string_literal "array" : !cc.ptr> +// CHECK: %[[VAL_5:.*]] = cc.cast %[[VAL_4]] : (!cc.ptr>) -> !cc.ptr +// CHECK: %[[VAL_6:.*]] = arith.constant 2 : i64 +// CHECK: call @__quantum__rt__array_record_output(%[[VAL_6]], %[[VAL_5]]) : (i64, !cc.ptr) -> () +// CHECK: %[[VAL_7:.*]] = quake.mz %[[VAL_1]] name "r00000" : (!quake.ref) -> !quake.measure +// CHECK: %[[VAL_8:.*]] = quake.discriminate %[[VAL_7]] : (!quake.measure) -> i1 +// CHECK: %[[VAL_9:.*]] = cc.cast %[[VAL_3]] : (!cc.ptr>) -> !cc.ptr +// CHECK: %[[VAL_10:.*]] = cc.cast unsigned %[[VAL_8]] : (i1) -> i8 +// CHECK: cc.store %[[VAL_10]], %[[VAL_9]] : !cc.ptr +// CHECK: %[[VAL_11:.*]] = quake.mz %[[VAL_2]] name "r00001" : (!quake.ref) -> !quake.measure +// CHECK: %[[VAL_12:.*]] = quake.discriminate %[[VAL_11]] : (!quake.measure) -> i1 +// CHECK: %[[VAL_13:.*]] = cc.compute_ptr %[[VAL_3]][1] : (!cc.ptr>) -> !cc.ptr +// CHECK: %[[VAL_14:.*]] = cc.cast unsigned %[[VAL_12]] : (i1) -> i8 +// CHECK: cc.store %[[VAL_14]], %[[VAL_13]] : !cc.ptr +// CHECK: quake.dealloc %[[VAL_0]] : !quake.veq<2> +// CHECK: return +// CHECK: } + +// CHECK-LABEL: func.func @__nvqpp__mlirgen__multi_vector() attributes {"cudaq-entrypoint", "cudaq-kernel"} { +// CHECK: %[[VAL_0:.*]] = quake.alloca !quake.veq<4> +// CHECK: %[[VAL_1:.*]] = quake.extract_ref %[[VAL_0]][0] : (!quake.veq<4>) -> !quake.ref +// CHECK: quake.h %[[VAL_1]] : (!quake.ref) -> () +// CHECK: %[[VAL_2:.*]] = quake.extract_ref %[[VAL_0]][1] : (!quake.veq<4>) -> !quake.ref +// CHECK: quake.x {{\[}}%[[VAL_1]]] %[[VAL_2]] : (!quake.ref, !quake.ref) -> () +// CHECK: %[[VAL_3:.*]] = quake.extract_ref %[[VAL_0]][3] : (!quake.veq<4>) -> !quake.ref +// CHECK: quake.h %[[VAL_3]] : (!quake.ref) -> () +// CHECK: %[[VAL_4:.*]] = quake.extract_ref %[[VAL_0]][2] : (!quake.veq<4>) -> !quake.ref +// CHECK: quake.h %[[VAL_4]] : (!quake.ref) -> () +// CHECK: quake.x {{\[}}%[[VAL_3]]] %[[VAL_4]] : (!quake.ref, !quake.ref) -> () +// CHECK: quake.h %[[VAL_4]] : (!quake.ref) -> () +// CHECK: %[[VAL_5:.*]] = cc.alloca !cc.array +// CHECK: %[[VAL_6:.*]] = cc.string_literal "array" : !cc.ptr> +// CHECK: %[[VAL_7:.*]] = cc.cast %[[VAL_6]] : (!cc.ptr>) -> !cc.ptr +// CHECK: %[[VAL_8:.*]] = arith.constant 4 : i64 +// CHECK: call @__quantum__rt__array_record_output(%[[VAL_8]], %[[VAL_7]]) : (i64, !cc.ptr) -> () +// CHECK: %[[VAL_9:.*]] = quake.mz %[[VAL_1]] name "r00000" : (!quake.ref) -> !quake.measure +// CHECK: %[[VAL_10:.*]] = quake.discriminate %[[VAL_9]] : (!quake.measure) -> i1 +// CHECK: %[[VAL_11:.*]] = cc.cast %[[VAL_5]] : (!cc.ptr>) -> !cc.ptr +// CHECK: %[[VAL_12:.*]] = cc.cast unsigned %[[VAL_10]] : (i1) -> i8 +// CHECK: cc.store %[[VAL_12]], %[[VAL_11]] : !cc.ptr +// CHECK: %[[VAL_13:.*]] = quake.mz %[[VAL_2]] name "r00001" : (!quake.ref) -> !quake.measure +// CHECK: %[[VAL_14:.*]] = quake.discriminate %[[VAL_13]] : (!quake.measure) -> i1 +// CHECK: %[[VAL_15:.*]] = cc.compute_ptr %[[VAL_5]][1] : (!cc.ptr>) -> !cc.ptr +// CHECK: %[[VAL_16:.*]] = cc.cast unsigned %[[VAL_14]] : (i1) -> i8 +// CHECK: cc.store %[[VAL_16]], %[[VAL_15]] : !cc.ptr +// CHECK: %[[VAL_17:.*]] = cc.alloca !cc.array +// CHECK: %[[VAL_18:.*]] = quake.mz %[[VAL_4]] name "r00002" : (!quake.ref) -> !quake.measure +// CHECK: %[[VAL_19:.*]] = quake.discriminate %[[VAL_18]] : (!quake.measure) -> i1 +// CHECK: %[[VAL_20:.*]] = cc.cast %[[VAL_17]] : (!cc.ptr>) -> !cc.ptr +// CHECK: %[[VAL_21:.*]] = cc.cast unsigned %[[VAL_19]] : (i1) -> i8 +// CHECK: cc.store %[[VAL_21]], %[[VAL_20]] : !cc.ptr +// CHECK: %[[VAL_22:.*]] = quake.mz %[[VAL_3]] name "r00003" : (!quake.ref) -> !quake.measure +// CHECK: %[[VAL_23:.*]] = quake.discriminate %[[VAL_22]] : (!quake.measure) -> i1 +// CHECK: %[[VAL_24:.*]] = cc.compute_ptr %[[VAL_17]][1] : (!cc.ptr>) -> !cc.ptr +// CHECK: %[[VAL_25:.*]] = cc.cast unsigned %[[VAL_23]] : (i1) -> i8 +// CHECK: cc.store %[[VAL_25]], %[[VAL_24]] : !cc.ptr +// CHECK: quake.dealloc %[[VAL_0]] : !quake.veq<4> +// CHECK: return +// CHECK: } +// CHECK: func.func private @__quantum__rt__bool_record_output(i1 {llvm.zeroext}, !cc.ptr) +// CHECK: func.func private @__quantum__rt__double_record_output(f64, !cc.ptr) +// CHECK: func.func private @__quantum__rt__int_record_output(i64, !cc.ptr) +// CHECK: func.func private @__quantum__rt__tuple_record_output(i64, !cc.ptr) +// CHECK: func.func private @__quantum__rt__array_record_output(i64, !cc.ptr) diff --git a/unittests/output_record/RecordParserTester.cpp b/unittests/output_record/RecordParserTester.cpp index 0183a5c861b..b4565de50d5 100644 --- a/unittests/output_record/RecordParserTester.cpp +++ b/unittests/output_record/RecordParserTester.cpp @@ -791,3 +791,70 @@ CUDAQ_TEST(ParserTester, checkNamedResults) { buffer = nullptr; origBuffer = nullptr; } + +CUDAQ_TEST(ParserTester, checkResultTypeWithArray) { + const std::string log = + "HEADER\tschema_id\tlabeled\n" + "HEADER\tschema_version\t1.0\n" + "START\n" + "METADATA\tentry_point\n" + "METADATA\toutput_labeling_schema\tschema_id\n" + "METADATA\toutput_names\t[[[0,[0,\"r00000\"]],[1,[1,\"r00001\"]]]]\n" + "METADATA\tqir_profiles\tadaptive_profile\n" + "METADATA\trequired_num_qubits\t2\n" + "METADATA\trequired_num_results\t2\n" + "OUTPUT\tARRAY\t2\tarray\n" + "OUTPUT\tRESULT\t1\tr00000\n" + "OUTPUT\tRESULT\t1\tr00001\n" + "END\t0\n" + "START\n" + "OUTPUT\tARRAY\t2\tarray\n" + "OUTPUT\tRESULT\t1\tr00000\n" + "OUTPUT\tRESULT\t1\tr00001\n" + "END\t0\n" + "START\n" + "OUTPUT\tARRAY\t2\tarray\n" + "OUTPUT\tRESULT\t0\tr00000\n" + "OUTPUT\tRESULT\t0\tr00001\n" + "END\t0\n" + "START\n" + "OUTPUT\tARRAY\t2\tarray\n" + "OUTPUT\tRESULT\t1\tr00000\n" + "OUTPUT\tRESULT\t1\tr00001\n" + "END\t0\n"; + + cudaq::RecordLogParser parser; + parser.parse(log); + auto *origBuffer = parser.getBufferPtr(); + std::size_t bufferSize = parser.getBufferSize(); + char *buffer = static_cast(malloc(bufferSize)); + std::memcpy(buffer, origBuffer, bufferSize); + cudaq::details::RunResultSpan span = {buffer, bufferSize}; + // This is parsed as a vector of bool vectors + std::vector> results = { + reinterpret_cast *>(span.data), + reinterpret_cast *>(span.data + span.lengthInBytes)}; + + // 4 shots + EXPECT_EQ(4, results.size()); + for (const auto &result : results) { + // 2 measured bits each + EXPECT_EQ(2, result.size()); + } + // 1st shot: 1, 1 + EXPECT_EQ(1, results[0][0]); + EXPECT_EQ(1, results[0][1]); + // 2nd shot: 1, 1 + EXPECT_EQ(1, results[1][0]); + EXPECT_EQ(1, results[1][1]); + // 3rd shot: 0, 0 + EXPECT_EQ(0, results[2][0]); + EXPECT_EQ(0, results[2][1]); + // 4th shot: 1, 1 + EXPECT_EQ(1, results[3][0]); + EXPECT_EQ(1, results[3][1]); + + free(buffer); + buffer = nullptr; + origBuffer = nullptr; +} diff --git a/utils/mock_qpu/quantinuum/__init__.py b/utils/mock_qpu/quantinuum/__init__.py index 3c028b99d0f..360cc9d4652 100644 --- a/utils/mock_qpu/quantinuum/__init__.py +++ b/utils/mock_qpu/quantinuum/__init__.py @@ -218,6 +218,9 @@ async def create_job(job: dict): funcPtr = engine.get_function_address(kernelFunctionName) kernel = ctypes.CFUNCTYPE(None)(funcPtr) + # Clear any leftover log from previous jobs + cudaq.testing.getAndClearOutputLog() + # Invoke the Kernel if is_ng_device: qir_log = f"HEADER\tschema_id\tlabeled\nHEADER\tschema_version\t1.0\nSTART\nMETADATA\tentry_point\nMETADATA\tqir_profiles\tadaptive_profile\nMETADATA\trequired_num_qubits\t{numQubitsRequired}\nMETADATA\trequired_num_results\t{numResultsRequired}\n"