Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[rfc] Support for writing kernels as methods of a class #204

Open
1ntEgr8 opened this issue Aug 20, 2021 · 4 comments
Open

[rfc] Support for writing kernels as methods of a class #204

1ntEgr8 opened this issue Aug 20, 2021 · 4 comments

Comments

@1ntEgr8
Copy link

1ntEgr8 commented Aug 20, 2021

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

@amccaskey
Copy link
Collaborator

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.

@amccaskey
Copy link
Collaborator

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.

@amccaskey
Copy link
Collaborator

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...).

@1tnguyen
Copy link
Contributor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants