Skip to content

Commit c47d92c

Browse files
Implementation of the cudaq::apply_noise feature (#2635)
* Fine-grain kraus channel application in a kernel * updates * compile time checks for number of args * better type safety on input args * cleanup * small fixes * update to use integer key for registry * remove old headers * remove the name member * [core] Support for apply_noise. Add an apply_noise operator. Teach the bridge to recognize cudaq::apply_noise and lower it. Add a pass to erase apply_noise operations. Modify QIR codegen to generate the C++ callback. Make test for elimination more robust. Arguments have to be passed by rvalue reference since some of them are qubits, which cannot be copied. Thread the pointer through all the places where there were floating-point types. Update tests. Add a bit more smart to the cudaq::apply_noise stub. Automatically counts the leading number of floating-point arguments, so the user doesn't have to supply this information. Uses the applyNoiseImpl<> template as is. Add some constraints to fine-tune the apply_noise overload selection. Add std::vector<double> overload processing to the bridge. * Make the key an i64 and verify it. * Rope off the C++20 sections. * Make the noise_func symbol optional. * Add to the roundtrip test. * Fix test. * Fix signature. * Add cc.call_vararg op. * Add codegen test. * Fix test. * Enable C++17 in the headers. * Add python support for apply_noise (#4) * Start on Python apply_noise support * add some docs * Extend __quantum__qis__apply_kraus_channel_generalized() to support spans as part of the variadic arguments. * Fix typo. * Content checks. * Do it again.* And again. * Catch curious exception being thrown. * More whining about formatting. * Try to exclude register_channel to avoid the error. * Take care of review comments. * Address PR comments, checks on corner cases, warnings emitted * Better noise model checking, handle cases with no context * Rework the noise model header files to support float and double. * clang-format * Add enum to the apply_noise entry point to select the floating point type. * Move template to lambda. * Fix a bug and more unit tests. * Add another test. --------- Signed-off-by: Alex McCaskey <[email protected]> Signed-off-by: Eric Schweitz <[email protected]> Co-authored-by: Alex McCaskey <[email protected]>
1 parent a2614d7 commit c47d92c

36 files changed

+1586
-54
lines changed

docs/sphinx/api/languages/python_api.rst

+1
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ Noisy Simulation
218218

219219
.. autoclass:: cudaq::NoiseModel
220220
:members:
221+
:exclude-members: register_channel
221222
:special-members: __init__
222223

223224
.. autoclass:: cudaq::BitFlipChannel

include/cudaq/Optimizer/CodeGen/QIRFunctionNames.h

+7
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,11 @@ static constexpr const char QIRRecordOutput[] =
9494
static constexpr const char QIRClearResultMaps[] =
9595
"__quantum__rt__clear_result_maps";
9696

97+
/// Used to specify the type of the data elements in the `QISApplyKrausChannel`
98+
/// call. (`float` or `double`)
99+
enum class KrausChannelDataKind { FloatKind, DoubleKind };
100+
101+
static constexpr const char QISApplyKrausChannel[] =
102+
"__quantum__qis__apply_kraus_channel_generalized";
103+
97104
} // namespace cudaq::opt

include/cudaq/Optimizer/Dialect/Quake/QuakeOps.td

+53
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,59 @@ def quake_ComputeActionOp : QuakeOp<"compute_action"> {
477477
}];
478478
}
479479

