Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft of dependency graph generation algorithm #1848

Closed
wants to merge 40 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
a447bd9
Add dependency analysis code
atgeller Jun 24, 2024
c465360
Formatting
atgeller Jun 24, 2024
0943b5a
Use lifetimes computed from dependency graph to map to physical qubits.
atgeller Jun 27, 2024
31a7d64
Use lifetimes to determine when it is safe to reuse qubits, and reord…
atgeller Jul 3, 2024
49edf9e
Insert into new block and whack old block after instead of erasing in…
atgeller Jul 3, 2024
7a31f75
Missed an erase
atgeller Jul 3, 2024
615ee75
Forgot to actually erase old block
atgeller Jul 3, 2024
7a752d9
Some cleanup and fixes
atgeller Jul 10, 2024
96fa396
Merge branch 'main' into dependency_analysis
atgeller Jul 10, 2024
3565142
Fix up scheduling to address bugs, includes two test cases for these …
atgeller Jul 11, 2024
fc5c485
Merge branch 'dependency_analysis' of github.com:atgeller/cuda-quantu…
atgeller Jul 11, 2024
0212dde
Add filecheck commands to tests
atgeller Jul 11, 2024
07050d7
Add assertions and validation functions, and tests for errors. Also i…
atgeller Jul 13, 2024
d959274
More assertions in place
atgeller Jul 15, 2024
2884c07
Add support for classical values
atgeller Jul 16, 2024
4b3136b
Merge branch 'main' into dependency_analysis
atgeller Jul 16, 2024
a9d4941
Fixes several bugs (mostly related to classical values) and adds test…
atgeller Jul 19, 2024
0f5f20a
Merge branch 'dependency_analysis' of github.com:atgeller/cuda-quantu…
atgeller Jul 19, 2024
68a056e
Some cleanup, remove unused functions
atgeller Jul 19, 2024
9a9f826
Remove unnecessary TODO
atgeller Jul 19, 2024
9d5d12d
Improve error messages, add missing test files
atgeller Jul 22, 2024
6526b5e
Improve errors a bit
atgeller Jul 24, 2024
24843c1
Fix performance issue with finding nodes at a given cycle
atgeller Jul 24, 2024
a61ac20
Ensure that classical operations after quantum values are generated
atgeller Jul 24, 2024
0d7160a
Preliminary addition of qubit management passes to JIT pipeline
atgeller Jul 24, 2024
622b8b2
Update pass descriptions
atgeller Jul 24, 2024
f4e93bc
Add qubit management pipeline
atgeller Jul 24, 2024
ffc4c4c
Merge remote-tracking branch 'upstream/main' into dependency_analysis
atgeller Jul 24, 2024
32ecc13
Add to yaml configs, add options to disable qubit management and to d…
atgeller Jul 24, 2024
02b45c9
Add targettests for qubit management
atgeller Jul 24, 2024
ffad80d
Fix bug counting number of virtual qubits
atgeller Jul 25, 2024
da4f915
Update how qubit-management is added to qir-base pipeline
atgeller Jul 29, 2024
6c0ff58
Forgot to remove debug code
atgeller Jul 30, 2024
3de2006
Run qubit management before mapping
atgeller Aug 2, 2024
c61600e
Impose ordering on assigning physical qubits
atgeller Aug 2, 2024
c11779c
Fix various issues in dependency tests
atgeller Aug 6, 2024
8c8f054
Fix bug with codegen for classical values
atgeller Aug 6, 2024
775239f
Avoid reliance on statistic variables to avoid multithreading issues
atgeller Aug 6, 2024
3f01e51
Missed a test file
atgeller Aug 6, 2024
ab3c9c4
Small fix to qubit management pipeline
atgeller Aug 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions include/cudaq/Optimizer/Dialect/Quake/QuakeOps.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ void genericOpPrinter(mlir::OpAsmPrinter &_odsPrinter, mlir::Operation *op,
// Utility functions to test the form of an operation.
//===----------------------------------------------------------------------===//

// Is \p op in the Quake dialect?
inline bool isQuakeOperation(mlir::Operation *op) {
if (auto *dialect = op->getDialect())
return dialect->getNamespace().equals("quake");
return false;
}

namespace quake {
/// Returns true if and only if any quantum operand has type `!quake.ref` or
/// `!quake.veq`.
Expand Down
2 changes: 2 additions & 0 deletions include/cudaq/Optimizer/Transforms/Passes.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ void registerAggressiveEarlyInlining();

void registerUnrollingPipeline();

void registerQubitManagementPipeline();

std::unique_ptr<mlir::Pass> createApplyOpSpecializationPass();
std::unique_ptr<mlir::Pass>
createApplyOpSpecializationPass(bool computeActionOpt);
Expand Down
52 changes: 52 additions & 0 deletions include/cudaq/Optimizer/Transforms/Passes.td
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,17 @@ def ApplySpecialization : Pass<"apply-op-specialization", "mlir::ModuleOp"> {
];
}

def AssignIDs : Pass<"assign-ids", "mlir::func::FuncOp"> {
let summary = "Generate and assign unique identifiers for virtual qubits.";
let description = [{
Attachs a matching unique ID attribute to each `quake.null_wire` and corresponding
`quake.sink`. This is used in DependencyAnalysis to associate allocations and
deallocations for tracking qubit lifetimes.
}];

let dependentDialects = ["quake::QuakeDialect"];
}

def BasisConversionPass: Pass<"basis-conversion", "mlir::ModuleOp"> {
let summary = "Converts kernels to a set of basis operations.";
let description = [{
Expand Down Expand Up @@ -161,6 +172,47 @@ def DecompositionPass: Pass<"decomposition", "mlir::ModuleOp"> {
];
}

def DependencyAnalysis : Pass<"dep-analysis", "mlir::func::FuncOp"> {
let summary = "Maps qubits and reorders operations based on dependency graph.";
let description = [{
A dependency graph is a Directed Acyclic Graph (DAG) where each node represents
an operation, and each edge represents a "depends on" relation between that operation
and another operation. For example, in the following snippet the `x` operation
depends on the `h` operation because it is applied to the same qubit (`q`), so the
`h` operation must happen before the `x` operation:

```
cudaq::qubit q;
x(q);
h(q);
```

Once a dependency graph is created, it is then scheduled, assigning each operation
a virtual cycle. Operations that don't depend on each other may be scheduled at the
same cycle. However, an operation that depends on a second operation must be scheduled
after the second operation. The scheduling algorithm tries to pack operations as
densely as possible, minimizing the number of cycles between operations.

From this dependency graph, we can calculate the lifetime of a qubit: from the cycle
in which it is first used through the cycle in which it is last used. If two virtual
qubits have non-overlapping lifetimes, they can be assigned to the same physical qubit,
as every virtual qubit is assumed to be fully reset before release. Failure to fully
reset virtual qubits before release is undefinied behavior, and will likely lead to
incorrect output when running DependencyAnalysis.
}];

let dependentDialects = ["quake::QuakeDialect"];

let statistics = [
Statistic<"numVirtualQubits", "num-virtual-qubits",
"Number of virtual qubits used">,
Statistic<"numPhysicalQubits", "num-physical-qubits",
"Number of phyiscal qubits used">,
Statistic<"numCycles", "num-cycles",
"Length of kernel in cycles">,
];
}

def EraseNopCalls : Pass<"erase-nop-calls", "mlir::func::FuncOp"> {
let summary = "Erase calls to any builtin intrinsics that are NOPs.";
let description = [{
Expand Down
9 changes: 0 additions & 9 deletions lib/Optimizer/Dialect/Quake/QuakeOps.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,6 @@ namespace {
#include "cudaq/Optimizer/Dialect/Quake/Canonical.inc"
} // namespace

// Is \p op in the Quake dialect?
// TODO: Is this StringRef comparison faster than calling MLIRContext::
// getLoadedDialect("quake")?
static bool isQuakeOperation(Operation *op) {
if (auto *dialect = op->getDialect())
return dialect->getNamespace().equals("quake");
return false;
}

static LogicalResult verifyWireResultsAreLinear(Operation *op) {
for (Value v : op->getOpResults())
if (isa<quake::WireType>(v.getType())) {
Expand Down
174 changes: 174 additions & 0 deletions lib/Optimizer/Transforms/AssignIDs.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*******************************************************************************
* Copyright (c) 2022 - 2024 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 "cudaq/Frontend/nvqpp/AttributeNames.h"
#include "cudaq/Optimizer/Dialect/CC/CCDialect.h"
#include "cudaq/Optimizer/Dialect/Quake/QuakeDialect.h"
#include "cudaq/Optimizer/Dialect/Quake/QuakeOps.h"
#include "cudaq/Optimizer/Transforms/Passes.h"
#include "mlir/IR/PatternMatch.h"
#include "mlir/IR/Threading.h"
#include "mlir/InitAllDialects.h"
#include "mlir/Rewrite/FrozenRewritePatternSet.h"
#include "mlir/Transforms/DialectConversion.h"

using namespace mlir;

//===----------------------------------------------------------------------===//
// Generated logic
//===----------------------------------------------------------------------===//
namespace cudaq::opt {
#define GEN_PASS_DEF_ASSIGNIDS
#include "cudaq/Optimizer/Transforms/Passes.h.inc"
} // namespace cudaq::opt

namespace {
bool isMeasureOp(Operation *op) {
return dyn_cast<quake::MxOp>(*op) || dyn_cast<quake::MyOp>(*op) ||
dyn_cast<quake::MzOp>(*op);
}

int numClassicalInput(Operation *op) {
if (dyn_cast<quake::RxOp>(*op) || dyn_cast<quake::RyOp>(*op) ||
dyn_cast<quake::RzOp>(*op))
return 1;

if (dyn_cast<quake::PhasedRxOp>(*op))
return 2;

return 0;
}

class NullWirePat : public OpRewritePattern<quake::NullWireOp> {
public:
unsigned *counter;

NullWirePat(MLIRContext *context, unsigned *c)
: OpRewritePattern<quake::NullWireOp>(context), counter(c) {}

LogicalResult matchAndRewrite(quake::NullWireOp alloc,
PatternRewriter &rewriter) const override {
if (alloc->hasAttr("qid"))
return failure();

auto qid = (*counter)++;

rewriter.startRootUpdate(alloc);
alloc->setAttr("qid", rewriter.getUI32IntegerAttr(qid));
rewriter.finalizeRootUpdate(alloc);

return success();
}
};

std::optional<uint> findQid(Value v) {
auto defop = v.getDefiningOp();
if (!defop)
return std::nullopt;

if (defop->getRegions().size() != 0) {
defop->emitOpError(
"AssignIDsPass cannot handle non-function operations with regions."
" Do you have if statements in a Base Profile QIR program?");
return std::nullopt;
}

if (!isa<quake::WireType>(v.getType()))
return std::nullopt;

assert(quake::isLinearValueForm(defop) &&
"AssignIDsPass requires operations to be in value form");

if (defop->hasAttr("qid")) {
uint qid = defop->getAttr("qid").cast<IntegerAttr>().getUInt();
return std::optional<uint>(qid);
}

// Figure out matching operand
size_t i = 0;
for (; i < defop->getNumResults(); i++)
if (defop->getResult(i) == v)
break;

// Special cases where result # != operand #:
// Wire is second output but sole input
if (isMeasureOp(defop))
i = 0;
// Classical values preceding wires as input are consumed and not part of the results
i += numClassicalInput(defop);
// Swap op swaps wires
if (dyn_cast<quake::SwapOp>(defop))
i = (i == 1 ? 0 : 1);

return findQid(defop->getOperand(i));
}

class SinkOpPat : public OpRewritePattern<quake::SinkOp> {
public:
SinkOpPat(MLIRContext *context) : OpRewritePattern<quake::SinkOp>(context) {}

LogicalResult matchAndRewrite(quake::SinkOp release,
PatternRewriter &rewriter) const override {
auto qid = findQid(release.getOperand());

if (!qid.has_value())
return failure();

rewriter.startRootUpdate(release);
release->setAttr("qid", rewriter.getUI32IntegerAttr(qid.value()));
rewriter.finalizeRootUpdate(release);

return success();
}
};

//===----------------------------------------------------------------------===//
// Pass implementation
//===----------------------------------------------------------------------===//

struct AssignIDsPass : public cudaq::opt::impl::AssignIDsBase<AssignIDsPass> {
using AssignIDsBase::AssignIDsBase;

void runOnOperation() override {
auto func = getOperation();

if (!func->hasAttr("cudaq-kernel") || func.getBlocks().empty())
return;

if (!func.getFunctionBody().hasOneBlock()) {
func.emitError("AssignIDsPass cannot handle multiple blocks. Do "
"you have if statements in a Base Profile QIR program?");
signalPassFailure();
return;
}

assign();
}

void assign() {
auto *ctx = &getContext();
func::FuncOp func = getOperation();
RewritePatternSet patterns(ctx);
unsigned x = 0;
patterns.insert<NullWirePat>(ctx, &x);
patterns.insert<SinkOpPat>(ctx);
ConversionTarget target(*ctx);
target.addLegalDialect<quake::QuakeDialect>();
target.addDynamicallyLegalOp<quake::NullWireOp>(
[&](quake::NullWireOp alloc) { return alloc->hasAttr("qid"); });
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The attribute approach is brittle as other transformations will not necessarily keep the information. We should really use WireSet, BorrowWire, ReturnWire for explicitly mapping the identity of a qubit from an infinite space (virtual registers) to a finite space (physical registers).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, right now Assign IDs and Dependency Analysis are very tangled passes, so they pretty much have to be run one right after another, and Dependency Analysis has to check that the attributes are present.

target.addDynamicallyLegalOp<quake::SinkOp>(
[&](quake::SinkOp sink) { return sink->hasAttr("qid"); });
if (failed(applyPartialConversion(func.getOperation(), target,
std::move(patterns)))) {
func.emitOpError("Assigning qids failed");
signalPassFailure();
}
}
};

} // namespace
2 changes: 2 additions & 0 deletions lib/Optimizer/Transforms/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ add_cudaq_library(OptTransforms
AggressiveEarlyInlining.cpp
ApplyControlNegations.cpp
ApplyOpSpecialization.cpp
AssignIDs.cpp
BasisConversion.cpp
CombineQuantumAlloc.cpp
ConstPropComplex.cpp
Expand All @@ -39,6 +40,7 @@ add_cudaq_library(OptTransforms
LowerUnwind.cpp
Mapping.cpp
MemToReg.cpp
DependencyAnalysis.cpp
MultiControlDecomposition.cpp
ObserveAnsatz.cpp
PruneCtrlRelations.cpp
Expand Down
Loading
Loading