Skip to content
This repository was archived by the owner on Jan 12, 2024. It is now read-only.

Conversation

@troels-im
Copy link
Contributor

@troels-im troels-im commented Aug 4, 2021

Profile adoption tool

Note PR is WiP.

Goal

The overall goal of this PR is to create a first version of a tool that allows transformation of a general QIR to a base profile.

Quick start

Once the project is built (see next sections), you can generate a new QIR as follows:

./Source/Apps/qat --generate --profile baseProfile -S ../examples/QubitAllocationAnalysis/analysis-example.ll

Likewise, you can validate that a QIR follows a specification by running:

./Source/Apps/qat --validate --profile baseProfile -S ../examples/QubitAllocationAnalysis/analysis-example.ll

Example

In this example, we start with a QIR generated by the Q# frontend. Rather than giving the full 3445 lines of QIR, we instead give the frontend code:

namespace TeleportChain {
    open Microsoft.Quantum.Intrinsic;
    open Microsoft.Quantum.Canon;
    open Microsoft.Quantum.Arrays;
    open Microsoft.Quantum.Measurement;
    open Microsoft.Quantum.Preparation;

    operation PrepareEntangledPair(left : Qubit, right : Qubit) : Unit is Adj + Ctl {
        H(left);
        CNOT(left, right);
    }

    operation ApplyCorrection(src : Qubit, intermediary : Qubit, dest : Qubit) : Unit {
        if (MResetZ(src) == One) { Z(dest); }
        if (MResetZ(intermediary) == One) { X(dest); }
    }

    operation TeleportQubitUsingPresharedEntanglement(src : Qubit, intermediary : Qubit, dest : Qubit) : Unit {
        Adjoint PrepareEntangledPair(src, intermediary);
        ApplyCorrection(src, intermediary, dest);
    }

    operation TeleportQubit(src : Qubit, dest : Qubit) : Unit {
        use intermediary = Qubit();
        PrepareEntangledPair(intermediary, dest);
        TeleportQubitUsingPresharedEntanglement(src, intermediary, dest);
    }

    operation DemonstrateEntanglementSwapping() : (Result, Result) {
        use (reference, src, intermediary, dest) = (Qubit(), Qubit(), Qubit(), Qubit());
        PrepareEntangledPair(reference, src);
        TeleportQubit(src, dest);
        return (MResetZ(reference), MResetZ(dest));
    }

    @EntryPoint()
    operation DemonstrateTeleportationUsingPresharedEntanglement() : Unit {
        let nPairs = 2;
        use (leftMessage, rightMessage, leftPreshared, rightPreshared) = (Qubit(), Qubit(), Qubit[nPairs], Qubit[nPairs]);
        PrepareEntangledPair(leftMessage, rightMessage);
        for i in 0..nPairs-1 {
            PrepareEntangledPair(leftPreshared[i], rightPreshared[i]);
        }

        TeleportQubitUsingPresharedEntanglement(rightMessage, leftPreshared[0], rightPreshared[0]);
        for i in 1..nPairs-1 {
            TeleportQubitUsingPresharedEntanglement(rightPreshared[i-1], leftPreshared[i], rightPreshared[i]);
        }

        let _ = MResetZ(leftMessage);
        let _ =  MResetZ(rightPreshared[nPairs-1]);
    }
}

Once compiled and the initial QIR is generated and save in the file analysis-example.ll, we execute the command

./Source/Apps/qat --generate --profile baseProfile ./analysis-example.ll

The QAT tool will now attempt to map the QIR in analysis-example.ll into a QIR which is compatible with the base format. Removing type and function declarations, the correspoding code reads:

; ModuleID = './analysis-example.ll'
source_filename = "./analysis-example.ll"