480+
def quake_ApplyNoiseOp : QuakeOp<"apply_noise", [AttrSizedOperandSegments]> {
481+
let summary = "Apply a noise operation to qubits.";
482+
let description = [{
483+
This operation provides support for the `cudaq::apply_noise` template
484+
function. This function is only valid is simulation contexts where the
485+
simulator is part of the same process as the C++ host executable itself.
486+
487+
A noise operator is the application of a Kraus channel to a selected set
488+
of qubits. This is a point-wise annotation approach that a user might
489+
deploy to introduce "noise" to their circuit under simulation. It is unlike
490+
a general (unitary) gate application in that there is no notion of controls
491+
or an adjoint.
492+
}];
493+
494+
let arguments = (ins
495+
OptionalAttr<FlatSymbolRefAttr>:$noise_func,
496+
Optional<AnySignlessInteger>:$key,
497+
Variadic<AnyType>:$parameters,
498+
Variadic<NonStruqRefType>:$qubits
499+
);
500+
501+
let hasVerifier = 1;
502+
let hasCustomAssemblyFormat = 1;
503+
504+
let builders = [
505+
OpBuilder<(ins "mlir::StringRef":$noise_func,
506+
"mlir::ValueRange":$parameters,
507+
"mlir::ValueRange":$targets), [{
508+
return build($_builder, $_state, mlir::TypeRange{},
509+
mlir::FlatSymbolRefAttr::get($_builder.getContext(), noise_func), {},
510+
parameters, targets);
511+
}]>,
512+
OpBuilder<(ins "mlir::FlatSymbolRefAttr":$noise_func,
513+
"mlir::ValueRange":$parameters,
514+
"mlir::ValueRange":$targets), [{
515+
return build($_builder, $_state, mlir::TypeRange{}, noise_func, {},
516+
parameters, targets);
517+
}]>,
518+
OpBuilder<(ins "mlir::Value":$key,
519+
"mlir::ValueRange":$parameters,
520+
"mlir::ValueRange":$targets), [{
521+
return build($_builder, $_state, mlir::TypeRange{},
522+
mlir::FlatSymbolRefAttr{}, key, parameters, targets);
523+
}]>
524+
];
525+
526+
let extraClassDeclaration = [{
527+
static constexpr mlir::StringRef getNoiseFuncAttrNameStr() {
528+
return "noise_func";
529+
}
530+
}];
531+
}
532+
480533
//===----------------------------------------------------------------------===//
481534
// Memory and register conversion instructions: These operations are useful for
482535
// intermediate conversions between memory-SSA and value-SSA semantics and vice

include/cudaq/Optimizer/Transforms/Passes.td

+10-1
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,16 @@ def DependencyAnalysis : Pass<"dep-analysis", "mlir::ModuleOp"> {
310310
];
311311
}
312312

