PennyLaneAI / pennylane

PennyLane is a cross-platform Python library for quantum computing, quantum machine learning, and quantum chemistry. Train a quantum computer the same way as a neural network.
https://pennylane.ai
Apache License 2.0
2.29k stars 591 forks source link

QubitDevice creates circuit graph on every call to `execute` #1180

Closed co9olguy closed 3 years ago

co9olguy commented 3 years ago

To keep speeds quick, we'd like to remove construction of graph representations of circuits (aka a "CircuitGraph") from the default code execution paths for constructing and running a circuit. Instead, access should be provided via optional calls, to be invoked only when needed.

It seems however, that QubitDevice always creates a circuit graph when execute is called:

https://github.com/PennyLaneAI/pennylane/blob/528db6b315e7c0466424fbef2675eb797b0d92d8/pennylane/_qubit_device.py#L180

This circuit graph is created in order to generate a hash, but right now it takes place even if caching is turned off!

We should

antalszava commented 3 years ago

Two things come to mind:

How about introducing a new keyword argument needs_hash to QubitDevice? It would be False by default, but it would be set to True for caching and parametric compilation. Then the CircuitGraph construction logic in execute would be conditional on dev.needs_hash.

co9olguy commented 3 years ago

Adding a new keyword argument seems like overkill here.

If this step is only needed by one plugin, we shouldn't force it to be the default behaviour. There's nothing stopping the PL-Forest plugin from doing the graph construction itself, its a one-line call to an already existing method. And since the other use-case is already covered by an existing keyword argument (cache), there's no need to double up

co9olguy commented 3 years ago

@antalszava any ideas how we could obtain a circuit hash more cheaply? Hashes are supposed to be low-overhead bookkeeping mechanisms, it feels unnatural to go through graph construction just to obtain a hash

antalszava commented 3 years ago

That's a good question! Circuit hashes are used to uniquely identify quantum circuits and give a basis for comparison in situations where we can use intermediary results from a circuit execution. It comes down to what the use case is because circuit equivalence could depend on the use case.

Caching

My understanding is that here the circuit equivalence is strict: we compare two circuits by building the circuit graph, serializing the output and comparing the hash of the serialized circuit.

Apart from this, QuantumTape objects represent a quantum circuit. If we can guarantee that the same QuantumTape object is used between qnode evaluations, we could store the id of such objects and skip building the circuit graph. This is not the case at the moment, not sure if it could be feasible on the QNode level 🤔 (maybe @josh146 would have thoughts on this?):

import pennylane as qml

dev = qml.device('default.qubit', wires=3)

def circuit():
    qml.RY(0.4, wires=0)
    return qml.expval(qml.PauliZ(0))

qnode = qml.QNode(circuit, dev)

qnode()

first_id = id(qnode.qtape)

qnode()

second_id = id(qnode.qtape)
assert first_id == second_id

Raises an AssertionError:.

Parametric compilation

This technique is used when a framework compiles to a lower-level quantum programming language that supports symbolic parameters (e.g., quil). We compare two circuits just as for caching, however, the circuit equivalence here is less strict. Two circuits can be equivalent even if the parameters in place of symbolic parameters differ (hence the name parametric). Not quite sure if we could get away with another method other than building the circuit graph, though will think a bit more about it 🤔

On top of these two cases, we also started considering that hashes will be useful for more general compilation in PennyLane (e.g., circuit optimization). Once a circuit is optimized, it can be cached such that the optimized circuit is used on subsequent calls. For this, we'll need a method for circuit comparison (and potentially a similar process to the one used for caching/param compilation).

josh146 commented 3 years ago

@antalszava, correct me if wrong, but I feel that generating the hash from the tape (not the circuit graph) will be fine for most use-cases. True, there are cases where two tapes will have different hashes but result in equivalent circuits, e.g.,

RX(0.2, wires=0)
RY(0.5, wires=1)

and

RY(0.2, wires=1)
RX(0.2, wires=0)

However, tape construction in PL should be fairly deterministic, and should always result in the same order of operations on the tape. Are there any edge cases where this is not guaranteed?

If we hash the tape instead, all we need to take into account is:

to generate the hash value.

If we can guarantee that the same QuantumTape object is used between qnode evaluations, we could store the id of such objects and skip building the circuit graph. This is not the case at the moment, not sure if it could be feasible on the QNode level 🤔

Yep, unfortunately this is not the case, and I think fairly non-trivial due to how Python function closures work

antalszava commented 3 years ago

generating the hash from the tape (not the circuit graph) will be fine for most use-cases

Agreed! Considering the order of operations and symbolic parameters should suffice.

Maybe we could note in the docs that the order of operations is being considered rather than circuit equivalence. One edge case that comes to mind is when the underlying QuantumTape would be swapped for some reason: though this is an unlikely and non-user facing case.

trbromley commented 3 years ago

Thanks @antalszava! Closing this issue since the circuit graph is no longer built for every call to execute (thanks to #1182). We still need to think about making hashing more efficient, as described in #1187.