define internal fastcc void @TeleportChain__DemonstrateTeleportationUsingPresharedEntanglement__body() unnamed_addr {
entry:
  call void @__quantum__qis__h(%Qubit* null)
  call void @__quantum__qis__cnot(%Qubit* null, %Qubit* nonnull inttoptr (i64 1 to %Qubit*))
  call void @__quantum__qis__h(%Qubit* null)
  call void @__quantum__qis__cnot(%Qubit* null, %Qubit* nonnull inttoptr (i64 2 to %Qubit*))
  call void @__quantum__qis__h(%Qubit* nonnull inttoptr (i64 1 to %Qubit*))
  call void @__quantum__qis__cnot(%Qubit* nonnull inttoptr (i64 1 to %Qubit*), %Qubit* nonnull inttoptr (i64 3 to %Qubit*))
  call void @__quantum__qis__cnot(%Qubit* nonnull inttoptr (i64 1 to %Qubit*), %Qubit* null)
  call void @__quantum__qis__h(%Qubit* nonnull inttoptr (i64 1 to %Qubit*))
  %0 = call i1 @__quantum__qir__read_result(%Result* nonnull inttoptr (i64 4 to %Result*))
  call void @__quantum__qis__mz__body(%Qubit* nonnull inttoptr (i64 1 to %Qubit*), %Result* nonnull inttoptr (i64 4 to %Result*))
  call void @__quantum__qis__reset__body(%Qubit* nonnull inttoptr (i64 1 to %Qubit*))
  br i1 %0, label %then0__1.i.i, label %continue__1.i.i

then0__1.i.i:                                     ; preds = %entry
  call void @__quantum__qis__z(%Qubit* nonnull inttoptr (i64 2 to %Qubit*))
  br label %continue__1.i.i

continue__1.i.i:                                  ; preds = %then0__1.i.i, %entry
  %1 = call i1 @__quantum__qir__read_result(%Result* nonnull inttoptr (i64 5 to %Result*))
  call void @__quantum__qis__mz__body(%Qubit* null, %Result* nonnull inttoptr (i64 5 to %Result*))
  call void @__quantum__qis__reset__body(%Qubit* null)
  br i1 %1, label %then0__2.i.i, label %TeleportChain__TeleportQubitUsingPresharedEntanglement__body.1.exit

then0__2.i.i:                                     ; preds = %continue__1.i.i
  call void @__quantum__qis__x(%Qubit* nonnull inttoptr (i64 2 to %Qubit*))
  br label %TeleportChain__TeleportQubitUsingPresharedEntanglement__body.1.exit

TeleportChain__TeleportQubitUsingPresharedEntanglement__body.1.exit: ; preds = %continue__1.i.i, %then0__2.i.i
  call void @__quantum__qis__cnot(%Qubit* nonnull inttoptr (i64 2 to %Qubit*), %Qubit* nonnull inttoptr (i64 1 to %Qubit*))
  call void @__quantum__qis__h(%Qubit* nonnull inttoptr (i64 2 to %Qubit*))
  %2 = call i1 @__quantum__qir__read_result(%Result* nonnull inttoptr (i64 6 to %Result*))
  call void @__quantum__qis__mz__body(%Qubit* nonnull inttoptr (i64 2 to %Qubit*), %Result* nonnull inttoptr (i64 6 to %Result*))
  call void @__quantum__qis__reset__body(%Qubit* nonnull inttoptr (i64 2 to %Qubit*))
  br i1 %2, label %then0__1.i.i2, label %continue__1.i.i3

then0__1.i.i2:                                    ; preds = %TeleportChain__TeleportQubitUsingPresharedEntanglement__body.1.exit
  call void @__quantum__qis__z(%Qubit* nonnull inttoptr (i64 3 to %Qubit*))
  br label %continue__1.i.i3

continue__1.i.i3:                                 ; preds = %then0__1.i.i2, %TeleportChain__TeleportQubitUsingPresharedEntanglement__body.1.exit
  %3 = call i1 @__quantum__qir__read_result(%Result* nonnull inttoptr (i64 7 to %Result*))
  call void @__quantum__qis__mz__body(%Qubit* nonnull inttoptr (i64 1 to %Qubit*), %Result* nonnull inttoptr (i64 7 to %Result*))
  call void @__quantum__qis__reset__body(%Qubit* nonnull inttoptr (i64 1 to %Qubit*))
  br i1 %3, label %then0__2.i.i4, label %TeleportChain__TeleportQubitUsingPresharedEntanglement__body.2.exit

then0__2.i.i4:                                    ; preds = %continue__1.i.i3
  call void @__quantum__qis__x(%Qubit* nonnull inttoptr (i64 3 to %Qubit*))
  br label %TeleportChain__TeleportQubitUsingPresharedEntanglement__body.2.exit

TeleportChain__TeleportQubitUsingPresharedEntanglement__body.2.exit: ; preds = %continue__1.i.i3, %then0__2.i.i4
  call void @__quantum__qis__mz__body(%Qubit* null, %Result* null)
  call void @__quantum__qis__reset__body(%Qubit* null)
  call void @__quantum__qis__mz__body(%Qubit* nonnull inttoptr (i64 3 to %Qubit*), %Result* nonnull inttoptr (i64 1 to %Result*))
  call void @__quantum__qis__reset__body(%Qubit* nonnull inttoptr (i64 3 to %Qubit*))
  ret void
}

