microsoft / qsharp-runtime

Runtime components for Q#
https://docs.microsoft.com/quantum
MIT License
285 stars 93 forks source link

IQuantumProcessor extensibility for implementing custom quantum operations #196

Open avasch01 opened 4 years ago

avasch01 commented 4 years ago

Please describe what you would like the feature to accomplish. We need to provide extensibility to IQuantumProcessor interface. Users, especially researchers, should be able to add custom gates/operations to their quantum processors, simulators, emulators, hardware translators, etc. Use case: if some simulator supports a custom gate BCZ, there should be a way ot add it to the interface, implement it on the side of the simulator, and expose it to Q# users so that they can use it when they run against the simulator that supports it. Example: A simulator may support applying a unitary to the state many times more efficiently that decomposing it into elementary operations. It needs a way to define an API "ApplyUnitary(Martix, ArrayOfQubits). Example: An emulator might want to implement a super-efficient way to do state preparation in one shot. Need a custom function that takes in the state and array of qubits. Since we cannot anticipate all usages of this - research is open-ended - we need a flexible way for researchers to add any operation they want to experiment with it.

Describe the solution you'd like IQuantumProcessor should add APIs:

    /// <summary>
    /// Used as extensibility mechanism to add custom operations to IQuantumProcessor. 
    /// </summary>
    /// <remarks>
    /// The custom operation may use only some of the parameters. Depending on the nature of the operation, all of them may be optional. 
    /// A variety of parameter types are provided for flexibility.
    /// </remarks>
    /// <param name="operationId">The id of the custom operation. It is up to the custom operation implementor to define the meaning.</param>
    /// <param name="qubits">Array of qubits to apply the operation to. Their meaning is specific to the custom operation.</param>
    /// <param name="paulis">Array of single-qubit Pauli values. Their meaning is specific to the custom operation.</param>
    /// <param name="longs">Array of long parameters. Their meaning is specific to the custom operation.</param>
    /// <param name="doubles">Array of double parameters. Their meaning is specific to the custom operation.</param>
    /// <param name="bools">Array of bool parameters. Their meaning is specific to the custom operation.</param>
    /// <param name="results">Array of Result parameters. Their meaning is specific to the custom operation.</param>
    /// <param name="strings">Array of string parameters. Their meaning is specific to the custom operation.</param>
    void CustomOperation(long operationId, IQArray<Qubit> qubits, IQArray<Pauli> paulis, IQArray<long> longs, IQArray<double> doubles, IQArray<bool> bools, IQArray<Result> results, IQArray<string> strings);

    /// <summary>
    /// Used as extensibility mechanism to add custom operations to IQuantumProcessor. Implements controlled version.
    /// </summary>
    /// <remarks>
    /// The custom operation may use only some of the parameters. Depending on the nature of the operation, all of them may be optional. 
    /// A variety of parameter types are provided for flexibility.
    /// </remarks>
    /// <param name="operationId">The id of the custom operation. It is up to the custom operation implementor to define the meaning.</param>
    /// <param name="controls">Array of control qubits for this operation.</param>
    /// <param name="qubits">Array of qubits to apply the operation to. Their meaning is specific to the custom operation.</param>
    /// <param name="paulis">Array of single-qubit Pauli values. Their meaning is specific to the custom operation.</param>
    /// <param name="longs">Array of long parameters. Their meaning is specific to the custom operation.</param>
    /// <param name="doubles">Array of double parameters. Their meaning is specific to the custom operation.</param>
    /// <param name="bools">Array of bool parameters. Their meaning is specific to the custom operation.</param>
    /// <param name="results">Array of Result parameters. Their meaning is specific to the custom operation.</param>
    /// <param name="strings">Array of string parameters. Their meaning is specific to the custom operation.</param>
    void ControlledCustomOperation(long operationId, IQArray<Qubit> controls, IQArray<Qubit> qubits, IQArray<Pauli> paulis, IQArray<long> longs, IQArray<double> doubles, IQArray<bool> bools, IQArray<Result> results, IQArray<string> strings);

Describe alternatives you've considered IQuantumProcessor is easy to use to implement simulators. We considered alternatives such as extensibility through SimulatorBase, but it requires too much code to write, and our customers refuse to participate. With the proposed approach it is very easy to implement a simulator, and to add a custom operation. This approach allows for easy mirroring of this solution to the native C code through interop. Some other considered solutions relied on features such as reflection, so they did not allow for this interoperability. So, for alternative solutions, I have two requirements:

  1. It must be as easy to the customer to use: just implement one function on the interface and you are done.
  2. It must be suitable for native code: no managed-only features dependency.