313-
def EraseNopCalls : Pass<"erase-nop-calls", "mlir::func::FuncOp"> {
313+
def EraseNoise : Pass<"erase-noise"> {
314+
let summary = "Erase the inject of noise via Kraus channels.";
315+
let description = [{
316+
Although CUDA-Q allows the user to specify the application of noise via
317+
Kraus channels, these are not needed and must be removed if the code is to
318+
run on quantum hardware, for example.
319+
}];
320+
}
321+
322+
def EraseNopCalls : Pass<"erase-nop-calls"> {
314323
let summary = "Erase calls to any builtin intrinsics that are NOPs.";
315324
let description = [{
316325
The code may contain marker function calls that do not generate any actual

lib/Frontend/nvqpp/ConvertExpr.cpp

+56-4
Original file line numberDiff line numberDiff line change
@@ -1503,6 +1503,60 @@ bool QuakeBridgeVisitor::VisitCallExpr(clang::CallExpr *x) {
15031503
return true;
15041504
}
15051505

1506+
if (funcName == "apply_noise") {
1507+
SmallVector<Value> params;
1508+
SmallVector<Value> qubits;
1509+
bool inParams = true;
1510+
for (auto iter : llvm::enumerate(args)) {
1511+
auto a = iter.value();
1512+
Type aTy = a.getType();
1513+
if (inParams) {
1514+
if (auto ptrTy = dyn_cast<cudaq::cc::PointerType>(aTy))
1515+
if (isa<FloatType>(ptrTy.getElementType())) {
1516+
params.push_back(a);
1517+
continue;
1518+
}
1519+
if (auto stdvecTy = dyn_cast<cudaq::cc::StdvecType>(aTy))
1520+
if (stdvecTy.getElementType() == builder.getF64Type() &&
1521+
iter.index() == 0) {
1522+
params.push_back(a);
1523+
inParams = false;
1524+
continue;
1525+
}
1526+
inParams = false;
1527+
}
1528+
// The first argument that is not floating-point must be a qubit. If
1529+
// the user has interleaved floating-point and qubit arguments, that's
1530+
// an error.
1531+
if (isa<quake::RefType, quake::VeqType>(aTy)) {
1532+
qubits.push_back(a);
1533+
} else {
1534+
reportClangError(x, mangler,
1535+
"apply_noise argument types not supported.");
1536+
return false;
1537+
}
1538+
}
1539+
1540+
if (auto callee = calleeOp.getDefiningOp<func::ConstantOp>()) {
1541+
StringRef calleeName = callee.getValue();
1542+
builder.create<quake::ApplyNoiseOp>(loc, calleeName, params, qubits);
1543+
1544+
// Add the declaration of the function to the module.
1545+
SmallVector<Type> argTys;
1546+
for (auto p : params)
1547+
argTys.push_back(p.getType());
1548+
for (auto q : qubits)
1549+
argTys.push_back(q.getType());
1550+
auto calleeTy = FunctionType::get(builder.getContext(), argTys, {});
1551+
cudaq::opt::factory::getOrAddFunc(loc, calleeName, calleeTy, module);
1552+
return true;
1553+
}
1554+
1555+
reportClangError(x, mangler,
1556+
"apply_noise with a vector argument is deprecated.");
1557+
return false;
1558+
}
1559+
15061560
if (funcName.equals("mx") || funcName.equals("my") ||
15071561
funcName.equals("mz")) {
15081562
// Measurements always return a bool or a std::vector<bool>.
@@ -1807,8 +1861,7 @@ bool QuakeBridgeVisitor::VisitCallExpr(clang::CallExpr *x) {
18071861
kernelArgs);
18081862
return inlinedFinishControlNegations();
18091863
}
1810-
if (auto func =
1811-
dyn_cast_or_null<func::ConstantOp>(calleeValue.getDefiningOp())) {
1864+
if (auto func = calleeValue.getDefiningOp<func::ConstantOp>()) {
18121865
auto funcTy = cast<FunctionType>(func.getType());
18131866
auto callableSym = func.getValueAttr();
18141867
inlinedStartControlNegations();
@@ -1920,8 +1973,7 @@ bool QuakeBridgeVisitor::VisitCallExpr(clang::CallExpr *x) {
19201973
/*isAdjoint=*/true, ValueRange{},
19211974
kernArgs);
19221975
}
1923-
if (auto func =
1924-
dyn_cast_or_null<func::ConstantOp>(kernelValue.getDefiningOp())) {
1976+
if (auto func = kernelValue.getDefiningOp<func::ConstantOp>()) {
19251977
auto kernSym = func.getValueAttr();
19261978
auto funcTy = cast<FunctionType>(func.getType());
19271979
auto kernArgs =

lib/Optimizer/Builder/Intrinsics.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,7 @@ static constexpr IntrinsicCode intrinsicTable[] = {
435435
func.func private @__quantum__qis__custom_unitary__adj(!cc.ptr<complex<f64>>, !qir_array, !qir_array, !qir_charptr)
436436
437437
llvm.func @generalizedInvokeWithRotationsControlsTargets(i64, i64, i64, i64, !qir_llvmptr, ...) attributes {sym_visibility = "private"}
438+
llvm.func @__quantum__qis__apply_kraus_channel_generalized(i64, i64, i64, i64, i64, ...) attributes {sym_visibility = "private"}
438439
)#"},
439440

440441
// Declarations for base and adaptive profile QIR functions used by codegen.

lib/Optimizer/CodeGen/ConvertToQIRAPI.cpp

+159-10
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,155 @@ struct AllocaOpToIntRewrite : public OpConversionPattern<quake::AllocaOp> {
281281
}
282282
};
283283

284+
struct ApplyNoiseOpRewrite : public OpConversionPattern<quake::ApplyNoiseOp> {
285+
using OpConversionPattern::OpConversionPattern;
286+
287+
LogicalResult
288+
matchAndRewrite(quake::ApplyNoiseOp noise, OpAdaptor adaptor,
289+
ConversionPatternRewriter &rewriter) const override {
290+
auto loc = noise.getLoc();
291+
292+
if (!noise.getNoiseFunc()) {
293+
// This is the key-based variant. Call the generalized version of the
294+
// apply_kraus_channel helper function. Let it do all the conversions into
295+
// contiguous buffers for us, greatly simplifying codegen here.
296+
SmallVector<Value> args;
297+
const bool pushASpan =
298+
adaptor.getParameters().size() == 1 &&
299+
isa<cudaq::cc::StdvecType>(adaptor.getParameters()[0].getType());
300+
const bool usingDouble = [&]() {
301+
if (adaptor.getParameters().empty())
302+
return true;
303+
auto param0 = adaptor.getParameters()[0];
304+
if (pushASpan)
305+
return cast<cudaq::cc::StdvecType>(param0.getType())
306+
.getElementType() == rewriter.getF64Type();
307+
return cast<cudaq::cc::PointerType>(param0.getType())
308+
.getElementType() == rewriter.getF64Type();
309+
}();
310+
if (usingDouble) {
311+
auto code = static_cast<std::int64_t>(
312+
cudaq::opt::KrausChannelDataKind::DoubleKind);
313+
args.push_back(rewriter.create<arith::ConstantIntOp>(loc, code, 64));
314+
} else {
315+
auto code = static_cast<std::int64_t>(
316+
cudaq::opt::KrausChannelDataKind::FloatKind);
317+
args.push_back(rewriter.create<arith::ConstantIntOp>(loc, code, 64));
318+
}
319+
args.push_back(adaptor.getKey());
320+
if (pushASpan) {
321+
args.push_back(rewriter.create<arith::ConstantIntOp>(loc, 1, 64));
322+
args.push_back(rewriter.create<arith::ConstantIntOp>(loc, 0, 64));
323+
} else {
324+
args.push_back(rewriter.create<arith::ConstantIntOp>(loc, 0, 64));
325+
auto numParams = std::distance(adaptor.getParameters().begin(),
326+
adaptor.getParameters().end());
327+
args.push_back(
328+
rewriter.create<arith::ConstantIntOp>(loc, numParams, 64));
329+
}
330+
auto numTargets =
331+
std::distance(adaptor.getQubits().begin(), adaptor.getQubits().end());
332+
args.push_back(
333+
rewriter.create<arith::ConstantIntOp>(loc, numTargets, 64));
334+
if (pushASpan) {
335+
Value stdvec = adaptor.getParameters()[0];
336+
auto stdvecTy = cast<cudaq::cc::StdvecType>(stdvec.getType());
337+
auto dataTy = cudaq::cc::PointerType::get(
338+
cudaq::cc::ArrayType::get(stdvecTy.getElementType()));
339+
args.push_back(
340+
rewriter.create<cudaq::cc::StdvecDataOp>(loc, dataTy, stdvec));
341+
args.push_back(rewriter.create<cudaq::cc::StdvecSizeOp>(
342+
loc, rewriter.getI64Type(), stdvec));
343+
} else {
344+
args.append(adaptor.getParameters().begin(),
345+
adaptor.getParameters().end());
346+
}
347+
args.append(adaptor.getQubits().begin(), adaptor.getQubits().end());
348+
349+
rewriter.replaceOpWithNewOp<cudaq::cc::VarargCallOp>(
350+
noise, TypeRange{}, cudaq::opt::QISApplyKrausChannel, args);
351+
return success();
352+
}
353+
354+
// This is a noise_func variant. Call the noise function. There are two
355+
// cases that must be considered.
356+
//
357+
// 1. The parameters to the Kraus channel are passed in an object of type
358+
// `std::vector<double>`. To do that requires a bunch of code to translate
359+
// the span of doubles on the device side into a `std::vector<double>` on
360+
// the stack for passing to the host-side function. It is ABSOLUTELY
361+
// CRITICAL that the host side NOT use move semantics or otherwise try to
362+
// claim ownership of the fake vector being passed back as that will crash
363+
// the executable. The host side should not modify the content of the vector
364+
// either. These assumptions are made in this code as the argument to the
365+
// host side is `const std::vector<double>&`. This code must also modify the
366+
// signature of the called function since the bridge will have assumed it
367+
// was a span. Again all of this chicanery is so we don't call the function
368+
// with the wrong data type and/or have the callee try to modify the vector.
369+
// Such actions will result in the executable CRASHING or giving WRONG
370+
// ANSWERS.
371+
//
372+
// 2. Easier by a jaw-dropping margin, just pass rvalue references to double
373+
// values, each individually, back to the host-side function. Since that's
374+
// already the case, we just append the operands.
375+
SmallVector<Value> args;
376+
if (adaptor.getParameters().size() == 1 &&
377+
isa<cudaq::cc::StdvecType>(adaptor.getParameters()[0].getType())) {
378+
Value svp = adaptor.getParameters()[0];
379+
// Convert the device-side span back to a host-side vector so that C++
380+
// doesn't crash.
381+
auto stdvecTy = cast<cudaq::cc::StdvecType>(svp.getType());
382+
auto *ctx = rewriter.getContext();
383+
auto ptrTy = cudaq::cc::PointerType::get(stdvecTy.getElementType());
384+
auto ptrArrTy = cudaq::cc::PointerType::get(
385+
cudaq::cc::ArrayType::get(stdvecTy.getElementType()));
386+
auto hostVecTy = cudaq::cc::ArrayType::get(ctx, ptrTy, 3);
387+
auto hostVec = rewriter.create<cudaq::cc::AllocaOp>(loc, hostVecTy);
388+
Value startPtr =
389+
rewriter.create<cudaq::cc::StdvecDataOp>(loc, ptrArrTy, svp);
390+
auto i64Ty = rewriter.getI64Type();
391+
Value len = rewriter.create<cudaq::cc::StdvecSizeOp>(loc, i64Ty, svp);
392+
Value endPtr = rewriter.create<cudaq::cc::ComputePtrOp>(
393+
loc, ptrTy, startPtr, ArrayRef<cudaq::cc::ComputePtrArg>{len});
394+
Value castStartPtr =
395+
rewriter.create<cudaq::cc::CastOp>(loc, ptrTy, startPtr);
396+
auto ptrPtrTy = cudaq::cc::PointerType::get(ptrTy);
397+
Value ptr0 = rewriter.create<cudaq::cc::ComputePtrOp>(
398+
loc, ptrPtrTy, hostVec, ArrayRef<cudaq::cc::ComputePtrArg>{0});
399+
rewriter.create<cudaq::cc::StoreOp>(loc, castStartPtr, ptr0);
400+
Value ptr1 = rewriter.create<cudaq::cc::ComputePtrOp>(
401+
loc, ptrPtrTy, hostVec, ArrayRef<cudaq::cc::ComputePtrArg>{1});
402+
rewriter.create<cudaq::cc::StoreOp>(loc, endPtr, ptr1);
403+
Value ptr2 = rewriter.create<cudaq::cc::ComputePtrOp>(
404+
loc, ptrPtrTy, hostVec, ArrayRef<cudaq::cc::ComputePtrArg>{2});
405+
rewriter.create<cudaq::cc::StoreOp>(loc, endPtr, ptr2);
406+
407+
// N.B. This pointer must be treated as const by the C++ side and should
408+
// never have move semantics!
409+
args.push_back(hostVec);
410+
411+
// Finally, we need to modify the called function's signature.
412+
auto module = noise->getParentOfType<ModuleOp>();
413+
auto funcTy = FunctionType::get(ctx, {}, {});
414+
auto [fn, flag] = cudaq::opt::factory::getOrAddFunc(
415+
loc, *noise.getNoiseFunc(), funcTy, module);
416+
funcTy = fn.getFunctionType();
417+
SmallVector<Type> inputTys{funcTy.getInputs().begin(),
418+
funcTy.getInputs().end()};
419+
inputTys[0] = hostVec.getType();
420+
auto newFuncTy = FunctionType::get(ctx, inputTys, funcTy.getResults());
421+
fn.setFunctionType(newFuncTy);
422+
} else {
423+
args.append(adaptor.getParameters().begin(),
424+
adaptor.getParameters().end());
425+
}
426+
args.append(adaptor.getQubits().begin(), adaptor.getQubits().end());
427+
rewriter.replaceOpWithNewOp<func::CallOp>(noise, TypeRange{},
428+
*noise.getNoiseFunc(), args);
429+
return success();
430+
}
431+
};
432+
284433
struct MaterializeConstantArrayOpRewrite
285434
: public OpConversionPattern<cudaq::codegen::MaterializeConstantArrayOp> {
286435
using OpConversionPattern::OpConversionPattern;
@@ -919,10 +1068,10 @@ struct MeasurementOpPattern : public OpConversionPattern<quake::MzOp> {
9191068
auto cstringGlobal =
9201069
createGlobalCString(mz, loc, rewriter, regNameAttr.getValue());
9211070
if constexpr (!M::discriminateToClassical) {
922-
// These QIR profile variants force all record output calls to appear at
923-
// the end. In these variants, control-flow isn't allowed in the final
924-
// LLVM. Therefore, a single basic block is assumed but unchecked here
925-
// as the verifier will raise an error.
1071+
// These QIR profile variants force all record output calls to appear
1072+
// at the end. In these variants, control-flow isn't allowed in the
1073+
// final LLVM. Therefore, a single basic block is assumed but unchecked
1074+
// here as the verifier will raise an error.
9261075
rewriter.setInsertionPoint(rewriter.getBlock()->getTerminator());
9271076
}
9281077
auto recOut = rewriter.create<func::CallOp>(
@@ -1454,8 +1603,8 @@ static void commonClassicalHandlingPatterns(RewritePatternSet &patterns,
14541603
static void commonQuakeHandlingPatterns(RewritePatternSet &patterns,
14551604
TypeConverter &typeConverter,
14561605
MLIRContext *ctx) {
1457-
patterns.insert<GetMemberOpRewrite, MakeStruqOpRewrite, RelaxSizeOpErase,
1458-
VeqSizeOpRewrite>(typeConverter, ctx);
1606+
patterns.insert<ApplyNoiseOpRewrite, GetMemberOpRewrite, MakeStruqOpRewrite,
1607+
RelaxSizeOpErase, VeqSizeOpRewrite>(typeConverter, ctx);
14591608
}
14601609

14611610
//===----------------------------------------------------------------------===//
@@ -1865,10 +2014,10 @@ struct QuakeToQIRAPIPrepPass
18652014

18662015
// Recursive walk in func.
18672016
func.walk([&](Operation *op) {
1868-
// Annotate all qubit allocations with the starting qubit index value.
1869-
// This ought to handle both reference and value semantics. If the
1870-
// value semantics is using wire sets, no (redundant) annotation is
1871-
// needed.
2017+
// Annotate all qubit allocations with the starting qubit index
2018+
// value. This ought to handle both reference and value semantics. If
2019+
// the value semantics is using wire sets, no (redundant) annotation
2020+
// is needed.
18722021
if (auto alloc = dyn_cast<quake::AllocaOp>(op)) {
18732022
auto allocTy = alloc.getType();
18742023
if (isa<quake::RefType>(allocTy)) {

0 commit comments

Comments
 (0)