We note the absence of loops, and that quantum registers are "allocated" at compile time meaning that each qubit instance is assigned a unique ID. As some code may be dead and optimised away, the qubit allocation is not garantueed to be sequential at this point in time. Future work will include writing a qubit ID remapper which will allow qubits.

We also note that the function TeleportChain__TeleportQubitUsingPresharedEntanglement__body was cloned twice. This is due to the allocation of qubits and the function being called twice. At present, the analyser does not take qubit release into account and just assumes that it will never be released due to the complicated nature for dealing with nested functions at compile time.

Current TODOs include getting LLVM to remove dead code, do better constant folding and function inlining. Once this is performed correctly, next steps is the remapper and finally a better analysis on what call paths potentially create problems in terms of qubit allocation.

Dependencies

This library is written in C++ and depends on:

  • LLVM

Additional development dependencies include:

  • CMake
  • clang-format
  • clang-tidy

Building the passes

To build the passes, create a new build directory and switch to that directory:

mkdir Debug
cd Debug/

To build the library, first configure CMake from the build directory

cmake ..

and then make your target

make [target]

The default target is all. Other valid targets are the name of the folders in libs/ found in the passes root.

Profile adoption tool

Building QAT

First

cd Debug
make qat

then

./Source/Apps/qat

Implementing a profile pass

As an example of how one can implement a new profile pass, we here show the implementational details of our example pass which allows mapping the teleportation code to the base profile:

        pb.registerPipelineParsingCallback([](StringRef name, FunctionPassManager &fpm,
                                              ArrayRef<PassBuilder::PipelineElement> /*unused*/) {
          // Base profile
          if (name == "restrict-qir<base-profile>")
          {
            RuleSet rule_set;

            // Defining the mapping
            auto factory = RuleFactory(rule_set);

            factory.useStaticQuantumArrayAllocation();
            factory.useStaticQuantumAllocation();
            factory.useStaticResultAllocation();

            factory.optimiseBranchQuatumOne();
            //  factory.optimiseBranchQuatumZero();

            factory.disableReferenceCounting();
            factory.disableAliasCounting();
            factory.disableStringSupport();

            fpm.addPass(TransformationRulePass(std::move(rule_set)));
            return true;
          }

          return false;
        });
      }};

Transformations of the IR will happen on the basis of what rules are added to the rule set. The purpose of the factory is to make easy to add rules that serve a single purpose as well as making a basis for making rules unit testable.

Implementing new rules

Implementing new rules consists of two steps: Defining a pattern that one wish to replace and implementing the corresponding replacement logic. Inside a factory member function, this look as follows:

  auto get_element =
      Call("__quantum__rt__array_get_element_ptr_1d", "arrayName"_cap = _, "index"_cap = _);
  auto cast_pattern = BitCast("getElement"_cap = get_element);
  auto load_pattern = Load("cast"_cap = cast_pattern);

  addRule({std::move(load_pattern), access_replacer});

