qir-alliance/pyqir

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:

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.

I've created a tracking issue #280 which gives the high-level list of items needed to be able to generation adaptive profile QIR which includes backwards branching (loops).

Thank you. @idavis!