Closed co9olguy closed 3 years ago
Two things come to mind:
circuit
object is being passed to execute
, the cached CircuitGraph
object will be used as per:
https://github.com/PennyLaneAI/pennylane/blob/528db6b315e7c0466424fbef2675eb797b0d92d8/pennylane/tape/tape.py#L900How 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
.
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
@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
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).
@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:
tape.operations
tape.measurements
tape.get_parameters()
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
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.
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.
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 whenexecute
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
if
statement that executes only if caching is requested