where addRule adds the rule to the current rule set.

Capturing patterns

The pattern defined in this snippet matches IR like:

  %0 = call i8* @__quantum__rt__array_get_element_ptr_1d(%Array* %leftPreshared, i64 0)
  %1 = bitcast i8* %0 to %Qubit**
  %2 = load %Qubit*, %Qubit** %1, align 8

In the above rule, the first and a second argument of __quantum__rt__array_get_element_ptr_1d is captured as arrayName and index, respectively. Likewise, the bitcast instruction is captured as cast. Each of these captures will be available inside the replacement function access_replacer.

Implementing replacement logic

After a positive match is found, the lead instruction alongside a IRBuilder, a capture table and a replacement table is passed to the replacement function. Here is an example on how one can access the captured variables to perform a transformation of the IR:

  auto access_replacer = [qubit_alloc_manager](Builder &builder, Value *val, Captures &cap,
                                               Replacements &replacements) {
    // ...
    auto cst = llvm::dyn_cast<llvm::ConstantInt>(cap["index"]);
    // ...
    auto llvm_size = cst->getValue();
    auto offset    = qubit_alloc_manager->getOffset(cap["arrayName"]->getName().str());

    auto idx = llvm::APInt(llvm_size.getBitWidth(), llvm_size.getZExtValue() + offset);
    auto new_index = llvm::ConstantInt::get(builder.getContext(), idx);
    auto instr = new llvm::IntToPtrInst(new_index, ptr_type);
    instr->takeName(val);

    // Replacing the lead instruction with a the new instruction
    replacements.push_back({llvm::dyn_cast<Instruction>(val), instr});

    // Deleting the getelement and cast operations
    replacements.push_back({llvm::dyn_cast<Instruction>(cap["getElement"]), nullptr});
    replacements.push_back({llvm::dyn_cast<Instruction>(cap["cast"]), nullptr});

    return true;
  };

@swernli
Copy link
Contributor

swernli commented Aug 15, 2021

@troelsfr I'm about halfway through the review, and I should be able to finish it before Monday morning your time so you have the chance to respond and we can get this ready to merge by EOD!

Copy link
Contributor

@swernli swernli left a comment

Choose a reason for hiding this comment

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

The changes mostly look good, just a few questions before I'm ready to sign off! There are also some suggestions about minor updates (file renames or comments) that you can either address here or in a follow up PR that we work on Monday to get the feature branch ready for sharing.

Copy link
Contributor

@swernli swernli left a comment

Choose a reason for hiding this comment

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

Thanks for addressing my questions! We should be ready to merge this once the builds pass.

/// are matched in order and by size.
void addChild(Child const& child);

/// Flags that this operand should be captured. This function ensures
Copy link
Contributor

Choose a reason for hiding this comment

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

The comment alone here is not sufficient to understand what the purpose is of this flag; what operands can and should be captured? What happens with captured operands? A see also reference to the piece of code that uses this information might help to clarify as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think what you possible need is some high level description of the OperandPrototype, its relation to creating rules and in that context. what captures are?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes.

Copy link
Contributor

@bettinaheim bettinaheim left a comment

Choose a reason for hiding this comment

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

I went over the code relatively quick, and I am sure there are places where I didn't look carefully enough. I did a quick read through everything, though.

@@ -1,156 +1,157 @@
# QIR Passes for LLVM
Copy link
Contributor

Choose a reason for hiding this comment

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

What was the reason for removing the general intro and links to the docs? Also the link in ## Out-of-source Pass might be useful to keep.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As such we can keep it as training material, but it does not belong at the top-level README as this file should contain the "user" documentation such as "What does the tool do", "How to build the tool", "How to run the tool" and "How do I create new profiles". Passes and the usage of passes is an implementation detail that only has relevance for those contributing to the implementation of the library.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd keep the info in the same place as any instructions for contributing to the passes.

@troels-im troels-im merged commit 700cdec into microsoft:features/llvm-passes Aug 16, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants