You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Guppy is a conscious deviation from the traditional circuit builder pattern of constructing quantum programs, towards source level programming. With that comes traditional source level assumptions about dynamism and runtime capabilities. Guppy works best when the runtime of the compiled program behaves as much like a traditional classical runtime as possible.
However, quite often that is not desired. Quantum resources are expensive (both in cost and more importantly quantum error) and classical are cheap, so we are incentivised to perform as much classical computation ahead of time as possible. This could be understood in compiler tradition as constant propagation, dead code elimination, branch flattening, loop unrolling etc. This picture takes a program which can work at runtime, and tries to make it do less - i.e. it is a fully compiler problem. Let's call this the "static evaluation" approach.
However, an alternative way of looking at it is to think how can we let the programmer request as much compile-time computation as possible. Typically we would think of this as metaprogramming, and it comes for free with the traditional builder pattern. There, you use the host language to perform any compile time logic, outputting a program containing only the logic you need executed at runtime. For guppy quantum programs you could think of this as programs containing only quantum operations and logic/control flow that depends on quantum measurement outcomes.
Guppy already includes a number of features to make the compiler's life easier for static evaluation. The most important is fixed size arrays and functions generic over array size. This allows two important things:
Qubit usage known at compile time
Avoiding heap allocation at runtime
From the perspective of someone used to building circuits in python (e.g. with pytket or qiskit), this is somewhat bizarre, because they always know their register sizes at build time so can just have normal functions that are parametric not generic over register size. They can use ergonomic structures like lists, resize them as necessary and end up with a program with the same desired properties. The same can be said of control flow - say the desired outcome is a gate applied to a set of qubits, in the compiled program you want it to be a flat program with each gate written out, but it is much more convenient to write:
But the resulting program now contains a runtime loop (unless the compiler does loop unrolling, which the user would have to request or assume separately to their programming).
So what?
The existing design is suitable for the launch of core Guppy functionality. But looking further afield, I think we should take metaprogramming more seriously, especially since we already naturally have the host language of Python in which to do it. The below are some ideas as to how to go about it, to kick start discussion only.
1. Just build the output
Guppy compiles to HUGR. The output is a HUGR python object. This is a fairly easy object to handle, and is designed to be easy to write Python to build and inspect it. So one approach is to shift the "metaprogramming" problem to the HUGR builder, and only use Guppy to build sub-sections more conveniently. So taking the example above but assuming h is some non-trivial physical implementation of a hadamard
The downsides here are somewhat obvious: the user has to learn two new formalisms, and deal with any impedance mismatch wherever they interact.
2. Tracing
This involves maintaining some build state and letting the Python execution modify that state, such that at the end you can extract a built program. This was the approach taken by the Tierkreis builder, and the pain of working with that was much of the reason for moving to explicit source compilation in Guppy. However, there is a key difference with guppy that may save us. With the Tierkreis builder it needed to express all runtime control flow along side python control flow, this is what led to much of the confusion. Guppy does a perfectly good job of expressing runtime control flow well, so the tracing system need only worry about building flat programs.
This then naturally leads to the question of which is the default context (compiled or traced) and how to switch between them. Via its embedding in Python, Guppy already has this outer host context and an inner compiled one. To add tracing we face adding complexity by having deeper nestings.
One option is to introduce a new function decorator, say @guppy_traced that changes the default context to a traced one, and allows calling compiled functions internally. So for example:
@guppy_traceddeftransversal_h(qs: array[qubit, N]):
foriinrange(N): # a normal python for looplogical_h(qs[i]) # guppy function call gets wired in per loop iteration
From here it becomes even more powerful once you can start nesting contexts
@guppy_traceddeftransversal_h(qs: array[qubit, N]):
foriinrange(N): # a normal python for loopwithguppy.source():
# inline definition of non-trivial logical_h circuit# needs to be able to refer to `qs` from outer traced scope
Note there are many details to be worked out here about how guppy values interact with traced wires, matching signatures, etc.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
-
Guppy is a conscious deviation from the traditional circuit builder pattern of constructing quantum programs, towards source level programming. With that comes traditional source level assumptions about dynamism and runtime capabilities. Guppy works best when the runtime of the compiled program behaves as much like a traditional classical runtime as possible.
However, quite often that is not desired. Quantum resources are expensive (both in cost and more importantly quantum error) and classical are cheap, so we are incentivised to perform as much classical computation ahead of time as possible. This could be understood in compiler tradition as constant propagation, dead code elimination, branch flattening, loop unrolling etc. This picture takes a program which can work at runtime, and tries to make it do less - i.e. it is a fully compiler problem. Let's call this the "static evaluation" approach.
However, an alternative way of looking at it is to think how can we let the programmer request as much compile-time computation as possible. Typically we would think of this as metaprogramming, and it comes for free with the traditional builder pattern. There, you use the host language to perform any compile time logic, outputting a program containing only the logic you need executed at runtime. For guppy quantum programs you could think of this as programs containing only quantum operations and logic/control flow that depends on quantum measurement outcomes.
Guppy already includes a number of features to make the compiler's life easier for static evaluation. The most important is fixed size arrays and functions generic over array size. This allows two important things:
From the perspective of someone used to building circuits in python (e.g. with pytket or qiskit), this is somewhat bizarre, because they always know their register sizes at build time so can just have normal functions that are parametric not generic over register size. They can use ergonomic structures like lists, resize them as necessary and end up with a program with the same desired properties. The same can be said of control flow - say the desired outcome is a gate applied to a set of qubits, in the compiled program you want it to be a flat program with each gate written out, but it is much more convenient to write:
In Guppy you would naturally think to write:
But the resulting program now contains a runtime loop (unless the compiler does loop unrolling, which the user would have to request or assume separately to their programming).
So what?
The existing design is suitable for the launch of core Guppy functionality. But looking further afield, I think we should take metaprogramming more seriously, especially since we already naturally have the host language of Python in which to do it. The below are some ideas as to how to go about it, to kick start discussion only.
1. Just build the output
Guppy compiles to HUGR. The output is a HUGR python object. This is a fairly easy object to handle, and is designed to be easy to write Python to build and inspect it. So one approach is to shift the "metaprogramming" problem to the HUGR builder, and only use Guppy to build sub-sections more conveniently. So taking the example above but assuming
h
is some non-trivial physical implementation of a hadamardThe downsides here are somewhat obvious: the user has to learn two new formalisms, and deal with any impedance mismatch wherever they interact.
2. Tracing
This involves maintaining some build state and letting the Python execution modify that state, such that at the end you can extract a built program. This was the approach taken by the Tierkreis builder, and the pain of working with that was much of the reason for moving to explicit source compilation in Guppy. However, there is a key difference with guppy that may save us. With the Tierkreis builder it needed to express all runtime control flow along side python control flow, this is what led to much of the confusion. Guppy does a perfectly good job of expressing runtime control flow well, so the tracing system need only worry about building flat programs.
This then naturally leads to the question of which is the default context (compiled or traced) and how to switch between them. Via its embedding in Python, Guppy already has this outer host context and an inner compiled one. To add tracing we face adding complexity by having deeper nestings.
One option is to introduce a new function decorator, say
@guppy_traced
that changes the default context to a traced one, and allows calling compiled functions internally. So for example:From here it becomes even more powerful once you can start nesting contexts
Note there are many details to be worked out here about how guppy values interact with traced wires, matching signatures, etc.
Beta Was this translation helpful? Give feedback.
All reactions