bettinaheim commented 4 years ago

Hi @avasch01, Thanks for filing! I would like to start with the concrete need to satisfy, rather than focusing on a particular implementation - let's task about the functionality (what do you want the capability to be?) so we can better assess what options we have.

I believe the feature request is the following: You have a custom simulator that implements a certain set of quantum operations. You would like to use those operations as part of your Q# program. One way to do that today is to have a Q# library that defines these operations as intrinsic and define suitable C# classes that provide the necessary implementation as part of you simulator implementation. Today, this would look something like this:

        // inner class within your simulator: 
        public class QSimR : Intrinsic.R
        {
            // assuming your simulator is written in C++
            [DllImport(QSIM_DLL_NAME, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl, EntryPoint = "R")]
            private static extern void R(uint id, Pauli basis, double angle, uint qubit);

            // ... constructor etc

            public override Func<(Pauli, double, Qubit), QVoid> Body => (_args) =>
            {
                // ... implementation invoking R
            };

            // ... more code

This is not particularly straight-forward to implement, so the ask would be to make this easier. Would something that instead looks like this for your simulator implementation meet your need:

        // no class needed, but instead directly within your simulator: 

        [DllImport(QSIM_DLL_NAME, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl, EntryPoint = "R")]
        private static extern void _R(uint id, Pauli basis, double angle, uint qubit);

        public override void R (Pauli axis, double angle, Qubit target)
        {
            // ... implementation invoking _R
        };

A custom simulator then would only inherit from our base class, and implement whatever overrides it wants to specify explicitly. That should be fairly minimal in terms of the code that is needed to define a custom simulator.

swernli commented 4 years ago

Here are some thoughts I had on providing the boilerplate for quantum processor extensions. I'm hoping it can be fodder for further ideas and brainstorming.

We could define an interface called IQuantumProcessorExtensions, and have SimulatorBase inherit from that in addition to IQuantumProcessor. This interface would actually be something that is then produced during our C# generation step, empty by default. Similarly, we'd have an IQuantumDispatcherExtensions that the default dispatcher inherits from, also produced by C# generation and empty by default. Then as part of C# generation, we would look for methods with a special annotation, something like:

@ProcessorExtension("Csharp.Namespace.For.Extension")
operation W(q0 : Qubit, q1 : Qubit, q2 : Qubit) : Unit is Adj + Ctl {
    body intrinsic;
}

Then the C# generation would automatically generate a processor extension and dispatcher extension that handle dispatching to a wrapper around an invocation of W from the given namespace. We could also have an alternate version, @NativeProcessorExtension that takes a dll name and any relevant signature info such that the generated processor extension instead wraps a call to a native method. In both managed and native code, we would establish a calling convention where the signature would include a reference/pointer to a simulator object and then the remainder of the arguments based on the operation definition.

This would hopefully meet the goals of having simple interface definition (just writing the Q# operation with the appropriate annotation and having the rest generated for you) and be compatible with C# or C++ depending on which annotation is used. Maybe even future annotations for extensions in other languages could be introduced, like Python or Rust, etc.

Possibly challenging would be the fact that these extensions would need to be static and thus everything they would need to access from the simulator would have to have appropriate accessors on the provided simulator object.

cgranade commented 4 years ago

Thanks for sharing your thoughts, @swernli!

@ProcessorExtension("Csharp.Namespace.For.Extension")
operation W(q0 : Qubit, q1 : Qubit, q2 : Qubit) : Unit is Adj + Ctl {
    body intrinsic;
}

My main concern with this approach would be that the Q# source code is aware of the C# runtime implementation, making it difficult for the Q# intrinsic definitions to be used across different simulators or hardware. We should likely add something along these lines to the Q# API Design Principles, but ideally, Q# APIs shouldn't encourage writing quantum programs that can only be simulated, or that behave in fundamentally different ways when run on a simulator and on actual hardware.

From that perspective, it's important that operations which enable fast-paths for simulation have pure Q# fallbacks, and don't expose implementation details of the runtime.

swernli commented 4 years ago

@cgranade That’s an excellent point. Putting any C# or C++ references directly in the Q# limits its flexibility and portability. I wonder if there is a more generic, portable way to add additional info to operations defined as “body intrinsic” that limits the infrastructure/boilerplate developers need to implement themselves and also makes it easier to incorporate new native code into an existing simulator. I’ll think about that some more.

cgranade commented 4 years ago

I wonder if there is a more generic, portable way to add additional info to operations defined as “body intrinsic” that limits the infrastructure/boilerplate developers need to implement themselves and also makes it easier to incorporate new native code into an existing simulator.

Thanks for the discussion, @swernli! To expand a bit on my thoughts above, at least part of the issue is that in many cases, for portability you'll want simulator-specific intrinsics to have pure-Q# fallbacks. In principle, that allows for a Q# program to be operationally correct across simulators (and eventually hardware) while still allowing a fast-path for simulators that admit a more efficient implementation. That's distinct from the use case of exposing machine-specific functionality (e.g.: Mølmer–Sørensen), but covers cases such as estimating measurement frequencies (common in variational algorithms) and implementing state preparation and unitary evolution without needing to explicitly run each step in the pure-Q# decomposition.

As a consequence, the simulator-specific Q# operations will quite often need to be correct Q# operations in and of themselves, such that we'll need to keep that in mind with a proposed solution.

bettinaheim commented 4 years ago

@cgranade @swernli I actually don't have an issue with annotating the Q# code in this case, though instead of being intrinsic it should have an implementation. Here is my thinking: If you want to build a custom simulator that exposes a particular gate that is not usually defined as part of our intrinsics, then you also by necessity need to provide a library with it defining that gate. That library then should also have the Q# implementation, such that the code would run anywhere. I don't see in that sense how the code based on that library wouldn't remain generally applicable. Attributes in a sense are supposed to contain whatever metadata you would like to annotate.

alan-geller commented 4 years ago

@cgranade @swernli @bettinaheim Stefan's proposal is very similar to a feature that was in the original Q# spec, but never implemented, called externals. The notion was that you could define a function or operation that was implemented by an arbitrary .NET routine, with the routine name provided as part of the declaration. Moving the routine name into an attribute makes a lot of sense.

I like @cgranade 's suggestion of allowing such callables to also provide a Q# implementation as a fallback. I could imagine, in such a case, providing some sort of runtime (or perhaps compile-time) switch to say whether such an operation should be executed using the external routine or the callback.

I think this is pretty easy to address as part of the code gen. There are several options for the generated code; I'll write up a proposal.

alan-geller commented 4 years ago

@cgranade @swernli @bettinaheim Here's a proposal:

  1. The actual quantum gates implemented by a target (simulator or not) should be defined by a Q# file that is effectively an interface spec for the target. This would specify the actual low-level physical operations that the target supports. The namespace should be target-specific.
  2. There should be a second body of Q# code, whether in the same file or not, that implements the operations from the Microsoft.Quantum.Intrinsics namespace in terms of the physical operations for the target. This would have to be in the Microsoft.Quantum.Intrinsics namespace, and effectively replaces the default implementation of that namespace.
  3. The above two items, together, make up a library that a Q# program can be linked against. It's also possible to link dynamically in order to select a target at runtime, since in both cases Q# application code is written to the API defined by the Microsoft.Quantum.Intrinsics namespace.
  4. The Q# compiler should have a mode where you can run it against the target interface in item 1 that causes it to code gen a subclass of Microsoft.Quantum.Simulation.Common.SimulatorBase. For each physical operation that the target supports, an appropriate subclass of AbstractCallable is generated. For each specialization of that operation that is supported, a stub is generated in the subclass that unwraps the argument tuple, calls a method on the generated target-wide class, and wraps the result as needed. The SimulatorBase subclass would have all of the stub methods that were required as part of the callable generation.
  5. If the target-specific namespace imports the target-specific namespace for another target, the target's SimulatorBase subclass should be implemented as a subclass of the imported target's SimulatorBase subclass. This makes it easy to add an emulated operation to an existing simulator.

There are (obviously) some details to be worked out, but I think this approach is both general and simple. I like the idea of explicitly defining an "interface" for each target -- I think @vadym-kl and I talked about that several years ago. This would also let us drop the QuantumProcessor special case, since it would just become another generated target simulator class.

Thoughts?

bettinaheim commented 4 years ago

I think we are getting to a good plan. Let's pursue this as part of our larger refactoring to pull in target specific decompositions.