- About MLIR
- QMLIR Types
- Qubit Type
- QMLIR Operations
- Allocation
- Manipulation
- Measurement
- Transformation
- Examples
- Teleportation
- Deutsch-Josza
- Current features
- Simulation
- Lowering to LLVMIR
- Ideas for further work
- Register Allocation
- Choice of Universal Gate Set
- More Qubit Types
- Circuit Optimizations
The MLIR project is a novel approach to building reusable and extensible compiler infrastructure. MLIR aims to address software fragmentation, improve compilation for heterogeneous hardware, significantly reduce the cost of building domain specific compilers, and aid in connecting existing compilers together.
For the purpose of this presentation, the key features of MLIR that are used can be summarized as follows:
- Co-existing dialects, each introducing some operations and types.
- SSA based IR.
- Basic blocks of instructions, and Regions containing them.
Linear qubit arrays. Can have dynamic sizes.
qubit<10> // static
qubit<?> // dynamic
Allocate a qubit array of a given size.
// static size
%q = allocate() : qubit<10>
// dynamic size
// %n : index
%q = allocate(%n) : qubit<?>
// %q : qubit<4>
%q1, %q2 = split %q : qubit<4> -> (qubit<2>, qubit<2>)
%q3 = merge %q1, %q2 : (qubit<2>, qubit<2>) -> qubit<4>
Rationale: Providing array addressing to access qubits make dataflow analysis hard, and adds a few concerns:
- Operations like
CNOT(a, b)
could havea = b
, which is undesirable. - We cannot tell the range in which a qubit is live. Knowing live ranges makes register allocation simpler.
Therefore, we use the split-and-merge operations to manipulate the arrays.
And enforce a constraint: a qubit SSA can only be used once.
The cast
operation supports conversions between static and dynamic qubit arrays.
// %q0 : qubit<5>
%q1 = cast %q0 : qubit<5> to qubit<?>
The dim
operation is used to extract the size of a dynamic qubit array.
// %q0 : qubit<?>
%q1, %n = dim %q0 : qubit<?>
Measure (and deallocate) qubits in the standard bases (Pauli-X) and return an array of bits.
// %q0 : qubit<?>
%res = measure %q0 : qubit<?> -> memref<? x i1>
Currently, a simple gate set is supported - the Pauli gates, Hadamard, CNOT.
// %q0 : qubit<?>
%q1 = pauliX %q0 : qubit<?> // applies X on each qubit
%q2 = H %q1 : qubit<?> // applies H on each qubit
// %q0 : qubit<2>
%q1 = CNOT %q0 : qubit<2>
It also supports unregistered transformations - which can be converted by optimizations, or lowered based on target.
// %q0 : qubit<?>
%q1 = transform(...) %q0 : qubit<?> // pass floating point parameters
//// example
// %alpha : f32
%q1 = transform(%alpha : f32) { name = "Rx" } %q0 : qubit<?>
//// a controlled gate
// %qc : qubit<1> // control qubit
%q1 = controlled(%alpha : f32) { name = "C-Rx" } [qc : qubit<1>] %q0 : qubit<?>
func @std_to_bell(%qs: !quantum.qubit<2>) -> !quantum.qubit<2> {
// H(qs[0])
%q0, %q1 = quantum.split %qs : !quantum.qubit<2> -> (!quantum.qubit<1>, !quantum.qubit<1>)
%q2 = quantum.H %q0 : !quantum.qubit<1>
// CNOT(qs[0], qs[1])
%q3 = quantum.concat %q2, %q1 : (!quantum.qubit<1>, !quantum.qubit<1>) -> !quantum.qubit<2>
%q4 = quantum.CNOT %q3 : !quantum.qubit<2>
return %q4 : !quantum.qubit<2>
}
func @bell_to_std(%qs : !quantum.qubit<2>) -> !quantum.qubit<2> {
// CNOT(qs[0], qs[1])
%q0 = quantum.CNOT %qs : !quantum.qubit<2>
// H(qs[0])
%q1, %q2 = quantum.split %q0 : !quantum.qubit<2> -> (!quantum.qubit<1>, !quantum.qubit<1>)
%q3 = quantum.H %q1 : !quantum.qubit<1>
%q4 = quantum.concat %q3, %q2 : (!quantum.qubit<1>, !quantum.qubit<1>) -> !quantum.qubit<2>
return %q4 : !quantum.qubit<2>
}
func @teleport(%psiA: !quantum.qubit<1>, %eb: !quantum.qubit<2>) -> (!quantum.qubit<1>) {
%ebA, %psiB0 = quantum.split %eb : !quantum.qubit<2> -> (!quantum.qubit<1>, !quantum.qubit<1>)
// Alice's qubits
%qsA0 = quantum.concat %psiA, %ebA : (!quantum.qubit<1>, !quantum.qubit<1>) -> !quantum.qubit<2>
// Measure in Bell basis
%qsA1 = call @bell_to_std(%qsA0) : (!quantum.qubit<2>) -> !quantum.qubit<2>
%resA = quantum.measure %qsA1 : !quantum.qubit<2> -> memref<2xi1>
// Apply corrections
// 1. Apply X correction, if resA[0] == 1
%idx0 = constant 0 : index
%corrX = load %resA[%idx0] : memref<2xi1>
%psiB1 = scf.if %corrX -> !quantum.qubit<1> {
%temp = quantum.pauliX %psiB0 : !quantum.qubit<1>
scf.yield %temp : !quantum.qubit<1>
} else {
scf.yield %psiB0 : !quantum.qubit<1>
}
// 2. Apply Z correction, if resA[1] == 1
%idx1 = constant 1 : index
%corrZ = load %resA[%idx1] : memref<2xi1>
%psiB2 = scf.if %corrZ -> !quantum.qubit<1> {
%temp = quantum.pauliZ %psiB1 : !quantum.qubit<1>
scf.yield %temp : !quantum.qubit<1>
} else {
scf.yield %psiB1 : !quantum.qubit<1>
}
return %psiB2 : !quantum.qubit<1>
}
func @prepare_bell(%qa : !quantum.qubit<1>, %qb : !quantum.qubit<1>) -> !quantum.qubit<2> {
%q0 = quantum.concat %qa, %qb : (!quantum.qubit<1>, !quantum.qubit<1>) -> !quantum.qubit<2>
%q1 = call @std_to_bell(%q0) : (!quantum.qubit<2>) -> !quantum.qubit<2>
return %q1 : !quantum.qubit<2>
}
func @main() {
// Alice's qubits
%psiA = quantum.allocate() : !quantum.qubit<1>
%ebA = quantum.allocate() : !quantum.qubit<1>
// Bob's qubits
%ebB = quantum.allocate() : !quantum.qubit<1>
// Entangle the qubits
%eb = call @prepare_bell(%ebA, %ebB) : (!quantum.qubit<1>, !quantum.qubit<1>) -> !quantum.qubit<2>
// Teleport |psi> from Alice to Bob
%psiB = call @teleport(%psiA, %eb) : (!quantum.qubit<1>, !quantum.qubit<2>) -> !quantum.qubit<1>
return
}
// implements U|x⟩|y⟩ = |x⟩|y ⊕ f(x)⟩
func @oracle(%x : !quantum.qubit<?>, %y : !quantum.qubit<1>)
-> (!quantum.qubit<?>, !quantum.qubit<1>)
// implements U|x⟩ = (-1)^{f(x)} |x⟩
func @phase_flip_oracle(%x : !quantum.qubit<?>)
-> !quantum.qubit<?> {
%y0 = quantum.allocate() : !quantum.qubit<1>
%y1 = quantum.pauliX %y0 : !quantum.qubit<1>
%y2 = quantum.H %y1 : !quantum.qubit<1>
%x1, %y3 = call @oracle(%x, %y2)
: (!quantum.qubit<?>, !quantum.qubit<1>) -> (!quantum.qubit<?>, !quantum.qubit<1>)
%0 = quantum.measure %y3 : !quantum.qubit<1> -> memref<1xi1>
return %x1: !quantum.qubit<?>
}
// return false for constant, true for balanced
func @deutsch_josza(%n : index) -> i1 { // %n : number of input bits
%x0 = quantum.allocate(%n) : !quantum.qubit<?>
%x1 = quantum.H %x0 : !quantum.qubit<?>
%x2 = call @phase_flip_oracle(%x1) : (!quantum.qubit<?>) -> !quantum.qubit<?>
%x3 = quantum.H %x2 : !quantum.qubit<?>
%res = quantum.measure %x3 : !quantum.qubit<?> -> memref<?xi1>
// compute bitwise-OR of all the bits in %res
%false = constant 0 : i1
%0 = constant 0 : index
%1 = constant 1 : index
%lst = subi %n, %1 : index
%ans = scf.for %i = %0 to %lst step %1
iter_args(%out = %false) -> i1 {
%v = load %res[%i] : memref<?xi1>
%cur = or %out, %v : i1
scf.yield %cur : i1
}
return %ans : i1
}
Implemented a barebones simulation library which can handle around 10 qubits efficiently.
Only supports the basic gates mentioned above (X, Y, Z, CNOT, H
)
A pass currently can lower our dialect into the standard dialect, which is in-turn lowered into LLVMIR and can be executed (by calling simulator functions)
Can implement different register allocation schemes, based on existing literature.
A few constraints in mind:
- Hardware specific restrictions
- Multiqubit gates on nearby qubits
- Only supports certain multiqubit gates
- Allocation/Deallocation strategies
- Currently: Deallocate on measure
- Multiple works on different universal gates. Can see which works best with our framework.
- Conversions between different sets.
Google Cirq has a few different processors with different layouts. Maybe we can factor that into our qubit type representation.
Similar to Cirq, can implement various circuit/gate optimizations and rewriting.
One idea was to implement gate factoring: break down a large multiqubit gate into smaller (fundamental) gates.