qcor
qcor copied to clipboard
[rfc] Support for writing kernels as methods of a class
Summary
Allow writing kernels as methods of a class.
class TestClass {
__qpu__ void test_kernel(qreg a, qreg b) { .. }
__qpu__ virtual void test_abstract_kernel(qreg a);
}
Example: Implementing Grover oracles
Grover's algorithm requires an oracle to perform its iterations. However, each oracle varies in the number of inputs it needs. This means that we would need to implement a grover function for each oracle that varies in its kernel signature.
For example, consider the following two oracle signatures:
using Oracle1 = KernelSignature<qreg>;
using Oracle2 = KernelSignature<qreg, qreg>;
We cannot write trivially write a generic grover kernel that works on both of these kernels using only functions. We would need separate grover kernels for each oracle.
__qpu__ void grover_1(qreg input, Oracle1 oracle) { .. }
__qpu__ void grover_2(qreg input1, qreg input2, Oracle2 oracle) { .. }
This leads to duplication of code. The problem exacerbates if grover uses any internal functions (for modularity and separation of concerns sake), because we will need to have separate implementations of those functions two.
We could use variadic function arguments to implement it. However, we lose some of the guarantees that typechecking offers.
C++ classes, however, provide the right abstraction for the task. We can have a Oracle
abstract base class. Concrete oracles with extend and implement its interface.
class Oracle {
__qpu__ virtual void apply(qreg register, qubit target);
}
class VertexColoringOracle : Oracle {
private:
// oracle-specific internal state
qreg color_assignments;
qreg edge_conflict_qubits;
vector<tuple<int, int>> starting_colors;
public:
VertexColoringOracle(qreg color_assignments, qreg edge_conflict_qubits,
vector<tuple<int, int>> starting_colors)
: color_assignments(color_assignments),
edge_conflict_qubits(edge_conflict_qubits),
starting_colors(starting_colors) {}
public:
__qpu__ void apply(qreg register, qubit target) {
// oracle logic
}
}
__qpu__ void grover(qreg register, qubit target, Oracle oracle) {
// grover implementation
// can invoke oracle.apply(register, target) to apply the oracle
}
Note how here we can define a grover
kernel that can work with any oracle that satisfies the Oracle
interface
First off - I've been thinking about this for a long time, and have wanted to incorporate custom class definitions with qpu kernel methods. I think this is a great idea, thank you for adding this Issue tracker for it. There are many reasons one would want to do this.
One question I have about your reason for bringing this up for grover - you mention that for different grover oracles (with different signatures) you have to create a new KernelSignature, and therefore a new grover
function call taking that new KernelSignature. This is certainly correct, but I see the same issue if you elevate the oracle to a class too. For each oracle you would need to define a new class with a different qpu method signature, so you are sort of in the same place you started. Of course you pick up the ability to operate on internal data members, but you would still require a grover library call for each Oracle superclass type. Am I misunderstanding anything here?
You could of course define an Oracle supertype with a very simple apply
with no arguments, and then provide the argument data at Oracle subtype construction (stored as internal members). Then you could have a general grover that took the superclass pointer, and operate on it that way.
To just add on to my thinking on this - we could define like a qclass
macro that is treated like a keyword in the language extension. This macro could rewrite the class definition to be inside the function body of a qpu kernel, and we could define a token collector that parses it, extracts the qpu methods and writes them as standard functions declared before the class definition, and rewrites the entire class definition as the same class but with qpu kernel methods replaced to call the newly created kernel functions.
qclass TestClass {
public:
__qpu__ void qmethod(qreg q) { ...}
};
macro rewrites to
__qpu__ void TestClass(qreg q) {
class TestClass {
...
};
}
TokenCollector rewrites to
__qpu__ internal_qmethod(qreg q) {
...
}
class TestClass {
public:
void qmethod(qreg q) {
internal_qmethod(q);
}
};
We will need to autogen the qpu declared internal_qmethod manually since the SyntaxHandler will have already run (maybe we could forward declare it and it will be picked up immediately after...).
Adding to Alex's comments above, I think in general a Grover oracle can be represented as a KernelSignature<qreg>
.
In case we want to reuse an existing kernel with a different signature, a simple unpack would be sufficient:
__qpu__ void oracle_wrapper(qreg input) {
auto qreg1 = input.extract_range(..);
auto qreg2 = input.extract_range(..);
oracle(qreg1, qreg2);
}
In case there are extra variables that we want to provide to the oracle, the oracle's KernelSignature
could be constructed as a qpu_lambda
as well.