Commit 64588684 authored by Mccaskey, Alex's avatar Mccaskey, Alex
Browse files

adding qsc demo


Signed-off-by: Mccaskey, Alex's avatarAlex McCaskey <mccaskeyaj@ornl.gov>
parent 8744218d
Pipeline #168465 passed with stage
in 32 minutes and 42 seconds
# MLIR+QIR as an IR for Quantum-Classical Computing
Here we demonstrate the utility of the QIR and MLIR for enabling the development of compilers for available quantum languages.
Our motivation with this demonstration is to show how MLIR+QIR enable the main feature of a robust IR: mapping multiple languages or programming approaches to multiple backends.
Our examples will be simple GHZ and Bell circuits for NISQ and FTQC execution, respectively.
## Goals
- Demonstrate the utility of the MLIR and QIR for creating compilers and executable code for available quantum languages.
- Demonstrate MLIR as language-level IR for quantum-classical computing (control flow from Standard/Affine, etc.).
- Demonstrate write-once, run-on-any available quantum backend and multiple languages to multiple backends.
- Demonstrate accessibility of MLIR and QIR for available Pythonic circuit construction frameworks.
## NOTES
Poor man's way to count instructions in the LLVM output
```bash
qcor --emit-llvm bell.qasm -O3 &> bell_tmp.ll && cat bell_tmp.ll | grep '%* = \|call' | wc -l && rm bell_tmp.ll
```
## Outline
### Demo 1, Simple Language Lowering, Helpful Classical Passes
- Show off the code in `ghz.qasm`. Note the gate function.
- Goal is to highlight the unique benefits of mutliple levels of IR abstraction
- Compile and run, note we want to run this in NISQ mode
```bash
qcor -qrt nisq -shots 1000 ghz.qasm -o ghz.x
./ghz.x
```
- Run the compile command with `-v` to show the steps in the workflow
```bash
qcor -v ghz.qasm
```
- Show the MLIR and QIR Output, noting the Affine Loop (and that we benefit from leveraging classical IR Dialects)
```bash
qcor --emit-mlir ghz.qasm
qcor --emit-llvm ghz.qasm
```
- Show simple classical optimizations, here inlining and loop unrolling
```bash
qcor --emit-milr -O3 ghz.qasm
qcor --emit-llvm -O3 ghz.qasm
```
- Show off the FTQC mode code, noting that the for loop has a conditional statement
- Compile and run with -v
```bash
qcor -qrt ftqc -v bell.qasm -o bell.x
./bell.x
```
- Show the MLIR and QIR
```bash
qcor --emit-mlir bell.qasm
qcor --emit-llvm bell.qasm
```
- Note the need for multiple layers of IR, can't get opts from MLIR, but can from LLVM
```bash
qcor --emit-mlir -O3 bell.qasm
qcor --emit-llvm -O3 llvm.qasm
qcor --emit-llvm bell.qasm -O0 &> bell_tmp.ll && cat bell_tmp.ll | grep '%* = \|call' | wc -l && rm bell_tmp.ll
qcor --emit-llvm bell.qasm -O3 &> bell_tmp.ll && cat bell_tmp.ll | grep '%* = \|call' | wc -l && rm bell_tmp.ll
```
- Switch to the Python script to show the audience how all the MLIR/QIR infrastructure just shown can also be generated from Python. Moreover, that Qiskit and Pyquil can also generate MLIR/QIR.
- Note how `qjit` is the QCOR quantum just-in-time compiler, produces executable functions that enable `mlir()` and `llvm()` methods.
// qcor bell.qasm -o bell.x
// ./bell.x
//
// qcor -v bell.qasm -o bell.x
// qcor --emit-mlir bell.qasm
// qcor --emit-llvm bell.qasm
OPENQASM 3;
qubit q[2];
const shots = 15;
int count = 0;
for i in [0:15] {
// Create Bell state
h q[0];
cnot q[0], q[1];
// Measure and assert both are equal
bit c[2];
c = measure q;
if (c[0] == c[1]) {
print("iter", i, ": measured =", c[0], c[1]);
count += 1;
}
reset q;
}
print("count is", count);
// NISQ Mode Execution
//
// Compile and run on qpp
// qcor ghz.qasm -o ghz.x -qrt nisq -shots 1000
// ./ghz.x
//
// Show on local aer with noisy backend
// qcor ghz.qasm -o ghz.x -qrt nisq -shots 100 -qpu aer:ibmq_sydney
// ./ghz.x
//
// (Now Show off how this works, MLIR + QIR)
//
// qcor -v ghz.qasm -o ghz.x
// qcor --emit-mlir ghz.qasm
// qcor --emit-llvm ghz.qasm
OPENQASM 3;
const n_qubits = 3;
qubit q[n_qubits];
gate ctrl_x a, b {
ctrl @ x a, b;
}
h q[0];
for i in [0:n_qubits-1] {
ctrl_x q[i], q[i+1];
}
bit c[n_qubits];
c = measure q;
\ No newline at end of file
from qcor import qjit, qalloc
import qiskit
# Generate 3-qubit GHZ state with Qiskit
circ = qiskit.QuantumCircuit(3)
circ.h(0)
circ.cx(0, 1)
circ.cx(1, 2)
circ.measure_all()
# Creates a kernel parameterized on a qreg
qcor_kernel = qjit(circ)
# Allocate the qreg
q = qalloc(3)
# Convert to MLIR and print
mlir = qcor_kernel.mlir(q)
print(mlir)
# Convert to QIR and print
qir = qcor_kernel.qir(q, opt=3)
print(qir)
from pyquil import Program
from pyquil.gates import CNOT, H, MEASURE
p = Program()
p += H(0)
p += CNOT(0, 1)
ro = p.declare('ro', 'BIT', 2)
p += MEASURE(0, ro[0])
p += MEASURE(1, ro[1])
# This requires rigetti/quilc docker image
qcor_kernel_pyquil = qjit(p, opt=3)
r = qalloc(2)
# Convert to MLIR and print
mlir = qcor_kernel_pyquil.mlir(r)
print(mlir)
# Convert to QIR and print
qir = qcor_kernel_pyquil.qir(r)
print(qir)
\ No newline at end of file
# MLIR as an IR enabling Quantum Optimizations
Here we demonstrate the utility of the MLIR for quantum compiler optimization passes. The specific goal is to highlight the need for an SSA-based IR for quantum computing. We want to demonstrate select common quantum optimizations and how the MLIR enables their implementation.
## Goals
- Demonstrate the benefits of an SSA-based IR for quantum computing.
- Demonstrate common quantum optimizations (ID pairs, rotation merging, single-qubit gate merge, inlining, permute and cancel)
- Demonstrate classical optimizations that enable new quantum optimizations (inlining and loop unrolling)
## Outline
1. Start with `simple_opts.qasm`, walk through the various sections showing different code patterns that could be optimized (removed fully, or just reduced). Note what should remain after full optimization
2. Emit the unoptimized MLIR, note that all the quantum instructions are there (when they really do nothing).
3. Show the optimized MLIR, note how it was able to reduce to a couple instructions.
4. Show the unoptimized LLVM/QIR
5. Show the optimized LLVM/QIR.
6. Use `--pass-timing` to show exactly what was run.
7. Move on to `trotter_unroll_simple.qasm` and note the `for` loop will naively place ID pairs next to each other. Show the unoptimized MLIR, noting the `affine.for` loop.
8. Show the optimized MLIR, noting the `affine.for` is gone, and the compiler has optimized the code to the `for` loop body.
9. Show the same for unoptimized and optimized LLVM.
10. Open `trotter_unroll_with_inlining.qasm` to demonstrate a bit more complexity, and how inlining+ loop unrolling can work together to further optimized the code. Highlight the necessity of any Quantum IR to support both classical and quantum optimizations.
11. Go through the same workflow, unoptimized/optimized MLIR, unoptimized/optimized QIR.
12. Show `-print-final-submission` for unoptimized and optimized.
## Notes:
// Show off SSA-based quantum optimizations
//
// Show unoptimized
// qcor --emit-mlir simple_opts.qasm
//
// Show optimized mlir
// qcor --emit-mlir --q-optimize simple_opts.qasm
//
// Show unoptimized LLVM/QIR
// qcor --emit-llvm -O0 simple_opts.qasm
//
// Show optimized LLVM/QIR
// qcor --emit-llvm -O3 simple_opts.qasm
//
// Show the passes run and timing
// (note the passes run, heuristic n_runs == 5 to pick up opts
// generated by previous passes)
// qcor --emit-mlir --q-optimize simple_opts.qasm --pass-timing
OPENQASM 3;
// ID Pair Removal
qubit a[2];
x a[0];
x a[0];
cx a[0], a[1];
cx a[0], a[1];
// Rotation merging
qubit b[2];
// Merge X-Rx and Z-Rz
x b[0];
z b[1];
rx(1.2345) b[0];
rz(2.4566) b[1];
// Single qubit merge
qubit c;
h c;
z c;
h c;
// Inlining and Removal
gate oracle q_arg {
x q_arg;
}
qubit d;
x d;
oracle d;
// Permute and Cancel
qubit e[2];
// Can permute, Rz's cancel, CX then cancels
rz(0.123) e[0];
cx e[0], e[1];
rz(-0.123) e[0];
cx e[0], e[1];
// Result should only have Rx(pi+1.2345), Rz(pi+2.4566), and Rx(pi)
\ No newline at end of file
// Show off quantum optimizations induced by Loop Unrolling
//
// Show unoptimized (note the affine.for)
// qcor --emit-mlir trotter_unroll_simple.qasm
//
// Show optimized mlir (note affine.for removed)
// qcor --emit-mlir --q-optimize simple_opts.qasm
//
// Show unoptimized LLVM/QIR
// qcor --emit-llvm -O0 simple_opts.qasm
//
// Show optimized LLVM/QIR
// qcor --emit-llvm -O3 simple_opts.qasm
OPENQASM 3;
qubit qq[2];
for i in [0:100] {
h qq;
cx qq[0], qq[1];
rx(0.0123) qq[1];
cx qq[0], qq[1];
h qq;
}
\ No newline at end of file
// Show off quantum optimizations induced by Loop Unrolling + Inlining
//
// Show unoptimized (note the affine.for)
// qcor --emit-mlir trotter_unroll_with_inlining.qasm
//
// Show optimized mlir (note affine.for removed)
// qcor --emit-mlir trotter_unroll_with_inlining.qasm -O3
//
// Show unoptimized LLVM/QIR
// qcor --emit-llvm -O0 simple_opts.qasm
//
// Show optimized LLVM/QIR
// qcor --emit-llvm -O3 simple_opts.qasm
OPENQASM 3;
def cnot_ladder() qubit[4]:q {
h q[0];
h q[1];
cx q[0], q[1];
cx q[1], q[2];
cx q[2], q[3];
}
def cnot_ladder_inv() qubit[4]:q {
cx q[2], q[3];
cx q[1], q[2];
cx q[0], q[1];
h q[1];
h q[0];
}
qubit q[4];
double theta = 0.01;
for i in [0:100] {
cnot_ladder q;
rz(theta) q[3];
cnot_ladder_inv q;
}
\ No newline at end of file
namespace QCOR
{
open Microsoft.Quantum.Intrinsic;
open Microsoft.Quantum.Convert;
open Microsoft.Quantum.Canon;
operation SWAP(q1 : Qubit, q2: Qubit) : Unit is Adj {
CNOT(q1, q2);
CNOT(q2, q1);
CNOT(q1, q2);
}
operation IQFT(qq: Qubit[]): Unit {
Message("Hello from Q# IQFT");
for i in 0 .. Length(qq)/2 - 1 {
SWAP(qq[i], qq[Length(qq)-i-1]);
}
for i in 0 .. Length(qq) - 2 {
H(qq[i]);
let j = i + 1;
mutable y = i;
repeat {
let theta = -3.14159 / IntAsDouble(1 <<< (j-y));
// Controlled R1 == CPhase
Controlled R1([qq[j]], (theta, qq[y]));
set y = y - 1;
} until (y < 0);
}
H(qq[Length(qq) -1]);
}
}
\ No newline at end of file
#include "qir_nisq_kernel_utils.hpp"
// Compile:
// qcor qft.qs qpe.cpp -shots 1024 -print-final-submission
qcor_import_qsharp_kernel(QCOR__IQFT);
// Typedef for the Oracle Kernel Function
using QPEOracleSignature = KernelSignature<qubit>;
__qpu__ void qpe(qreg q, QPEOracleSignature oracle) {
// Extract the counting qubits and the state qubit
auto counting_qubits = q.extract_range({0,3});
auto state_qubit = q[3];
// Put it in |1> eigenstate
X(state_qubit);
// Create uniform superposition on all 3 qubits
H(counting_qubits);
// run ctr-oracle operations
for (auto i : range(counting_qubits.size())) {
const int nbCalls = 1 << i;
for (auto j : range(nbCalls)) {
oracle.ctrl(counting_qubits[i], state_qubit);
}
}
// Run Inverse QFT on counting qubits
// Using the Q# Kernel (wrapped as a QCOR kernel)
QCOR__IQFT(counting_qubits);
// Measure the counting qubits
Measure(counting_qubits);
}
// Oracle to consider
__qpu__ void oracle(qubit q) { T(q); }
int main(int argc, char **argv) {
auto q = qalloc(4);
qpe::print_kernel(q, oracle);
// Run
qpe(q, oracle);
q.print();
}
OPENQASM 3;
// QCOR can scan for this and make
// sure MLIRGen does not add main()
// This is useful for QASM3 files that are
// purely library functions
#pragma no_entrypoint;
// Inverse QFT subroutine on n_counting qubits
def inverse_qft(int[64]:nc) qubit[nc]:qq {
for i in [0:nc/2] {
swap qq[i], qq[nc-i-1];
}
for i in [0:nc-1] {
h qq[i];
int j = i + 1;
int y = i;
while (y >= 0) {
double theta = -pi / (2^(j-y));
cphase(theta) qq[j], qq[y];
y -= 1;
}
}
h qq[nc-1];
}
\ No newline at end of file
#include "qir_nisq_kernel_utils.hpp"
// Compile:
// qcor iqft.qasm qpe.cpp -shots 1024
using QPEOracleSignature = KernelSignature<qubit>;
// External QASM3 function with signature void(qreg, int)
// All imported kernels assumed to take qreg as first arg
qcor_import_qasm3_kernel(inverse_qft, int);
__qpu__ void qpe(qreg q, QPEOracleSignature oracle) {
// Extract the counting qubits and the state qubit
auto counting_qubits = q.extract_range({0,3});
auto state_qubit = q[3];
// Put it in |1> eigenstate
X(state_qubit);
// Create uniform superposition on all 3 qubits
H(counting_qubits);
// run ctr-oracle operations
for (auto i : range(counting_qubits.size())) {
const int nbCalls = 1 << i;
for (auto j : range(nbCalls)) {
oracle.ctrl(counting_qubits[i], state_qubit);
}
}
// Run Inverse QFT on counting qubits
// Using the Q# Kernel (wrapped as a QCOR kernel)
// Signature is void(qreg, int) as per above import stmt
inverse_qft(counting_qubits, counting_qubits.size());
// Measure the counting qubits
Measure(counting_qubits);
}
// Oracle to consider
__qpu__ void oracle(qubit q) { T(q); }
int main(int argc, char **argv) {
auto q = qalloc(4);
qpe::print_kernel(q, oracle);
// Run
qpe(q, oracle);
q.print();
}
// qcor -qdk-version 0.17.2106148041-alpha qrng.qs driver.qasm
// for i in {1..10} ; do ./a.out ; done
OPENQASM 3;
// Declare kernel:
// This one is from Q#...
kernel QCOR__GenerateRandomInt__body(int[64]) -> int64_t;
// Generate the random number (4 bits)
int64_t max_bits = 4;
int64_t n = QCOR__GenerateRandomInt__body(max_bits);
// Print the random number
print("[ OpenQASM3 ] Random", max_bits, "bit int = ", n);
\ No newline at end of file
namespace QCOR
{
open Microsoft.Quantum.Intrinsic;
operation GenerateRandomInt(maxBits: Int): Int {
mutable rngNumber = 0;
use qubit = Qubit()
{
for idx in 1..maxBits {
H(qubit);
if (M(qubit) == One) {
set rngNumber = rngNumber + (1 <<< (idx - 1));
// Reset
X(qubit);
}
}
}
Message($"Random number from Q# = {rngNumber}");
return rngNumber;
}
}
// Driver to tie them all together:
// - Declare the exported QASM3 callable
// - Use that as input to the Q# operation
// Compile with:
// qcor -qrt ftqc op_takes_callable.qs kernel.qasm driver.cpp -v
// Note: this driver can be part of QASM3 as well, but
// we don't have the concept of Callable type in QASM3 yet,
// hence, we cannot declare the Q# kernel taking Callable argumements as extern
// in QASM3 yet.
#include "qir-types.hpp"
// QASM3 function wrapping the quantum sub-routine as a QIR Callable
extern "C" ::Callable* qasm_x__callable();
// Q# functions:
// Apply Controlled version of a Callable (X gate in QASM3)
// This will just be a Bell experiment.
extern "C" void QCOR__ApplyControlledKernel__body(::Callable *);
int main() {
// Get the callable (QASM3)
auto qasm3_callable = qasm_x__callable();
// Pass it to Q#
// std::cout << "Apply the functor to each qubit:\n";
// QCOR__ApplyKernelToEachQubit__body(qasm3_callable);
// Run Bell experiment:
constexpr int COUNT = 50;
std::cout << "Apply controlled functor(Bell test):\n";
for (int i = 0; i < COUNT; ++i) {
std::cout << "Run " << i + 1 << ":\n";
QCOR__ApplyControlledKernel__body(qasm3_callable);
}
return 0;
}
\ No newline at end of file
#pragma no_entrypoint;
OPENQASM 3;
#pragma export;
def qasm_x qubit:qb {
print("Hello from QASM3!");
x qb;
}