Support for loops
Opened this issue · 5 comments
Is your feature request related to a problem? Please describe.
When a for
loop in PyQIR is encountered, the resulting QIR that's emitted is effectively the loop unrolled equivalent of that for
loop. Is there a way to use the branching infrastructure in LLVM to reduce the size of the resulting .ll
file? I'm not sure if this is physically realizable on a QPU (I'm very new to this space), so forgive me if this is a naive question.
Describe the solution you had in mind
It seems like most of the component pieces for a solution (in software) exist:
- you have the example
if_bool.py
example for executing different basic blocks based on some condition - a stub for implementing a
phi
node
I don't know anything about Rust, so I don't understand the function signatures for implementations, but it feels like the infrastructure for a solution is there.
Additional context
Maybe @wongey or @ausbin might be able to provide additional context/correct some things that I wrote.
This bit of LLVM IR doesn't do anything useful, but this is something I rolled by hand and is what I had in mind. Also, it works in this build of qir-runner
; ModuleID = 'qpe'
source_filename = "qpe"
%Qubit = type opaque
define void @main() #0 {
entry:
br label %loop0
loop0:
%i = phi i64 [ 0, %entry ], [ %nextval0, %loop0 ]
%tmp = inttoptr i64 %i to %Qubit*
call void @__quantum__qis__h__body(%Qubit* %tmp)
%nextval0 = add i64 %i, 1
%endcond0 = icmp ult i64 %i, 3
%loopcond0 = icmp ne i1 %endcond0, 0
br i1 %loopcond0, label %loop0, label %afterloop0
afterloop0:
ret void
}
declare void @__quantum__qis__h__body(%Qubit*)
attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom" "required_num_qubits"="4" "required_num_results"="3" }
!llvm.module.flags = !{!0, !1, !2, !3}
!0 = !{i32 1, !"qir_major_version", i32 1}
!1 = !{i32 7, !"qir_minor_version", i32 0}
!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
!3 = !{i32 1, !"dynamic_result_management", i1 false}
This is the LLVM IR (generated from PyQIR) that I based that off of:
; ModuleID = 'qpe'
source_filename = "qpe"
%Qubit = type opaque
define void @main() #0 {
entry:
call void @__quantum__qis__h__body(%Qubit* null)
call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 1 to %Qubit*))
call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 2 to %Qubit*))
ret void
}
declare void @__quantum__qis__h__body(%Qubit*)
attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom" "required_num_qubits"="4" "required_num_results"="3" }
!llvm.module.flags = !{!0, !1, !2, !3}
!0 = !{i32 1, !"qir_major_version", i32 1}
!1 = !{i32 7, !"qir_minor_version", i32 0}
!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
!3 = !{i32 1, !"dynamic_result_management", i1 false}
I hope #279 is somewhat helpful.
Here's an example of generating an n-qubit GHZ state:
#!/usr/bin/env python3
# Copyright (c) Austin J. Adams
# Licensed under CC-0
import pyqir
import pyqir.rt
num_qubits = 10
context = pyqir.Context()
builder = pyqir.Builder(context)
mod = pyqir.qir_module(context, "ghz")
entry_point = pyqir.entry_point(mod, "kernel", num_qubits, num_qubits)
header = pyqir.BasicBlock(context, "header", entry_point)
cond = pyqir.BasicBlock(context, "cond", entry_point)
body = pyqir.BasicBlock(context, "body", entry_point)
footer = pyqir.BasicBlock(context, "footer", entry_point)
builder.insert_at_end(header)
nullptr = pyqir.Constant.null(pyqir.PointerType(pyqir.IntType(context, 8)))
pyqir.rt.initialize(builder, nullptr)
pyqir.qis.h(builder, pyqir.qubit(context, 0))
builder.br(cond)
builder.insert_at_end(cond)
i64 = pyqir.IntType(mod.context, 64)
phi = builder.phi(i64)
zero_const = pyqir.const(i64, 0)
phi.add_incoming(zero_const, header)
num_qubits_const = pyqir.const(i64, num_qubits)
ub = builder.sub(num_qubits_const, pyqir.const(i64, 1))
icmp = builder.icmp(pyqir.IntPredicate.ULT, phi, ub)
builder.condbr(icmp, body, footer)
builder.insert_at_end(body)
one_const = pyqir.const(i64, 1)
incr = builder.add(phi, one_const)
pyqir.qis.cx(builder, builder.dyn_qubit(phi), builder.dyn_qubit(incr))
builder.br(cond)
phi.add_incoming(incr, body)
builder.insert_at_end(footer)
for i in range(num_qubits):
pyqir.qis.mz(builder, pyqir.qubit(context, i), pyqir.result(context, i))
pyqir.rt.tuple_record_output(builder, num_qubits_const, nullptr)
for i in range(num_qubits):
pyqir.rt.result_record_output(builder, pyqir.result(context, i), nullptr)
builder.ret()
mod.verify()
if __name__ == "__main__":
print(str(mod))
which generates
; ModuleID = 'ghz'
source_filename = "ghz"
%Qubit = type opaque
%Result = type opaque
define void @kernel() #0 {
header:
call void @__quantum__rt__initialize(i8* null)
call void @__quantum__qis__h__body(%Qubit* null)
br label %cond
cond: ; preds = %body, %header
%0 = phi i64 [ 0, %header ], [ %2, %body ]
%1 = icmp ult i64 %0, 9
br i1 %1, label %body, label %footer
body: ; preds = %cond
%2 = add i64 %0, 1
%3 = inttoptr i64 %0 to %Qubit*
%4 = inttoptr i64 %2 to %Qubit*
call void @__quantum__qis__cnot__body(%Qubit* %3, %Qubit* %4)
br label %cond
footer: ; preds = %cond
call void @__quantum__qis__mz__body(%Qubit* null, %Result* null)
call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Result* inttoptr (i64 1 to %Result*))
call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 2 to %Qubit*), %Result* inttoptr (i64 2 to %Result*))
call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 3 to %Qubit*), %Result* inttoptr (i64 3 to %Result*))
call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 4 to %Qubit*), %Result* inttoptr (i64 4 to %Result*))
call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 5 to %Qubit*), %Result* inttoptr (i64 5 to %Result*))
call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 6 to %Qubit*), %Result* inttoptr (i64 6 to %Result*))
call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 7 to %Qubit*), %Result* inttoptr (i64 7 to %Result*))
call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 8 to %Qubit*), %Result* inttoptr (i64 8 to %Result*))
call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 9 to %Qubit*), %Result* inttoptr (i64 9 to %Result*))
call void @__quantum__rt__tuple_record_output(i64 10, i8* null)
call void @__quantum__rt__result_record_output(%Result* null, i8* null)
call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 1 to %Result*), i8* null)
call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 2 to %Result*), i8* null)
call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 3 to %Result*), i8* null)
call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 4 to %Result*), i8* null)
call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 5 to %Result*), i8* null)
call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 6 to %Result*), i8* null)
call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 7 to %Result*), i8* null)
call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 8 to %Result*), i8* null)
call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 9 to %Result*), i8* null)
ret void
}
declare void @__quantum__rt__initialize(i8*)
declare void @__quantum__qis__h__body(%Qubit*)
declare void @__quantum__qis__cnot__body(%Qubit*, %Qubit*)
declare void @__quantum__qis__mz__body(%Qubit*, %Result* writeonly) #1
declare void @__quantum__rt__tuple_record_output(i64, i8*)
declare void @__quantum__rt__result_record_output(%Result*, i8*)
attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom" "required_num_qubits"="10" "required_num_results"="10" }
attributes #1 = { "irreversible" }
!llvm.module.flags = !{!0, !1, !2, !3}
!0 = !{i32 1, !"qir_major_version", i32 1}
!1 = !{i32 7, !"qir_minor_version", i32 0}
!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
!3 = !{i32 1, !"dynamic_result_management", i1 false}
qir-runner spits out all 0s or all 1s as you'd expect.
Of course, this is not the perfect experience from a programmer perspective. You're basically just using IRBuilder through many complex layers of abstraction at this point. Some syntax like this (like in qiskit.qasm3
) would be nice:
with builder.for_(pyqir.const(i64, 0), pyqir.const(i64, num_qubits)) as i:
incr = builder.add(i, pyqir.const(i64, 1))
pyqir.qis.cx(builder, builder.dyn_qubit(i), builder.dyn_qubit(incr))
I can look into that if you're interested.
#279 is very helpful, thanks for looking into this!
Apologies to the PyQIR devs; I didn't realize that part of my issue had already been raised in #242.
I agree with @ausbin though.
Of course, this is not the perfect experience from a programmer perspective. You're basically just using IRBuilder through many complex layers of abstraction at this point. Some syntax like this (like in
qiskit.qasm3
) would be nice:with builder.for_(pyqir.const(i64, 0), pyqir.const(i64, num_qubits)) as i: incr = builder.add(i, pyqir.const(i64, 1)) pyqir.qis.cx(builder, builder.dyn_qubit(i), builder.dyn_qubit(incr))
It'd be nice to have a more user-friendly method of capturing loops, but I'll leave it up to @ausbin to decide if they find it meaningful to add. Or at least wait until after #279 gets merged, because it seems like that'd be a major component of a PR toward more user-friendly loop generation.